Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

SEP-2243 Mcp-Param-* validation runs a full `tools/list` per `tools/call`, with no opt-out and no by-name fast path

Aperta
#3,565 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
45/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
python
Ambito
api, backend

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

spec-2026-07-28 v2

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 through serve_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 every tools/call with arguments or Mcp-Param-* headers.
    • handle_modern_request (line 438): calls _mcp_param_rejection before 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/paginated tools/list handlers 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-28 tools/call writes no bytes to the wire while the internal tools/list schema walk runs (up to 100 paginated serve_one round trips). A deployment whose tools/list handler is slower than the upstream proxy's idle-read timeout previously worked (the keepalive committed within 15s of dispatch) but would now have every validated tools/call reset 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 backend tools/list calls + 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

  1. By-name resolution hook. Let a lowlevel Server optionally accept a by-name schema resolver (e.g. an on_get_tool_schema(ctx, name) -> dict | None alongside on_list_tools) that _tool_input_schema prefers over the tools/list walk when present. MCPServer and downstream frameworks built on the lowlevel Server (e.g. fastmcp) can implement it against their own by-name lookup instead of paying for a full listing. Falls back to the existing tools/list walk when the hook is absent, so behavior for a bare Server with only a tools/list handler is unchanged.
  2. Skip when no tool declares x-mcp-header. A server could compute once (e.g. at the first tools/list, or lazily and cache) whether any registered tool actually declares an x-mcp-header-annotated property. If none do, no Mcp-Param-* header can ever be meaningfully violated for any tool, and the whole check — including the schema resolution — can be skipped server-wide. Note: MCPServer currently has no declaration/validation mechanism for x-mcp-header server-side (see #3484), so this flag would need to inspect raw inputSchema properties for the annotation key directly, not rely on a first-class registration API.
  3. 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/list handlers 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 — MCPServer has no x-mcp-header declaration or validation mechanism server-side; relevant to proposal (2) above, since there is currently no first-class way to ask "does any tool declare x-mcp-header" other than inspecting raw schemas.
  • #3513 — client-side analog: ClientSession.call_tool issues a tools/list after every tools/call when 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-header through 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

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di modelcontextprotocol/python-sdk

Tutte le issue di modelcontextprotocol/python-sdk

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.