Skip to content

Fix: resolve root-level $ref in outputSchema for MCP spec compliance#2727

Merged
jlowin merged 1 commit intorelease/2.xfrom
cherry-pick-2720
Dec 25, 2025
Merged

Fix: resolve root-level $ref in outputSchema for MCP spec compliance#2727
jlowin merged 1 commit intorelease/2.xfrom
cherry-pick-2720

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Dec 25, 2025

Cherry-pick of #2720 to release/2.x.

Resolves root-level $ref in outputSchema for self-referential Pydantic models to meet MCP spec requirement for type: object at root.

Closes #2455

@marvin-context-protocol marvin-context-protocol Bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality. labels Dec 25, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 25, 2025

Walkthrough

This pull request adds a utility function to resolve root-level JSON Schema $ref references and applies it during tool output schema generation. A new resolve_root_ref() function in src/fastmcp/utilities/json_schema.py inlines root-level $ref pointers to local $defs entries while preserving nested definition references. The function is invoked in src/fastmcp/tools/tool.py immediately after schema compression, ensuring that self-referential Pydantic models produce schemas with an explicit object type at the root, as required by the MCP specification.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is missing critical sections from the template such as Contributors Checklist and Review Checklist that are required per repository guidelines. Add the Contributors Checklist and Review Checklist sections from the template with appropriate checkboxes marked to indicate compliance.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: resolving root-level $ref in outputSchema to meet MCP spec compliance, which directly matches the changeset.
Linked Issues check ✅ Passed The code changes fully address issue #2455 by implementing root-level $ref resolution via resolve_root_ref, ensuring MCP-compliant schemas with type: object at root while preserving $defs.
Out of Scope Changes check ✅ Passed All changes are scoped to the linked issue objectives: added resolve_root_ref utility, integrated it into FunctionTool schema generation, with no extraneous modifications.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cherry-pick-2720

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/fastmcp/utilities/json_schema.py (1)

7-43: Consider shallow copy implications and potential $defs conflict.

The implementation correctly resolves root-level $ref references for MCP compliance. However, there are two edge cases to be aware of:

  1. Shallow copy on line 39: dict(defs[def_name]) creates a shallow copy. If the referenced definition contains nested dicts or lists, they'll be shared between the original and resolved schema. This is likely acceptable for read-only schema generation, but could cause issues if schemas are modified after resolution.

  2. $defs overwrite on line 41: If the referenced definition already contains its own $defs key (though uncommon in typical JSON Schema patterns), it will be overwritten by the root-level $defs. Consider checking for this case or documenting the behavior.

🔎 Optional enhancement to preserve nested $defs

If you want to handle the edge case where a referenced definition might have its own $defs:

                # Create a new schema by copying the referenced definition
                resolved = dict(defs[def_name])
-                # Preserve $defs for nested references (other fields may still use them)
-                resolved["$defs"] = defs
+                # Preserve $defs for nested references, merging if necessary
+                if "$defs" in resolved:
+                    # Merge with root $defs, preferring root definitions
+                    resolved["$defs"] = {**resolved["$defs"], **defs}
+                else:
+                    resolved["$defs"] = defs
                return resolved
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f56291 and 4efc52c.

⛔ Files ignored due to path filters (2)
  • tests/tools/test_tool.py is excluded by none and included by none
  • tests/utilities/test_json_schema.py is excluded by none and included by none
📒 Files selected for processing (2)
  • src/fastmcp/tools/tool.py
  • src/fastmcp/utilities/json_schema.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python ≥ 3.10 with full type annotations
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/tools/tool.py
  • src/fastmcp/utilities/json_schema.py
🧬 Code graph analysis (1)
src/fastmcp/tools/tool.py (1)
src/fastmcp/utilities/json_schema.py (2)
  • compress_schema (240-270)
  • resolve_root_ref (7-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
🔇 Additional comments (3)
src/fastmcp/utilities/json_schema.py (1)

4-4: LGTM: Import addition is appropriate.

The Any import is necessary for the type annotations in the new resolve_root_ref function.

src/fastmcp/tools/tool.py (2)

35-35: LGTM: Import addition is clean.

The resolve_root_ref import is added cleanly alongside the existing compress_schema import.


562-564: Excellent placement for root $ref resolution.

The call to resolve_root_ref is correctly positioned after schema compression and before returning the ParsedFunction instance. This ensures that self-referential Pydantic models produce MCP-compliant schemas with "type": "object" at the root level while preserving $defs for nested references. The explanatory comment is helpful for future maintainers.

@jlowin jlowin merged commit 4963b51 into release/2.x Dec 25, 2025
11 checks passed
@jlowin jlowin deleted the cherry-pick-2720 branch December 25, 2025 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants