Skip to content

Look the tool schema up by name for Mcp-Param-* validation instead of running tools/list - #3630

Merged
maxisbey merged 2 commits into
mainfrom
3565-param-validation-schema-lookup
Oct 2, 2026
Merged

maxisbey merged 2 commits into
mainfrom
3565-param-validation-schema-lookup

Conversation

@maxisbey

@maxisbey maxisbey commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #3565.

What was wrong

On the 2026-07-28 Streamable HTTP path, a tools/call that carries arguments is checked against the called tool's x-mcp-header declarations before dispatch. To find that tool's input schema, the server ran its own tools/list handler, page by page, on every such call.

  • A server whose listing is expensive (an aggregating gateway, a large catalog) paid for a full listing per call.
  • A slow listing delayed the call, and a failing one logged an error for it.
  • A call to an unknown tool walked every page.

What changes

  • MCPServer looks the called tool up by name among its registered tools. Nothing to configure.

  • The low-level Server takes a new optional keyword, get_tool_input_schema=:

    schemas: dict[str, dict[str, Any]] = {}  # kept up to date as the gateway lists its backends
    
    server = Server(
        "gateway",
        on_list_tools=list_tools,  # fans out to every backend
        on_call_tool=call_tool,
        get_tool_input_schema=schemas.get,
    )
    • It takes the tool name and returns that tool's input schema, or None when there is nothing to validate.
    • When it is set, it is called instead of the tools/list handler.
    • If it raises, the error is logged and the call is served unvalidated, the same as a failed listing.
    • A server that declares no x-mcp-header on any tool can pass lambda name: None.
    • It is called before middleware runs, so it is not scoped to the caller.
    • The value is also readable as server.get_tool_input_schema.

What users will notice

  • MCPServer:
    • Middleware no longer sees a tools/list for each tools/call.
    • The schema that is validated is the registered one. Middleware that filters or rewrites tools/list results no longer affects validation.
      • A registered tool that middleware hides from a caller used to be skipped. It is now validated like any other registered tool.
      • The check runs before middleware. A caller who names a hidden tool and its marked argument, without the header, is answered with the header mismatch, which names the header and the argument, rather than with whatever middleware would have answered. The tool is not run.
      • The TypeScript, Go, C#, Rust and PHP SDKs also resolve the tool by name before dispatch.
    • A subclass that overrides MCPServer.list_tools() to add tools outside the registry no longer gets those tools validated.
  • Low-level Server with get_tool_input_schema=:
    • The tools/list handler does not run for a tools/call.
    • Validation runs even when no tools/list handler is registered.
    • None from the function skips validation for that call. It does not fall back to the listing.

What is unchanged

  • A low-level Server without the keyword resolves the schema through tools/list exactly as before.
  • How a header is compared with the body, the 400 status and the -32020 error code. Apart from the cases above, the same requests are accepted and rejected.
  • A call with no arguments and no Mcp-Param-* headers is still not validated.
  • Earlier protocol versions, stdio and in-memory transports. The check only exists on the 2026-07-28 HTTP entry.

Docs

  • docs/advanced/header-parameters.md gains a short section, "Schemas by name", under "On the low-level Server", with a runnable example (docs_src/header_parameters/tutorial003.py) and a recap line.
  • docs/advanced/low-level-server.md gains one bullet under "The other handlers" that points to that section.
  • The "Servers validate Mcp-Param-* headers against the request body" section is removed from docs/migration.md, along with the link to it in docs/whats-new.md. The check is new in 2026-07-28, so there is nothing to migrate from v1, and the section described the internal listing this change replaces.

How it was checked

  • New tests in tests/server/test_streamable_http_modern.py:
    • An MCPServer tool with an x-mcp-header argument is accepted with a matching header, rejected with a mismatched or missing one, and an unregistered tool is left to dispatch. Middleware sees only the two dispatched tools/calls. On main it also sees a tools/list for each request.
    • A low-level server with get_tool_input_schema gets the same outcomes and its tools/list handler never runs.
    • A raising get_tool_input_schema is logged and the call is served.
  • New tests in tests/docs_src/test_header_parameters.py, over the 2026-07-28 HTTP path: the page's low-level example without the keyword dispatches a tools/list before each tools/call, the one with it dispatches only the tools/call, and it still rejects a mismatched Mcp-Param-Region header.
  • The existing tests for the listing path (pagination, cursor cycle, page cap, raising handler, caller envelope) pass unchanged. They cover a low-level server without the keyword.
  • For every tool on the everything-server, and on a server with nested models, aliases, optional and annotated arguments, the registered schema equals the inputSchema that tools/list returns.
  • The http-custom-header-server-validation conformance scenario passes against the everything-server.
  • ./scripts/test (100% coverage), ruff and pyright are clean.

AI Disclaimer

On the 2026-07-28 Streamable HTTP path, every tools/call with arguments
ran the server's whole tools/list handler, page by page, to find the
called tool's input schema for Mcp-Param-* header validation. A server
with an expensive listing paid for it on every call.

MCPServer now looks the called tool up by name among its registered
tools. Middleware no longer sees a tools/list for each tools/call, and
the schema validated is the registered one, whatever middleware does to
the listing.

The low-level Server takes a new optional get_tool_input_schema
keyword: a function from tool name to input schema, or None when there
is nothing to validate. When it is set it is called instead of the
tools/list handler; if it raises, the error is logged and the call is
served unvalidated, as for a failed listing. A server that does not set
it keeps resolving the schema through tools/list.

The check itself and the 400 / -32020 rejection are unchanged.

Fixes #3565
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3630.mcp-python-docs.pages.dev
Deployment https://a998038b.mcp-python-docs.pages.dev
Commit cf29493
Triggered by @maxisbey
Updated 2026-10-02 14:12:58 UTC

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/whats-new.md">

<violation number="1" location="docs/whats-new.md:202">
P3: This removes the What's New page's path to the detailed header-parameter rules after moving them out of the Migration Guide. Link this summary to `advanced/header-parameters.md` instead of dropping the documentation link.</violation>
</file>

<file name="docs/advanced/header-parameters.md">

<violation number="1" location="docs/advanced/header-parameters.md:57">
P2: The new callback path makes the low-level `Server` validate `Mcp-Param-*` header/body agreement, so the recap's blanket “checks nothing” claim now contradicts this section. Qualify that the low-level server does not validate annotation schemas itself, while noting that `get_tool_input_schema` enables request validation.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

* The server rejects a call whose header and body disagree.
* Only `str`, `int` and `bool` arguments can be marked. `MCPServer` raises `InvalidSignature` for anything else.
* The low-level `Server` checks nothing, and clients drop a tool whose annotation is invalid.
* `get_tool_input_schema` keeps the low-level `Server` from running `on_list_tools` on every call.

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: The new callback path makes the low-level Server validate Mcp-Param-* header/body agreement, so the recap's blanket “checks nothing” claim now contradicts this section. Qualify that the low-level server does not validate annotation schemas itself, while noting that get_tool_input_schema enables request validation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/advanced/header-parameters.md, line 57:

<comment>The new callback path makes the low-level `Server` validate `Mcp-Param-*` header/body agreement, so the recap's blanket “checks nothing” claim now contradicts this section. Qualify that the low-level server does not validate annotation schemas itself, while noting that `get_tool_input_schema` enables request validation.</comment>

<file context>
@@ -37,11 +37,23 @@ There you write `input_schema` by hand, so the key goes straight in:
 * The server rejects a call whose header and body disagree.
 * Only `str`, `int` and `bool` arguments can be marked. `MCPServer` raises `InvalidSignature` for anything else.
 * The low-level `Server` checks nothing, and clients drop a tool whose annotation is invalid.
+* `get_tool_input_schema` keeps the low-level `Server` from running `on_list_tools` on every call.
 
 The rest of the hand-written `Server` API is **[The low-level Server](low-level-server.md)**.
</file context>

Comment thread docs/whats-new.md

* **Identity is optional, per-message metadata.** The request-side `clientInfo` `_meta` key is optional (the required pair is `protocolVersion` + `clientCapabilities`), and `serverInfo` moved out of the `server/discover` result body: servers stamp it into every 2026-era result's `_meta` instead ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). The SDK always stamps; `client.server_info` is `None` when a server does not identify itself (for example, a middleware stripped the key). **[The low-level Server](advanced/low-level-server.md)** shows the stamp on the wire.
* **Requests are routable without parsing bodies.** Modern HTTP requests carry `Mcp-Method` (and, for the three tool-ish calls, `Mcp-Name`); a tool input-schema property annotated with `x-mcp-header` is mirrored into an `Mcp-Param-*` header and cross-checked by the server ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways and rate limiters can route on headers alone; the **[Migration Guide](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** has the rules.
* **Requests are routable without parsing bodies.** Modern HTTP requests carry `Mcp-Method` (and, for the three tool-ish calls, `Mcp-Name`); a tool input-schema property annotated with `x-mcp-header` is mirrored into an `Mcp-Param-*` header and cross-checked by the server ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways and rate limiters can route on headers alone.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This removes the What's New page's path to the detailed header-parameter rules after moving them out of the Migration Guide. Link this summary to advanced/header-parameters.md instead of dropping the documentation link.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/whats-new.md, line 202:

<comment>This removes the What's New page's path to the detailed header-parameter rules after moving them out of the Migration Guide. Link this summary to `advanced/header-parameters.md` instead of dropping the documentation link.</comment>

<file context>
@@ -199,7 +199,7 @@ At 2026-07-28 the standalone HTTP GET stream and `resources/subscribe` are repla
 
 * **Identity is optional, per-message metadata.** The request-side `clientInfo` `_meta` key is optional (the required pair is `protocolVersion` + `clientCapabilities`), and `serverInfo` moved out of the `server/discover` result body: servers stamp it into every 2026-era result's `_meta` instead ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). The SDK always stamps; `client.server_info` is `None` when a server does not identify itself (for example, a middleware stripped the key). **[The low-level Server](advanced/low-level-server.md)** shows the stamp on the wire.
-* **Requests are routable without parsing bodies.** Modern HTTP requests carry `Mcp-Method` (and, for the three tool-ish calls, `Mcp-Name`); a tool input-schema property annotated with `x-mcp-header` is mirrored into an `Mcp-Param-*` header and cross-checked by the server ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways and rate limiters can route on headers alone; the **[Migration Guide](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** has the rules.
+* **Requests are routable without parsing bodies.** Modern HTTP requests carry `Mcp-Method` (and, for the three tool-ish calls, `Mcp-Name`); a tool input-schema property annotated with `x-mcp-header` is mirrored into an `Mcp-Param-*` header and cross-checked by the server ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways and rate limiters can route on headers alone.
 * **Results carry cache hints.** List and read results declare `ttlMs` and `cacheScope` ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); you set them per method with `cache_hints=`, and `Client` honors them with a built-in response cache. A server that sends no hints (every pre-2026 server) sees identical, uncached traffic. **[Caching hints](client/caching.md)**.
 * **Extensions are first class.** Servers and clients declare optional capability bundles under reverse-DNS identifiers ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); the built-in `Apps` extension (MCP Apps) is the reference. **[Extensions](advanced/extensions.md)** and **[MCP Apps](advanced/apps.md)**.
</file context>
Suggested change
* **Requests are routable without parsing bodies.** Modern HTTP requests carry `Mcp-Method` (and, for the three tool-ish calls, `Mcp-Name`); a tool input-schema property annotated with `x-mcp-header` is mirrored into an `Mcp-Param-*` header and cross-checked by the server ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways and rate limiters can route on headers alone.
* **Requests are routable without parsing bodies.** Modern HTTP requests carry `Mcp-Method` (and, for the three tool-ish calls, `Mcp-Name`); a tool input-schema property annotated with `x-mcp-header` is mirrored into an `Mcp-Param-*` header and cross-checked by the server ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways and rate limiters can route on headers alone; the **[Header parameters](advanced/header-parameters.md)** page has the rules.

@maxisbey
maxisbey merged commit c54075c into main Oct 2, 2026
38 checks passed
@maxisbey
maxisbey deleted the 3565-param-validation-schema-lookup branch October 2, 2026 14:15

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One optional note from this repository's REVIEW.md or CLAUDE.md checks was not posted as a comment, over this review's limit for such notes; it is on this commit's check card.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment on lines +432 to +435
def _tool_input_schema(self, name: str) -> dict[str, Any] | None:
"""Called before middleware runs, so it also finds a tool that middleware hides from the caller."""
tool = self._tool_manager.get_tool(name)
return None if tool is None else tool.parameters

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Clients of an MCPServer whose middleware rewrites tools/list schemas start getting HTTP 400 -32020 on calls the base branch served. _tool_input_schema at src/mcp/server/mcpserver/server.py:435 returns the registry schema, so a middleware that strips or renames x-mcp-header in the listing no longer affects validation; a client that listed through that middleware sends no header and is rejected pre-dispatch. Fix: validate against the schema the caller was shown, or give MCPServer an opt-out such as its own get_tool_input_schema= keyword that can be set to lambda name: None, and state the 400 consequence in the docs. The PR text calls this a noticed change; the concrete effect is a rejected call, not a skipped check. [also at: src/mcp/server/mcpserver/server.py:434 - Callers of an MCPServer tool that a middleware hides from tools/list now get HTTP 400 -32020 on calls that the base branch served.]

Why this was flagged

