Skip to content

Backport: Dereference $ref in tool schemas for MCP client compatibility#2861

Merged
jlowin merged 2 commits intorelease/2.xfrom
backport/2814-schema-dereferencing
Jan 13, 2026
Merged

Backport: Dereference $ref in tool schemas for MCP client compatibility#2861
jlowin merged 2 commits intorelease/2.xfrom
backport/2814-schema-dereferencing

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Jan 13, 2026

Backports #2814 to release/2.x for inclusion in a 2.x release.

MCP clients like VS Code Copilot and Claude Desktop strip $defs before sending schemas to the LLM, leaving dangling $ref references that break enum parameters and complex types. This adds automatic dereferencing of all $ref entries at tool creation time.

# Before: Schema with $ref (breaks in VS Code Copilot)
{"$defs": {"Status": {"enum": ["active", "inactive"]}}, "properties": {"status": {"$ref": "#/$defs/Status"}}}

# After: Dereferenced schema (works everywhere)  
{"properties": {"status": {"enum": ["active", "inactive"]}}}

For circular schemas that can't be fully dereferenced, falls back to resolving only root-level $ref.

@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. v2 labels Jan 13, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 13, 2026

Walkthrough

The pull request adds JSON Schema dereferencing capabilities to FastMCP. Changes include documentation updates explaining automatic $ref inlining in tool schemas, and implementation of new dereferencing functions in the JSON Schema utilities module. Three new functions are introduced: dereference_refs() to inline $ref references, resolve_root_ref() to handle root-level references, and _merge_ref_siblings() to restore sibling keywords after dereferencing. Existing utility functions are updated with consistent type annotations, and the jsonref library is imported to support the dereferencing operations.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the problem, solution, and provides a concrete example, but is missing the required contributors checklist items and self-review confirmation. Complete the Contributors Checklist by checking the appropriate boxes and fill in the Review Checklist to indicate self-review completion and readiness for review.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding automatic dereferencing of $ref entries in tool schemas for MCP client compatibility.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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)

38-61: Consider moving the success return into an else block for clarity.

The static analysis tool flagged that the return dereferenced on line 56 could be moved into an else block to make the control flow more explicit. This is a minor style improvement.

♻️ Optional refactor to address TRY300
     try:
         # Use jsonref to resolve all $ref references
         # proxies=False returns plain dicts (not proxy objects)
         # lazy_load=False resolves immediately
         dereferenced = replace_refs(schema, proxies=False, lazy_load=False)

         # Merge sibling keywords that were lost during dereferencing
         # Pydantic puts description, default, examples as siblings to $ref
         defs = schema.get("$defs", {})
         merged = _merge_ref_siblings(schema, dereferenced, defs)
         # Type assertion: top-level schema is always a dict
         assert isinstance(merged, dict)
         dereferenced = merged

         # Remove $defs since all references have been resolved
         if "$defs" in dereferenced:
             dereferenced = {k: v for k, v in dereferenced.items() if k != "$defs"}

-        return dereferenced
-
     except JsonRefError:
         # Self-referencing/circular schemas can't be fully dereferenced
         # Fall back to resolving only root-level $ref (for MCP spec compliance)
         return resolve_root_ref(schema)
+    else:
+        return dereferenced
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 559b778 and e956422.

⛔ Files ignored due to path filters (4)
  • .loq_cache is excluded by none and included by none
  • pyproject.toml is excluded by none and included by none
  • tests/utilities/test_json_schema.py is excluded by none and included by none
  • uv.lock is excluded by !**/*.lock and included by none
📒 Files selected for processing (2)
  • docs/servers/tools.mdx
  • src/fastmcp/utilities/json_schema.py
🧰 Additional context used
📓 Path-based instructions (2)
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)

docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...

Files:

  • docs/servers/tools.mdx
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Python ≥3.10 with full type annotations required for all code
Never use bare except - be specific with exception types in Python code

Files:

  • src/fastmcp/utilities/json_schema.py
🧠 Learnings (3)
📚 Learning: 2026-01-12T16:24:54.978Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:54.978Z
Learning: Applies to src/tools/**/*.{ts,tsx,js,jsx} : Changes affecting MCP Tools (like adding tags, importing, etc.) must be adopted, applied, and tested consistently

Applied to files:

  • docs/servers/tools.mdx
📚 Learning: 2026-01-13T03:11:40.907Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T03:11:40.907Z
Learning: Applies to **/*.py : Python ≥3.10 with full type annotations required for all code

Applied to files:

  • docs/servers/tools.mdx
📚 Learning: 2026-01-13T03:11:40.907Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T03:11:40.907Z
Learning: Applies to **/__init__.py : Be intentional about module re-exports - only re-export fundamental types to fastmcp.*; prefer users importing from specific submodules

Applied to files:

  • docs/servers/tools.mdx
🪛 Ruff (0.14.10)
src/fastmcp/utilities/json_schema.py

56-56: Consider moving this statement to an else block

(TRY300)

⏰ 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: Python 3.10 on windows-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
🔇 Additional comments (4)
src/fastmcp/utilities/json_schema.py (3)

64-128: LGTM!

The recursive merge function correctly handles:

  • Sibling keyword restoration for $ref nodes (description, default, examples)
  • Circular reference prevention via the visited set
  • Parallel traversal of dict and list structures
  • Local siblings taking precedence over definition content (line 107)

The implementation is well-documented and handles edge cases properly.


170-170: LGTM!

The type annotation updates to use dict[str, Any] are consistent with Python 3.10+ style and align with the project's type annotation requirements. Based on learnings, Python ≥3.10 with full type annotations is required for all code.

Also applies to: 191-196, 364-370


6-7: Imports from jsonref are appropriate and properly declared.

The jsonref>=1.1.0 dependency is listed in pyproject.toml. Both imports (JsonRefError and replace_refs) are used correctly in the new dereferencing functions, with proper full type annotations and specific exception handling throughout.

docs/servers/tools.mdx (1)

176-178: LGTM!

The documentation note clearly explains the automatic $ref dereferencing behavior in a concise manner. The note:

  • Uses clear, direct language appropriate for technical audiences
  • Correctly identifies the affected MCP clients (VS Code Copilot, Claude Desktop)
  • Is placed appropriately before the Type Annotations section where users learn about complex types

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e956422ace

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/servers/tools.mdx
Comment on lines +176 to +178
<Note>
FastMCP automatically dereferences `$ref` entries in tool schemas to ensure compatibility with MCP clients that don't fully support JSON Schema references (e.g., VS Code Copilot, Claude Desktop). This means complex Pydantic models with shared types are inlined in the schema rather than using `$defs` references.
</Note>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wire dereference_refs into tool schema generation

The docs state that FastMCP automatically dereferences $ref in tool schemas, but the new dereference_refs helper is never called from tool schema generation (schemas are still built via compress_schema in src/fastmcp/tools/tool.py and src/fastmcp/tools/tool_transform.py). As a result, $ref/$defs remain in tool schemas and clients that strip $defs will still break; integrate dereference_refs into the schema pipeline or remove this note.

Useful? React with 👍 / 👎.

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Windows test timeout in test_enhanced_error_responses.py unrelated to PR changes.

Root Cause: The test failure occurs during fixture initialization in oauth_proxy fixture at tests/server/auth/test_enhanced_error_responses.py:293. The timeout happens when creating a DiskStore from the key_value package, which uses diskcache.Cache for persistent storage. This initialization is hitting the 5-second test timeout on Windows (tests use pytest-timeout with timeout = 5.0s).

The stack trace shows:

File "tests\server\auth\test_enhanced_error_responses.py", line 293, in oauth_proxy
  return OAuthProxy(
File "src\fastmcp\server\auth\oauth_proxy.py", line 822, in __init__
  key_value=DiskStore(directory=settings.home / "oauth-proxy"),
File "diskcache\core.py", line 591, in __init__
  self._sql  # pylint: disable=pointless-statement
+++++++++++++++++++++++++++++++++++ Timeout +++++++++++++++++++++++++++++++++++

Why This Is Unrelated to PR Changes: This PR only modifies:

  • src/fastmcp/utilities/json_schema.py (schema dereferencing logic)
  • tests/utilities/test_json_schema.py (tests for schema dereferencing)
  • Documentation and lock files

None of these changes affect OAuth proxy initialization or disk storage operations.

Suggested Solution:

  1. Mark the test with a higher timeout on Windows - Add a pytest marker to increase timeout for this specific test file on Windows:
# At the top of tests/server/auth/test_enhanced_error_responses.py
import sys
import pytest

pytestmark = pytest.mark.timeout(15 if sys.platform == 'win32' else 5)
  1. Or use an in-memory store for tests - Modify the oauth_proxy fixture to use an in-memory key-value store instead of DiskStore to avoid slow disk I/O:
# In the oauth_proxy fixture
from key_value.aio.stores import MemoryStore

return OAuthProxy(
    # ... other params ...
    key_value_store=MemoryStore(),  # Instead of letting it default to DiskStore
)
Detailed Analysis

Platform-Specific Behavior: The test passes on ubuntu-latest (3207 passed) but times out on windows-latest. This is a known issue with Windows having slower file I/O, especially for SQLite operations through diskcache.

Test File: tests/server/auth/test_enhanced_error_responses.py:287-306

The failing fixture creates an OAuthProxy with default disk storage, which initializes a SQLite database. On Windows, this can take longer than 5 seconds.

Files Changed in This PR:

  • .loq_cache
  • docs/servers/tools.mdx
  • pyproject.toml
  • src/fastmcp/utilities/json_schema.py
  • tests/utilities/test_json_schema.py
  • uv.lock

None of these touch OAuth, authentication, or disk storage code.

Related Files
  • tests/server/auth/test_enhanced_error_responses.py:293 - Failing fixture
  • src/fastmcp/server/auth/oauth_proxy.py:822 - OAuthProxy initialization with DiskStore
  • pyproject.toml - Contains timeout = 5.0s configuration
  • Windows runner exhibits slower disk I/O compared to Linux runner

@jlowin jlowin merged commit bc2f601 into release/2.x Jan 13, 2026
14 of 15 checks passed
@jlowin jlowin deleted the backport/2814-schema-dereferencing branch January 13, 2026 12:46
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. v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant