Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem 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

Đang mở
#3,565 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
45/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
python
Lĩnh vực
api, backend

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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

Ngôn ngữ chính
Python
Star
24.3k
Fork
4k
Merge trung bình
1 ngày 11 giờ
Pull request đã merge (30 ngày)
30

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của modelcontextprotocol/python-sdk

Tất cả issue của modelcontextprotocol/python-sdk

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.