An MCPServer registers a tool with an x-mcp-header annotated argument and installs a middleware that rewrites tools/list results. A 2026-07-28 HTTP client lists tools through that middleware, sees no annotation, and sends tools/call with the argument in the body and no Mcp-Param-* header. On the base branch _tool_input_schema in src/mcp/server/_streamable_http_modern.py ran the server's own tools/list through middleware, found no annotation and skipped validation, so the call was dispatched. After this change _mcp_param_rejection at src/mcp/server/_streamable_http_modern.py:365 calls MCPServer._tool_input_schema (src/mcp/server/mcpserver/server.py:435), which reads self._tool_manager.get_tool(name).parameters before middleware, finds the annotation, and validate_mcp_param_headers returns a 400 with error code -32020 for the missing header. No safeguard applies: the lookup is wired unconditionally at src/mcp/server/mcpserver/server.py:221 and MCPServer exposes no way to disable or replace it.

Verification: _mcp_param_rejection now takes lookup = app.get_tool_input_schema and calls lookup(name) directly (new lines 344-369), bypassing serve_one and therefore all middleware. MCPServer._tool_input_schema (src/mcp/server/mcpserver/server.py:432-435) returns self._tool_manager.get_tool(name).parameters, the raw registry schema with the x-mcp-header annotation intact.

[Server[LifespanResultT]],
AbstractAsyncContextManager[LifespanResultT],
] = lifespan,
get_tool_input_schema: Callable[[str], Mapping[str, Any] | None] | None = None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 (optional) A low-level server whose lookup returns a non-dict Mapping gets no Mcp-Param-* validation at all, silently, where the base always validated. The new parameter is typed Callable[[str], Mapping[str, Any] | None] (src/mcp/server/lowlevel/server.py:146), but the validator only walks dict nodes: _walk_schema_positions skips anything that is not a dict (src/mcp/shared/inbound.py:133), so a MappingProxyType or ChainMap schema yields zero annotated positions and every mismatched header is accepted with no log line. Fix: make the accepted type and the walker agree for every lookup result, either by annotating the parameter and attribute as dict[str, Any] | None or by having the walker accept Mapping at inbound.py:133 and :138. [also at: src/mcp/server/_streamable_http_modern.py:365 - Low-level servers whose get_tool_input_schema returns a non-dict Mapping get no header check: a mismatched Mcp-Param-* header is served instead of a 400.; src/mcp/server/_streamable_http_modern.py:372 - Low-level servers whose get_tool_input_schema returns a non-dict value get no Mcp-Param-* validation and no log line, so mismatched headers are served.]

Why this was flagged

A gateway author passes get_tool_input_schema=catalog.get where the catalog stores read-only schemas as types.MappingProxyType or a ChainMap over backend catalogs; pyright accepts it because the parameter is Mapping[str, Any] | None at src/mcp/server/lowlevel/server.py:146. On a tools/call, _mcp_param_rejection calls the lookup at src/mcp/server/_streamable_http_modern.py:365 and passes the result straight to validate_mcp_param_headers at :372. find_invalid_x_mcp_header and _annotated_positions both iterate _walk_schema_positions, which at src/mcp/shared/inbound.py:133 does if not isinstance(node, dict): continue, so a non-dict Mapping root produces no positions. The validator returns None and a request whose Mcp-Param-Region disagrees with the body is served with 200 instead of the spec's 400 -32020, and nothing is logged. On the base branch the schema always came from serve_one's JSON-dumped result (tool.get("inputSchema") at :310), which is a plain dict, so this input never reached the walker.

Verification: src/mcp/server/lowlevel/server.py:146 types the parameter Callable[[str], Mapping[str, Any] | None] | None; src/mcp/server/_streamable_http_modern.py:365-372 does input_schema = lookup(name) and passes it unchanged to validate_mcp_param_headers; src/mcp/shared/inbound.py:130-134 does if not isinstance(node, dict): continue, so a MappingProxyType root is dropped before yielding anything.

Comment thread docs/migration.md

Base64-sentinel decoding is strict everywhere it applies, including the `Mcp-Name` header: a `=?base64?...?=` value whose payload is not canonical base64 (wrong padding, stray characters, non-zero trailing bits) or not valid UTF-8 is rejected as malformed rather than leniently decoded.

