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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions