Skip to content

Document mounted server state store isolation in upgrade guide#3236

Merged
jlowin merged 2 commits intomainfrom
docs/mounted-server-state-isolation
Feb 19, 2026
Merged

Document mounted server state store isolation in upgrade guide#3236
jlowin merged 2 commits intomainfrom
docs/mounted-server-state-isolation

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Feb 19, 2026

Mounted servers in v3 each have their own state store, which means serializable state set by parent middleware isn't visible to mounted tools by default. This was an implicit behavior change from v2 where everything ran in one server context.

Adds a note to the upgrade guide (both the LLM prompt and the human-readable section) explaining the behavior and the workaround:

from key_value.aio.stores.memory import MemoryStore

store = MemoryStore()
parent = FastMCP("Parent", session_state_store=store)
child = FastMCP("Child", session_state_store=store)
parent.mount(child, namespace="child")

Non-serializable state (serializable=False) is request-scoped and automatically shared across mount boundaries, so no workaround is needed there.

Closes #3230

@marvin-context-protocol marvin-context-protocol Bot added the documentation Updates to docs, examples, or guides. Primary change is documentation-related. label Feb 19, 2026
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: e4c162cf69

ℹ️ 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/getting-started/upgrading/from-fastmcp-2.mdx
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The integration test test_github_api_schema_performance timed out (>10s) — not during OpenAPI parsing (which completed in 3.46s), but during the subsequent await mcp_server.list_tools() call, which triggers the DereferenceRefsMiddleware introduced in commit 35bbf48.

Root Cause: The DereferenceRefsMiddleware calls dereference_refs(tool.output_schema) for every tool on each list_tools() call. For the GitHub API schema (500+ tools), each tool's output_schema contains $defs and $ref entries inherited from the OpenAPI component schemas. The jsonref.replace_refs function gets stuck in deeply recursive _walk_refs traversal across these complex schemas — repeated 500+ times — causing the total list_tools() call to blow past the 10s test timeout.

This failure is not caused by the PR changes (docs-only). It's a pre-existing regression introduced when the dereference middleware was added.

Suggested Solution: The dereferencing middleware is re-running expensive schema resolution on every list_tools() call with no caching. A few options:

  1. Cache at the middleware level — memoize _dereference_tool by tool identity so repeated list_tools() calls don't redo work.
  2. Pre-compute at tool creation — apply dereference_refs eagerly in the OpenAPI parser or tool builder, so the tool carries an already-resolved schema.
  3. Fix the test — the test's 10s timeout was sized for parsing only. The list_tools() verification (line 65) should either move to a separate test without the tight timeout, or the timeout needs to account for dereferencing 500+ tools. Though this is the least desirable option since it would just paper over a real performance issue.

Option 1 or 2 is the right fix — list_tools() for a 500-tool server shouldn't require re-dereferencing every schema on every call.

Detailed Analysis

Failure trace:

tests/server/providers/openapi/test_openapi_performance.py:65: 
    tools = await mcp_server.list_tools()

src/fastmcp/server/middleware/dereference.py:30: in on_list_tools
    return [_dereference_tool(tool) for tool in tools]

src/fastmcp/server/middleware/dereference.py:52: in _dereference_tool
    updates["output_schema"] = dereference_refs(tool.output_schema)

src/fastmcp/utilities/json_schema.py:42: in dereference_refs
    dereferenced = replace_refs(schema, proxies=False, lazy_load=False)

# _walk_refs called 6+ levels deep, processing enormous _processed cache
E   Failed: Timeout (>10.0s) from pytest-timeout.

Timing from stdout:

OpenAPI parsing took 3.46s

The parsing itself was within limits — the timeout happens entirely inside list_tools().

Why output_schema is expensive to dereference: Each OpenAPI tool's output_schema is built by extract_output_schema_from_responses in src/fastmcp/utilities/openapi/schemas.py, which preserves full $defs sections from the OpenAPI component schemas. GitHub's schema has hundreds of shared component definitions. When replace_refs traverses these, it walks a huge graph of nested definitions for every single tool.

Files involved:

  • src/fastmcp/server/middleware/dereference.py — where the slow _dereference_tool is called per-tool with no caching
  • src/fastmcp/utilities/json_schema.pydereference_refs using jsonref.replace_refs
  • tests/server/providers/openapi/test_openapi_performance.py:65 — the list_tools() call that triggers the cascade
Related Files
  • src/fastmcp/server/middleware/dereference.pyDereferenceRefsMiddleware.on_list_tools and _dereference_tool; the source of the per-tool dereferencing on every list_tools() call
  • src/fastmcp/utilities/json_schema.pydereference_refs using jsonref.replace_refs; the slow path
  • src/fastmcp/utilities/openapi/schemas.pyextract_output_schema_from_responses; produces output schemas with large $defs sections
  • tests/server/providers/openapi/test_openapi_performance.py — the failing test; the list_tools() call at line 65 is inside the 10s timeout window but wasn't anticipated to be expensive when the timeout was set

🤖 Generated with Claude Code

@jlowin jlowin reopened this Feb 19, 2026
@jlowin jlowin merged commit 390a11d into main Feb 19, 2026
14 of 15 checks passed
@jlowin jlowin deleted the docs/mounted-server-state-isolation branch February 19, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Updates to docs, examples, or guides. Primary change is documentation-related.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mounted mcsp servers failing to receive middleware in v3.0

1 participant