## Need Help?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 (optional) Users lose the only documentation of several Mcp-Param-* behaviours, and the closed v1->v2 migration record is edited by deleting a whole section. AGENTS.md says docs/migration.md is closed to new entries and only corrections or clarity edits are fine; the diff removes the entire SEP-2243 section before docs/migration.md:2852. Fix: keep the section and only correct the sentence about the internal listing (now get_tool_input_schema or the registry for MCPServer), or move its still-true facts to docs/advanced/header-parameters.md before removing it: strict base64 decoding, duplicate recognized header rejection, and the public decode_header_value/validate_mcp_param_headers codec in mcp.shared.inbound. [also at: docs/migration.md:2853 - nit: AGENTS.md says docs/migration.md is a closed record where only corrections and clarity edits are fine. The diff deletes the whole 'Servers validate Mcp-Param-* headers against the request body (SEP-2243)' section, including still-accurate content (client-side auto-emission, duplicate-header rejection, strict base64-sentinel decoding, the public mcp.shared.inbound codec) and drops the whats-new.md link to it.]

Why this was flagged

The diff deletes the section "Servers validate Mcp-Param-* headers against the request body (SEP-2243)" from docs/migration.md (base lines 2852-2858) and the link to it from docs/whats-new.md:202. AGENTS.md states docs/migration.md "is closed to new entries. Correcting errors or improving clarity in what's there is fine."; removing a section is neither. A grep of docs/ shows no other page mentions decode_header_value, validate_mcp_param_headers, the rejection of a recognized header supplied more than once, or the strict base64-sentinel decoding of Mcp-Name; docs/advanced/header-parameters.md does not cover them either. After merge a reader of the published docs has no page describing those behaviours, whereas on the base branch the migration guide and the whats-new link lead to them. Only one sentence of the deleted section (the internal listing) became stale with this change.

Verification: /home/claude/python-sdk/AGENTS.md:19-20 reads "docs/migration.md is the v1 → v2 record and is closed to new entries. Correcting errors or improving clarity in what's there is fine." The diff (hunk @@ -2849,14 +2849,6 @@) deletes the entire SEP-2243 section rather than correcting the one now-stale sentence. The other statements were still true and are not re-homed anywhere.

return region

body = _tool_call_body({"region": "eu"})
async with _asgi_client(mcp._lowlevel_server) as http:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 nit (optional): the new MCPServer validation test reaches through the private mcp._lowlevel_server attribute at tests/server/test_streamable_http_modern.py:1030 instead of the public streamable_http_app(), which the test-quality skill AGENTS.md binds reviewers to disallows. The test is the one proving MCPServer no longer lists on each call, so it should read like user code. Fix: drive the MCPServer through its public ASGI app (or a public accessor) so the test does not depend on a private attribute, and apply the same to the sibling MCPServer tests added in this diff.

Why this was flagged

AGENTS.md:68 says tests must conform to .claude/skills/test-quality/SKILL.md, whose line 45 bars private imports unless they are the documented way. The new test at tests/server/test_streamable_http_modern.py:1030 passes mcp._lowlevel_server to _asgi_client. The dismissal relies on pyright's…

Verification: nit. SKILL.md:44-45 "Tests read like real user code: ... no private imports unless that is genuinely the documented way to do the thing." The new test at tests/server/test_streamable_http_modern.py:1030 does async with _asgi_client(mcp._lowlevel_server) as http: where mcp is an MCPServer; _lowlevel_server is a private attribute (src/mcp/server/mcpserver/server.py:212). Nothing fails at runtime.

icons=icons,
version=version,
cache_hints=cache_hints,
get_tool_input_schema=self._tool_input_schema,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 nit (optional): AGENTS.md says any change to an existing v2 API's signature or observable behaviour is an explicit maintainer design decision to generally avoid. Wiring get_tool_input_schema=self._tool_input_schema changes what MCPServer observably does on every validated tools/call: middleware no longer sees the internal tools/list, a tool middleware hides is now validated, and a list_tools() override's extra tools are no longer validated; Server.__init__ also gains a keyword (lowlevel/server.py:146). Fix: have a maintainer sign off on the behaviour change on the linked issue, or keep the old listing path as the default and make the registry lookup opt-in.

Why this was flagged

Nothing fails at runtime. The instruction guards the 2.x compatibility contract: a user whose middleware filters tools/list to hide a tool from a caller previously saw that caller's tools/call skip Mcp-Param validation; after this change the same call is validated against the registered schema and can now be rejected with 400/-32020. A subclass overriding MCPServer.list_tools() to add tools outside the registry loses validation for those tools. The PR description lists these under 'What users will notice', so the author knows; the instruction asks that a maintainer make this decision explicitly. Cost of the old behaviour (a full listing per call) is the motivation cited in #3565.

Verification: AGENTS.md (base cafa33b) "Branching Model": "v2 is released; its public API is a compatibility contract for the 2.x line. Removals, renames, or any change to an existing API's signature or observable behaviour ... is a design decision a maintainer makes explicitly, and should generally be avoided."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mcp-Param-* validation runs the full tools/list handler on every tools/call

1 participant