Skip to content

Commit ce53141

Browse files
feat(client,server): SEP-2243 Mcp-Param-* mirroring/validation on the response-cache substrate
this._toolDefinition(name) (a derived view over the response-cache's tools/list entry); on a HEADER_MISMATCH it evicts tools/list, refetches (auto-paginating listTools writes ONE entry), and retries once. The _cachedToolDefinitions map, cacheToolMetadata isFirstPage flag, and _refreshAllToolDefinitions walk are gone — the cache's lifecycle (list_changed evicts; reset clears) is the lifecycle. Adversarial-review fixes folded in: - scanXMcpHeaderDeclarations enforces the static-reachability MUST: descends items/additionalProperties/oneOf/anyOf/allOf/not/if/then/else/$defs and rejects an x-mcp-header anywhere outside the properties-only chain (incl. the schema root). Fixtures for each. - 400+JSON-RPC body → in-band ProtocolError is gated to modern-enveloped requests only (legacy 400 stays SdkHttpError). Covered by test + migration.md paragraph + lifted into its own changeset paragraph. - McpServer._toolInputSchemaJson: lazy re-derive memoizes; the rename branch reassigns the closure name so a later paramsSchema update evicts the live slot. - migration.md documents Mcp-Name emission and the 400 error-type change. PERMITTED_X_MCP_HEADER_TYPES retains 'number' (the spec says integer/string/ boolean only, but the pinned conformance referee's http-custom-headers scenario ships type:"number" parameters; tracked upstream in the constant's docstring).
1 parent 4280d2d commit ce53141

29 files changed

Lines changed: 1848 additions & 53 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@modelcontextprotocol/core': minor
3+
'@modelcontextprotocol/client': minor
4+
---
5+
6+
SEP-2243 `Mcp-Param-*` client-side mirroring (protocol revision 2026-07-28). On a 2026-07-28 connection over Streamable HTTP, `Client.callTool()` now mirrors tool arguments designated with `x-mcp-header` in the tool's `inputSchema` into `Mcp-Param-{Name}` HTTP headers (with the spec's `=?base64?…?=` sentinel encoding for values that are not safe plain-ASCII field values), and `Client.listTools()` excludes tool definitions whose `x-mcp-header` declarations violate the spec's constraints (logging a warning naming the tool and the reason) — the exclusion runs on both the no-argument aggregate and each explicit-cursor page. The legacy-era `callTool` and `listTools` paths are unchanged. Browser environments skip mirroring (dynamically named headers cannot be statically allow-listed for credentialed CORS); a conforming SEP-2243 server will reject a `tools/call` whose body carries a non-null value for an `x-mcp-header` parameter when the matching header is absent, so calling such a tool with that argument from a browser is a known limitation. New `CallToolRequestOptions.toolDefinition` lets callers supply the tool definition directly so mirroring and output-schema validation can run without a prior `tools/list`. `TransportSendOptions.headers` is added (additive, optional) for per-request HTTP headers; the Streamable HTTP transport skips reserved standard/auth header names (`authorization`, `mcp-protocol-version`, `mcp-method`, `mcp-name`, `mcp-session-id`, `content-type`); transports that share a single channel (stdio, in-memory) ignore it.
7+
8+
The Streamable HTTP transport now emits the `Mcp-Name` standard header on every modern-enveloped request (`params.name` for `tools/call`/`prompts/get`, `params.uri` for `resources/read`), sentinel-encoded.
9+
10+
**Behavior change (modern era only):** on a modern-enveloped request the Streamable HTTP transport now surfaces an HTTP `400` whose body is a well-formed JSON-RPC error response addressed to the pending request id in-band as a `ProtocolError` (instead of `SdkHttpError`), so the `HEADER_MISMATCH` recovery retry can fire. Legacy-era exchanges are unchanged.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/server': minor
3+
---
4+
5+
SEP-2243 `Mcp-Param-*` server-side validation (protocol revision 2026-07-28). On the modern (2026-07-28) serving path, `createMcpHandler` now validates `Mcp-Param-{Name}` headers against the named tool's `x-mcp-header` declarations and the body `arguments` before dispatch: a missing header for a present body value, a header that decodes to a different value than the body, or an invalid `=?base64?…?=` sentinel is rejected with `400 Bad Request` and JSON-RPC `-32001` (`HeaderMismatch`) — the same shape the existing standard-header cross-checks emit. A `null`/absent body value passes regardless of any header (the spec's "server MUST NOT expect the header" rows). `McpServer.registerTool` now warns at registration time when an `x-mcp-header` declaration violates the spec's constraints. The 2025-era serving paths and the low-level `Server` factory shape are unchanged.

