GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [v1.x] Give recursive tool return types an object-rooted output schema by maxisbey · Pull Request #3377 · modelcontextprotocol/python-sdk

Conversation

@maxisbey

@maxisbey maxisbey commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Backport of #3376 to the v1.x line. Refs #3337.

When a FastMCP tool's return type is self-referential, pydantic emits the output schema as {"$defs": {...}, "$ref": "#/$defs/Node"} with no type at the root. Tool.outputSchema requires type: "object" at the root on every protocol version v1.x speaks, and strict clients — TypeScript SDK 1.x, C# SDK 1.x, and python-sdk 2.x on a 2025-11-25 session — reject the entire tools/list result when one tool publishes that shape. This inlines the referenced definition onto the root and keeps $defs for the nested references.

Motivation and Context

On v1.x nothing errors on the Python side (Tool.outputSchema is dict[str, Any] and isn't shape-checked), so the failure shows up in the peer: a host embedding the TypeScript SDK 1.x sees the server as having no tools at all. That's the same report as PrefectHQ/fastmcp#2455. Given the blast radius against the most common client family and the size of the change, this seemed worth carrying on the maintenance line.

How Has This Been Tested?

  • test_structured_output_self_referential_model_gets_an_object_root pins the generated shape and round-trips a nested result.
  • test_tool_structured_output_self_referential_model drives an in-memory client session through tools/list and tools/call; both fail before the change.
  • Fed the resulting wire ListToolsResult to the TypeScript SDK 1.x ListToolsResultSchema: accepted after the change, tools.0.outputSchema.type Invalid input: expected "object" before. Also valid against the spec's schema/2025-11-25/schema.json.

Breaking Changes

None. The only observable change is the JSON content of outputSchema for recursive return types: the root gains the definition's keys, $defs is unchanged, structuredContent is unchanged.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Same out-of-scope note as #3376: a RootModel whose root isn't an object still publishes a non-object root.

AI Disclaimer

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@maxisbey
[v1.x] Give recursive tool return types an object-rooted output schema
d522df1
pydantic emits a self-referential model as {"$defs": {...}, "$ref": "#/$defs/Model"}
with no type at the root. Tool.outputSchema requires type: object at the root, and
strict clients (TypeScript SDK 1.x, C# SDK 1.x, python-sdk 2.x on a 2025-11-25
session) reject the entire tools/list result when one tool publishes that shape.

Inline the referenced definition onto the root when the generated schema is a
bare local $ref, keeping $defs for the nested references. Backport of the fix on
main.

Github-Issue: #3337
cubic-dev-ai[bot]
cubic-dev-ai Bot reviewed Aug 24, 2026
View reviewed changes

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Re-trigger cubic

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
claude[bot]
claude Bot reviewed Aug 24, 2026
View reviewed changes
Comment thread src/mcp/server/fastmcp/utilities/func_metadata.py
ref = schema.get("$ref")
if not isinstance(ref, str) or not ref.startswith(_LOCAL_DEFS_PREFIX):
return schema
definition = cast(dict[str, Any], schema["$defs"][ref.removeprefix(_LOCAL_DEFS_PREFIX)])

@claude claude Bot Aug 24, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor/edge: _inline_root_ref does unguarded schema["$defs"][name] lookups, so a root-level $ref without a matching local definition raises KeyError instead of being passed through

Extended reasoning...

A user customizes a return model's schema (e.g. model_config = ConfigDict(json_schema_extra={"$ref": "#/$defs/X"}) or a custom schema generator) so the generated schema has a root $ref starting with #/$defs/ but no $defs key or no X entry. Before this change the schema was published as-is; after it, func_metadata raises an uncaught KeyError inside _try_create_model_and_schema (the surrounding try only wraps model_json_schema), so mcp.tool() registration crashes at import/startup instead of registering the tool.

Verification: nit — Line 62 of src/mcp/server/fastmcp/utilities/func_metadata.py performs unguarded lookups: definition = cast(dict[str, Any], schema["$defs"][ref.removeprefix(_LOCAL_DEFS_PREFIX)]). The guard at lines 59-61 only checks that $ref is a string starting with "#/$defs/"; it never checks that $defs exists or contains the referenced name. The enclosing try/except in `_try_create_model_and_sc

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

claude[bot] reacted with thumbs up emoji claude[bot] reacted with thumbs down emoji
All reactions
  • 👍 1 reaction
  • 👎 1 reaction
@maxisbey
maxisbey merged commit b222713 into v1.x Aug 24, 2026
29 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@maxisbey
maxisbey deleted the fix/v1x-recursive-output-schema-object-root branch August 24, 2026 18:12
@coderabbitai coderabbitai Bot mentioned this pull request Sep 20, 2026
fix: Set FastMCP settings directly on mcp instance (mcp 1.27.0+) spkane/freecad-addon-robust-mcp-server#125
Closed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@cubic-dev-ai cubic-dev-ai[bot] cubic-dev-ai[bot] left review comments
@claude claude[bot] claude[bot] left review comments

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

1 participant

@maxisbey
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.