Skip to content

Make $ref dereferencing optional via FastMCP(dereference_refs=...)#3151

Merged
jlowin merged 7 commits intomainfrom
optional-dereference-refs
Feb 11, 2026
Merged

Make $ref dereferencing optional via FastMCP(dereference_refs=...)#3151
jlowin merged 7 commits intomainfrom
optional-dereference-refs

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Feb 11, 2026

compress_schema() unconditionally inlined all $ref/$defs, which can more than double schema size for complex Pydantic models. This was added for MCP client compatibility (VS Code Copilot doesn't handle $ref), but penalizes every server regardless of client.

Dereferencing is now controlled by a per-server kwarg and implemented as middleware that runs at serve-time rather than at schema creation time. Schemas are stored with $ref intact and only inlined when sent to clients via list_tools / list_resource_templates. The default is True (dereference on) since the non-compliant clients are popular and the failure mode — silent breakage only visible in certain clients — is much worse than the cost of larger schemas.

# Default: dereference for maximum compatibility
mcp = FastMCP("my-server")

# Opt out when you know your clients handle $ref
mcp = FastMCP("my-server", dereference_refs=False)

Closes #3141

@marvin-context-protocol marvin-context-protocol Bot added enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality. labels Feb 11, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 11, 2026

Caution

Review failed

The head commit changed during the review from 614bdb4 to 4965c96.

Walkthrough

Adds an opt-out for JSON Schema $ref dereferencing: FastMCP.init gains a dereference_refs: bool = True parameter that conditionally injects a new DereferenceRefsMiddleware to inline $ref when listing tools and resource templates. compress_schema gains a dereference: bool = False parameter; the function now always resolves root-level $ref and prunes unused $defs, and performs inlining only when dereference=True. Documentation files are updated to describe the middleware, the new constructor flag, and the extended compress_schema signature.

Possibly related PRs

  • jlowin/fastmcp PR 2861: Implements JSON Schema dereferencing and a dereference_refs helper, overlapping the dereferencing/inlining logic added here.
  • jlowin/fastmcp PR 2720: Changes resolve_root_ref() and related schema-resolution behavior that compress_schema now invokes unconditionally.
  • jlowin/fastmcp PR 2808: Introduced earlier dereferencing/inlining behavior that this change makes optional via the new middleware and constructor flag.
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: making $ref dereferencing optional via a FastMCP parameter.
Description check ✅ Passed The description covers the problem statement, solution approach, default behavior, and usage examples, but is missing the Contributors and Review Checklist items from the template.
Linked Issues check ✅ Passed All code changes directly address issue #3141's requirement to make $ref dereferencing optional with a per-server kwarg.
Out of Scope Changes check ✅ Passed All changes are in-scope: new middleware for optional dereferencing, updated compress_schema signature, FastMCP constructor parameter, and documentation.

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

✨ 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 optional-dereference-refs

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

@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: 861a76ea3e

ℹ️ 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".

- `server`: The server instance this lifespan is managing

**Returns:**
- An empty dictionary as the lifespan result.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revert edits under auto-generated python-sdk docs

This commit directly edits files under docs/python-sdk/**, but the repository guideline in /workspace/fastmcp/AGENTS.md says "Never modify docs/python-sdk/** (auto-generated)"; these edits are likely to be overwritten by the doc generator and create persistent drift/churn in future regenerations, so the change should be moved to the generator/source inputs instead of the generated output.

Useful? React with 👍 / 👎.

Comment on lines +385 to +386
if dereference:
schema = dereference_refs(schema)
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 Preserve root $ref normalization when dereference=False

Guarding dereference_refs() behind if dereference: removes the resolve_root_ref() fallback for schemas with a root-level $ref, so callers that now pass through compress_schema(..., dereference=False) can emit output schemas without a root type: object; this is a regression for recursive/self-referential return models when users set FastMCP(dereference_refs=False), because list_tools can return MCP-incompatible outputSchema payloads in that mode.

Useful? React with 👍 / 👎.

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

marvin-context-protocol Bot commented Feb 11, 2026

Test Failure Analysis

Summary: The test test_combined_operations expects $defs to be preserved when dereference=False (the new default), but compress_schema() is removing unused definitions via _single_pass_optimize(prune_defs=True).

Root Cause:

After _prune_param("remove") removes the remove parameter, both $defs.remove_def and $defs.unused_def become unreferenced. The call to _single_pass_optimize(prune_defs=True) at line 397-402 detects they're unused and removes them, leaving an empty $defs section which then gets removed entirely (lines 358-359).

This contradicts the test expectation (line 273-274) that $defs should be preserved when dereference=False.

Suggested Solution:

Change line 401 in src/fastmcp/utilities/json_schema.py from prune_defs=True to prune_defs=dereference:

# Apply combined optimizations in a single tree traversal.
# Only prune unused $defs when dereferencing (since dereference removes all $defs anyway).
# When dereference=False, preserve $defs structure for clients that support $ref.
schema = _single_pass_optimize(
    schema,
    prune_titles=prune_titles,
    prune_additional_properties=prune_additional_properties,
    prune_defs=dereference,  # Changed from True
)

Why this fix is correct:

  • When dereference=True: All $refs are inlined and $defs removed anyway, so pruning is redundant but harmless
  • When dereference=False: Preserve $ref/$defs structure for clients that support it, even if some defs become unused after parameter pruning
Detailed Analysis

Test flow (tests/utilities/test_json_schema.py:252-277):

Initial schema:

{
    "type": "object",
    "properties": {
        "keep": {"type": "string"},
        "remove": {"$ref": "#/$defs/remove_def"},
    },
    "required": ["keep", "remove"],
    "additionalProperties": False,
    "$defs": {
        "remove_def": {"type": "string"},
        "unused_def": {"type": "number"},
    },
}

After compress_schema(schema, prune_params=["remove"], prune_additional_properties=True):

  1. Line 385-386: Skip dereferencing (dereference=False)
  2. Line 389: resolve_root_ref() - no effect (no root $ref)
  3. Line 392-393: _prune_param("remove") - removes properties.remove and its $ref
  4. Line 397-402: _single_pass_optimize(prune_defs=True):
    • Finds no $ref references to any $defs
    • Removes both remove_def and unused_def as unused
    • Removes empty $defs section

Actual result (missing $defs):

{'properties': {'keep': {'type': 'string'}}, 'required': ['keep'], 'type': 'object'}

Expected result (should preserve $defs):

{
    'type': 'object',
    'properties': {'keep': {'type': 'string'}},
    'required': ['keep'],
    '$defs': {
        'remove_def': {'type': 'string'},
        'unused_def': {'type': 'number'},
    }
}

Test assertion:

# $defs are preserved (dereferencing is handled by middleware, not compress_schema)
assert "$defs" in result  # ← FAILS
Related Files
  • src/fastmcp/utilities/json_schema.py:397-402: Change prune_defs=True to prune_defs=dereference
  • tests/utilities/test_json_schema.py:252-277: Failing test test_combined_operations

Updated analysis: Changed suggestion from prune_defs=False to prune_defs=dereference for better consistency.

@jlowin jlowin merged commit fe57c3d into main Feb 11, 2026
13 checks passed
@jlowin jlowin deleted the optional-dereference-refs branch February 11, 2026 18:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make $ref dereferencing in schemas optional (fastmcp 3.0)

1 participant