docs/client.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,15 @@ const result = await client.callTool(
307307
console.log(result.content);
308308
```
309309

310+
### `x-mcp-header` parameter mirroring (2026-07-28 draft)
311+
312+
On a 2026-07-28 connection over Streamable HTTP, `callTool()` mirrors any argument whose `inputSchema` property carries an `x-mcp-header` annotation into an `Mcp-Param-{Name}` HTTP request header so intermediaries can route on it without parsing the body. The mirrored headers
313+
are built from the most recent `listTools()` result; if you already hold the tool definition (e.g. from configuration), pass it via `CallToolRequestOptions.toolDefinition` so mirroring runs without a prior list. On a cache miss the call is sent without `Mcp-Param-*` headers and,
314+
when a conforming server rejects it with `-32001` (`HeaderMismatch`), `callTool()` refreshes the definition cache once and retries.
315+
316+
On a modern HTTP connection `listTools()` **excludes** tool definitions whose `x-mcp-header` declarations violate the spec's constraints, logging a warning that names the tool and the reason. Browser clients skip mirroring (dynamically named headers cannot be statically
317+
allow-listed for credentialed CORS), so calling an `x-mcp-header` tool with a non-null designated argument from a browser against a server that enforces SEP-2243 validation will be rejected — a known limitation. The legacy-era `callTool`/`listTools` paths are unchanged.
318+
310319
## Resources
311320

312321
Resources are read-only data — files, database schemas, configuration — that your application can retrieve from a server and attach as context for the model (see [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources) in the MCP overview).

docs/migration.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,21 @@ The entry performs no Origin/Host validation (see the origin-validation middlewa
11071107
headers. Power users who want to compose routing themselves can use the exported `isLegacyRequest`, `classifyInboundRequest` and `PerRequestHTTPServerTransport` building blocks directly; the handler faces are bound properties, so they can be detached and passed around
11081108
(`const { fetch } = handler`).
11091109
1110+
### `Mcp-Param-*` request-metadata headers (SEP-2243, 2026-07-28 draft)
1111+
1112+
On a 2026-07-28 connection over Streamable HTTP, `Client.callTool()` mirrors tool arguments designated with `x-mcp-header` in the tool's `inputSchema` into `Mcp-Param-{Name}` HTTP request headers (Base64-sentinel-encoded where needed) so HTTP intermediaries can route on them
1113+
without parsing the body, and `createMcpHandler` rejects a `tools/call` whose `Mcp-Param-*` headers are missing for a present body value, malformed, or disagree with the body — `400 Bad Request` with JSON-RPC `-32001` (`HeaderMismatch`). The legacy-era serving paths and the
1114+
client's legacy-era `callTool`/`listTools` are unchanged.
1115+
1116+
The Streamable HTTP transport now also emits the `Mcp-Name` standard header on every modern-enveloped request (`tools/call`/`prompts/get``params.name`; `resources/read``params.uri`), sentinel-encoded the same way, so intermediaries can route on the resource name without
1117+
parsing the body. **On a modern-enveloped request only**, an HTTP `400` whose body is a well-formed JSON-RPC error response addressed to the pending request id is now delivered in-band as a `ProtocolError` (so the `HEADER_MISMATCH` recovery retry can fire); a legacy-era
1118+
exchange still surfaces `400` as the existing `SdkHttpError`, so `e instanceof SdkHttpError && e.status === 400` callers are unchanged.
1119+
1120+
Two additive options support this: `CallToolRequestOptions.toolDefinition` (pass the tool definition directly so mirroring and output-schema validation run without a prior `tools/list`) and `TransportSendOptions.headers` (per-request HTTP headers; the Streamable HTTP transport
1121+
skips the reserved standard/auth header names so a per-request header cannot override `mcp-protocol-version`/`mcp-method`/`mcp-name`/`mcp-session-id`/`authorization`; transports that share a single channel — stdio, in-memory — ignore it). On a modern HTTP connection,
1122+
`Client.listTools()` excludes tool definitions whose `x-mcp-header` declarations violate the spec's constraints (logging a warning naming the tool and the reason) — the exclusion runs on both the no-argument aggregate and each explicit-cursor page. Browser clients skip mirroring (dynamically named headers cannot be statically allow-listed for credentialed CORS); calling an `x-mcp-header`
1123+
tool with a non-null designated argument from a browser against a conforming SEP-2243 server is therefore a known limitation.
1124+
11101125
### Serving the 2026-07-28 draft revision on stdio: `serveStdio`
11111126
11121127
The server package ships a stdio entry point that mirrors `createMcpHandler` for long-lived connections: the entry owns the transport and the era decision, the client's opening exchange selects the era for the connection, and ONE instance from your factory is pinned to that

docs/server.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ const server = new McpServer(
100100

101101
Tools let clients invoke actions on your server — they are usually the main way LLMs call into your application (see [Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools) in the MCP overview).
102102

103-
Register a tool with {@linkcode @modelcontextprotocol/server!server/mcp.McpServer#registerTool | registerTool}. Provide an `inputSchema` (Zod) to validate arguments, and optionally an `outputSchema` for structured return values:
103+
Register a tool with {@linkcode @modelcontextprotocol/server!server/mcp.McpServer#registerTool | registerTool}. Provide an `inputSchema` (Zod) to validate arguments, and optionally an `outputSchema` for structured return values.
104+
105+
> On the 2026-07-28 draft serving path, a tool whose `inputSchema` carries an `x-mcp-header` annotation has that argument mirrored into an `Mcp-Param-{Name}` HTTP request header by conforming clients. `createMcpHandler` validates those headers before dispatch and rejects a
106+
> `tools/call` whose `Mcp-Param-*` headers are missing for a present body value, malformed, or disagree with the body — `400 Bad Request` with JSON-RPC `-32001` (`HeaderMismatch`). `registerTool` warns at registration time when an `x-mcp-header` declaration violates the
107+
> spec's constraints. The 2025-era serving paths and the low-level `Server` factory shape are unchanged.
104108
105109
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_basic"
106110
server.registerTool(

0 commit comments

Comments
 (0)