SEP-2243 Mcp-Param-* validation runs a full `tools/list` per `tools/call`, with no opt-out and no by-name fast path
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 45/100
Direzione di ricerca
The issue is in src/mcp/server/_streamable_http_modern.py, specifically the _tool_input_schema and _mcp_param_rejection functions. Start by reading the code around lines 264-362 to understand the validation flow. Run the minimal reproduction sketch to observe the behavior. Check related issues #3484 and #3513 for context. The fix involves adding a by-name resolution hook or an opt-out mechanism, requiring changes to the low-level Server class and the HTTP handling logic.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Summary
_mcp_param_rejection / _tool_input_schema in src/mcp/server/_streamable_http_modern.py (added by #3033, merged 2026-06-30) resolve a called tool's inputSchema by running the server's registered tools/list handler — through the full serve_one dispatch path (middleware, lifespans, up to _MCP_PARAM_LIST_PAGE_CAP = 100 pages) — before every tools/call that has non-empty arguments or any Mcp-Param-* header, on the 2026-07-28 protocol revision. This runs even when the tool being called advertises no x-mcp-header parameters at all, and there is no configuration to skip it. It is skipped only when app.get_request_handler("tools/list") is None or the call has neither arguments nor Mcp-Param-* headers (_mcp_param_rejection, lines 345–358).
For a server whose tools/list handler is itself expensive — an aggregator that fans a listing out to several backends, each requiring a per-user credential to enumerate — this turns every tool call into a full catalog refresh.
Where
src/mcp/server/_streamable_http_modern.py:_tool_input_schema(lines 264–329): pages throughserve_one(app, dctx, "tools/list", ...)to find one tool's schema._mcp_param_rejection(lines 332–362): the gate that calls it, pre-dispatch, for everytools/callwith arguments orMcp-Param-*headers.handle_modern_request(line 438): calls_mcp_param_rejectionbefore constructing the SSE deferral machinery.
PR #3033 acknowledges the cost
From the PR description:
The cost is an internal listing per validated
tools/call: middleware, lifespans, and expensive/paginatedtools/listhandlers see extra invocations … optimizable later behind the same surface (e.g. a registry fast-path for the built-in handler).
A review comment on the PR (on src/mcp/server/_streamable_http_modern.py:399) separately flags that this validation runs pre-dispatch, before the SSE deferral/keepalive window exists:
In SSE mode (
json_response=False), the new pre-dispatch Mcp-Param validation phase runs before the SSE deferral/keepalive machinery, so a 2026-07-28tools/callwrites no bytes to the wire while the internaltools/listschema walk runs (up to 100 paginatedserve_oneround trips). A deployment whosetools/listhandler is slower than the upstream proxy's idle-read timeout previously worked (the keepalive committed within 15s of dispatch) but would now have every validatedtools/callreset before dispatch — consider bounding the schema-resolving walk with a timeout that degrades to the existing logged fail-open skip.
That comment does not appear to have been acted on in 2.2.0 — _tool_input_schema has no timeout around the serve_one loop, only the page-count cap.
Aggregator impact (observed in production)
We run an MCP aggregating gateway (fastmcp 4.0.4 FastMCP with one ProxyProvider per backend MCP server, mounted via add_provider, ~7 backends, per-user credentials minted for backend requests). FastMCP's tools/list handler (AggregateProvider._list_tools) queries every provider's list_tools() in parallel; for a ProxyProvider this is a live upstream tools/list call, credential-minting included (see companion issue filed against PrefectHQ/fastmcp, which also has a caching gap on this path).
Observed:
- One
tools/call→ 7 backendtools/listcalls + 4 Kerberos ticket redemptions (credential minting triggered by listing certain backends). - A client batch of 10 parallel
tools/calls → roughly 70 backend listings.
The call itself only ever needs one tool's schema. The server can resolve that by name — FastMCP.get_tool(name) reaches this without a full fan-out: AggregateProvider._get_tool still queries every child provider, but each namespaced child is wrapped in a Namespace transform whose get_tool short-circuits to None without calling the underlying provider when the name's prefix doesn't match (fastmcp/server/transforms/namespace.py), and the one matching ProxyProvider._get_tool reads from its own _tools_cache when fresh (cache_ttl, default 300s) instead of hitting the network. None of that machinery is reachable from _tool_input_schema, which only knows how to call the registered tools/list handler.
Proposed fixes
- By-name resolution hook. Let a lowlevel
Serveroptionally accept a by-name schema resolver (e.g. anon_get_tool_schema(ctx, name) -> dict | Nonealongsideon_list_tools) that_tool_input_schemaprefers over thetools/listwalk when present.MCPServerand downstream frameworks built on the lowlevelServer(e.g. fastmcp) can implement it against their own by-name lookup instead of paying for a full listing. Falls back to the existingtools/listwalk when the hook is absent, so behavior for a bareServerwith only atools/listhandler is unchanged. - Skip when no tool declares
x-mcp-header. A server could compute once (e.g. at the firsttools/list, or lazily and cache) whether any registered tool actually declares anx-mcp-header-annotated property. If none do, noMcp-Param-*header can ever be meaningfully violated for any tool, and the whole check — including the schema resolution — can be skipped server-wide. Note:MCPServercurrently has no declaration/validation mechanism forx-mcp-headerserver-side (see #3484), so this flag would need to inspect rawinputSchemaproperties for the annotation key directly, not rely on a first-class registration API. - At minimum, a documented, explicit opt-out (constructor flag or env var) for deployments that accept the compliance gap in exchange for not paying full-listing cost per call, given some servers'
tools/listhandlers are cheap and others' are not.
Minimal reproduction sketch
from mcp import types
from mcp.server.lowlevel.server import Server
list_calls = 0
async def on_list_tools(ctx, params):
global list_calls
list_calls += 1
return types.ListToolsResult(tools=[...]) # one tool with an inputSchema
async def on_call_tool(ctx, params):
return types.CallToolResult(content=[...])
server = Server("repro", on_list_tools=on_list_tools, on_call_tool=on_call_tool)
# Serve over Streamable HTTP and send one `tools/call` with non-empty
# `arguments` from a client on protocol 2026-07-28 (no tools/list sent by the
# client). Observe list_calls == 1: the server listed its whole catalog to
# validate a single call.
Related
- #3484 —
MCPServerhas nox-mcp-headerdeclaration or validation mechanism server-side; relevant to proposal (2) above, since there is currently no first-class way to ask "does any tool declarex-mcp-header" other than inspecting raw schemas. - #3513 — client-side analog:
ClientSession.call_toolissues atools/listafter everytools/callwhen the output-schema cache is empty, with the same aggregator fan-out cost and no opt-out. Filed independently but the same underlying pattern (validation/caching bolted onto the request path via a full listing instead of a by-name lookup). - PR #3033 (merged) — the change this issue is about.
- PR #4622 (PrefectHQ/fastmcp, merged) — confirms fastmcp's HTTP transport passes
Mcp-Param-*/x-mcp-headerthrough untouched; does not address the per-call listing cost.
🤖 Generated with Claude Code
- Lingua principale
- Python
- Stelle
- 24.3k
- Fork
- 4k
- Merge medio
- 1g 11h
- PR unite (30g)
- 30
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di modelcontextprotocol/python-sdk
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
modelcontextprotocol/python-sdk#3566 ·
-
v1 v2
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
modelcontextprotocol/python-sdk#3546 · 5 commenti ·
-
v1 v2
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
modelcontextprotocol/python-sdk#3545 · 1 commento ·
-
v1 v2
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 91/100
modelcontextprotocol/python-sdk#3508 · 2 commenti ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 64/100
modelcontextprotocol/python-sdk#3504 ·
Tutte le issue di modelcontextprotocol/python-sdk
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
anthropics/skills#1811 · 1 commento ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
speaches-ai/speaches#678 ·
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
datalayer/mcp-compose#42 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
conda-forge/spacy-feedstock#177 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
UKGovernmentBEIS/inspect_evals#2523 ·