Skip to content

Add dereference helper for tool inputSchema with nested Pydantic models #2586

@Burton-David

Description

@Burton-David

Problem

When a tool's inputSchema is built from a Pydantic model containing nested BaseModel types, model_json_schema() emits a \$defs block with named definitions and \$ref pointers at each use site. The output is valid JSON Schema 2020-12, but some MCP clients (notably Claude Desktop during the tools/list discovery flow) don't resolve internal \$ref references — tools with nested types in their inputs become invisible or unusable in those clients.

The current workaround that downstream MCP servers reach for is "flatten every input model by hand" — this leaks a Pydantic limitation into the public API surface of every MCP author who hits the issue. In our case (research-mcp, ~14 tools), we hand-flattened all input models.

Proposed shape

A small, additive helper that post-processes the output of model_json_schema():

```python
from mcp.server.mcpserver.utilities.json_schema import dereference_json_schema

flat = dereference_json_schema(MyToolInput.model_json_schema())

`flat` has no $defs; every internal $ref has been inlined.

```

  • Pure function. Doesn't mutate the input.
  • Only handles internal `#/$defs/` refs. External refs (URLs, pointers into other documents) preserved verbatim.
  • Self-referential and mutually recursive definitions are handled safely: `$ref` is preserved at the cycle boundary and the relevant `$defs` entries are retained so the output schema stays valid.
  • Sibling keys to `$ref` (allowed by JSON Schema 2020-12; emitted by Pydantic for some types) are merged into the resolved object, with sibling values overriding the resolved definition's values (matches the JSON Schema 2020-12 semantics for sibling annotations).

Default behavior of the SDK is unchanged — this is purely additive. Callers opt in. Existing tools/users who prefer the compact `$ref` form are unaffected.

Why a helper rather than a flag on `model_json_schema`

A flag on `model_json_schema` would have to live in Pydantic. A helper in the SDK is a one-line change at the call site for users who need it, and doesn't require coordinating across projects.

Implementation

I have a working implementation with 21 tests covering: flat schemas, single-level inlining, transitive resolution, arrays, anyOf, sibling key merging, external refs, unknown internal refs, direct self-reference, mutual recursion, and idempotency. All existing `tests/server/mcpserver/test_func_metadata.py` and `test_tool_manager.py` tests still pass (no regressions).

Happy to open a PR once this is `ready for work`.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions