Skip to content

Give recursive tool output schemas an object root - #3368

Closed
Parker-Fawcett wants to merge 1 commit into
modelcontextprotocol:mainfrom
Parker-Fawcett:fix/3337-recursive-tool-output-schema-object-root
Closed

Give recursive tool output schemas an object root#3368
Parker-Fawcett wants to merge 1 commit into
modelcontextprotocol:mainfrom
Parker-Fawcett:fix/3337-recursive-tool-output-schema-object-root

Conversation

@Parker-Fawcett

Copy link
Copy Markdown

Give recursive tool output schemas an object root

Fixes #3337

What

A tool whose return type is self-referential publishes {"$defs": {...}, "$ref": "#/$defs/Node"} with no root "type". The 2025-11-25 OutputSchema model requires type: Literal["object"] at the root, so serving tools/list to a legacy-negotiated session failed validation for the entire listing — legacy clients lost every tool, not just the recursive one.

Reproduced on main: a MCPServer with one recursive-BaselineModel tool serves modern sessions fine, but Client(mcp, mode="legacy") gets MCPError: Handler returned an invalid result.

How

Normalization happens where derivation already lives — FuncMetadata.model_post_init, next to the existing StrictJsonSchema call. When pydantic emits a root that is a bare $ref to an object def, the schema is published as:

{"type": "object", "allOf": [{"$ref": "#/$defs/Node"}], "$defs": {"Node": {...}}}

Wrapping rather than inlining is what terminates on recursive models. Sibling $ref + type would also satisfy the model; wrapping keeps it draft-agnostic.

Scope guards:

  • Refs to non-object defs are left alone (only object-typed defs get wrapped).
  • Hand-built output_schemas are untouched — "a schema published without a model is advertised but not validated", and that stays true.
  • Modern sessions see no change for any non-recursive tool (pydantic only produces bare-$ref roots for self-referential types).

The reporter's separate question — whether one unrepresentable tool should degrade instead of failing the listing — is a design decision deliberately left out of this PR.

Tests

  • Unit (test_func_metadata.py): exact-schema assertion for a recursive BaseModel return — root type, allOf-wrapped ref, $defs preserved.
  • End-to-end (test_server.py): tools/list succeeds on both Client(mcp) and Client(mcp, mode="legacy") with a recursive tool registered.

Both fail on main exactly as #3337 describes. Full gate green locally: ./scripts/test, ruff format/check, pyright.

(AI-assisted implementation, prepared with a coding agent and reviewed by me.)

Pydantic emits a bare $ref root for self-referential return types,
which the published tool schema contract rejects: on 2025-11-25
sessions the whole tools/list result failed validation, so legacy
clients lost every tool, not just the recursive one.

Generated schemas whose root is a $ref to an object def are now
published as {"type": "object", "allOf": [$ref]} with $defs kept,
which terminates where inlining the root would not. Hand-built
schemas are untouched.

Fixes modelcontextprotocol#3337
@github-actions github-actions Bot added the missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md) label Aug 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the contribution. This repository only keeps pull requests open when they're linked to an issue that a maintainer has assigned to the author — CONTRIBUTING.md explains why and how we work. This PR has been closed for now because you aren't currently assigned to #3337.

If a maintainer would like this change as a PR from you, they'll assign you to #3337 and this PR will reopen automatically — there's nothing more you need to do. (If you opened the issue, this PR already shows up on its timeline.)

There's no need to open a new PR — this one will be reopened. While it's closed, please push any updates as new commits rather than force-pushing, since GitHub can't reopen a PR whose branch has been rewritten.

Maintainers: reopening this PR, removing the missing-issue-link label, or adding bypass-issue-check bypasses the check.

@github-actions github-actions Bot closed this Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recursive tool return type publishes an outputSchema with no root type, failing tools/list on 2025-11-25 sessions

1 participant