You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(client,server): SEP-2243 Mcp-Param-* mirroring/validation on the response-cache substrate
callTool() reads the x-mcp-header scan from the cached tools/list entry (via
ClientResponseCache.toolDefinition) and on a HEADER_MISMATCH evicts tools/list
(store-failure-safe), repopulates via the internal aggregating walk
(_ensureCachedToolsList), and retries once. Output-schema validation reads the
same cached entry via ClientResponseCache.outputValidator. The legacy
cacheToolMetadata / _cachedToolOutputValidators path is retired; public
listTools()/listPrompts()/listResources()/listResourceTemplates() are unchanged
(per-page).
The SEP-2243 invalid-x-mcp-header exclusion runs on the CACHED aggregate before
the write (era-gated only — the cached entry is never read for mirroring on
non-HTTP transports, so over-excluding there is harmless); whether the spec's
exclusion MUST also applies to the public per-page listTools() return is an
open question. Mcp-Name is sentinel-encoded via the same encodeMcpParamValue
path as Mcp-Param-* (CRLF / non-ASCII safe; pinned by core test).
RESERVED_REQUEST_HEADER_NAMES sits before the anySignal JSDoc so the doc
comment stays attached.
Static-reachability sweep covers items/additionalProperties/oneOf/anyOf/allOf/
not/if/then/else/$defs and now also dependentSchemas/unevaluatedProperties/
unevaluatedItems/propertyNames/definitions; fixtures for each. Server:
400+JSON-RPC body delivered in-band as ProtocolError on modern-enveloped
requests only; tool.update({name}) onto an already-registered name drops the
memo under BOTH old and new keys; toolInputSchemaJson lazy fallback memoizes;
per-tool output-schema compile guard.
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).
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 on a non-stdio modern connection `Client.listTools()` (and the client's internal `tools/list` cache) exclude tool definitions whose `x-mcp-header` declarations violate the spec's constraints, logging a warning naming the tool and the reason. The legacy-era `callTool` and `listTools` paths are unchanged at the wire level. 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 (every era):** output-schema validator compilation is now lazy — validators are compiled on the first `callTool()` against the cached `tools/list` entry, not eagerly inside `listTools()` — and non-throwing: an uncompilable `outputSchema` is `console.warn`-ed and validation is skipped for that tool only (previously `listTools()` threw). A pluggable `jsonSchemaValidator` provider therefore observes compilation at `callTool` time, not `listTools` time. The legacy-era `listTools()` path is unchanged at the wire level but is observably different at the validator-lifecycle level.
11
+
12
+
**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.
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.
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
307
+
are built from the client's internal `tools/list` cache; 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
308
+
and, when a conforming server rejects it with `-32001` (`HeaderMismatch`), `callTool()` refreshes the definition cache once and retries.
309
+
310
+
On a non-stdio modern connection `listTools()` (and the internal `tools/list` cache) exclude 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
311
+
headers cannot be statically 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`
312
+
paths are unchanged.
313
+
304
314
## Resources
305
315
306
316
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).
Copy file name to clipboardExpand all lines: docs/migration-SKILL.md
+13-1Lines changed: 13 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -116,14 +116,17 @@ Three error classes now exist:
116
116
| Capability not supported |`new Error(...)`|`SdkError` with `SdkErrorCode.CapabilityNotSupported`|
117
117
| Not connected |`new Error('Not connected')`|`SdkError` with `SdkErrorCode.NotConnected`|
118
118
| Invalid params (server response) |`McpError` with `ErrorCode.InvalidParams`|`ProtocolError` with `ProtocolErrorCode.InvalidParams`|
119
-
| HTTP transport error |`StreamableHTTPError`|`SdkHttpError` with `SdkErrorCode.ClientHttp*`|
119
+
| HTTP transport error (legacy era)|`StreamableHTTPError`|`SdkHttpError` with `SdkErrorCode.ClientHttp*`|
120
120
| Failed to open SSE stream |`StreamableHTTPError`|`SdkHttpError` with `SdkErrorCode.ClientHttpFailedToOpenStream`|
121
121
| 401 after re-auth (circuit break) |`StreamableHTTPError`|`SdkHttpError` with `SdkErrorCode.ClientHttpAuthentication`|
122
122
| 403 after upscoping |`StreamableHTTPError`|`SdkHttpError` with `SdkErrorCode.ClientHttpForbidden`|
123
123
| Unexpected content type |`StreamableHTTPError`|`SdkError` with `SdkErrorCode.ClientHttpUnexpectedContent`|
124
124
| Session termination failed |`StreamableHTTPError`|`SdkHttpError` with `SdkErrorCode.ClientHttpFailedToTerminateSession`|
125
125
| Response result fails schema |`ZodError` (raw) |`SdkError` with `SdkErrorCode.InvalidResult`|
126
126
127
+
**Modern-era exception** to the `SdkHttpError` rows above: on a modern-enveloped (2026-07-28) Streamable HTTP request, an HTTP `400` whose body is a well-formed JSON-RPC error response addressed to the pending request id is delivered in-band as a `ProtocolError` (e.g. `-32001`
128
+
HeaderMismatch from a SEP-2243 `Mcp-Param-*` rejection), not as `SdkHttpError`. Legacy-era exchanges and generic HTTP failures are unchanged.
129
+
127
130
New `SdkErrorCode` enum values:
128
131
129
132
-`SdkErrorCode.NotConnected` = `'NOT_CONNECTED'`
@@ -174,6 +177,13 @@ if (error instanceof SdkHttpError) {
174
177
break;
175
178
}
176
179
}
180
+
// Modern-era (2026-07-28) only: a 400 carrying a JSON-RPC error body addressed
181
+
// to the pending request id surfaces as ProtocolError, NOT SdkHttpError — e.g.
182
+
// a SEP-2243 -32001 HeaderMismatch from createMcpHandler. Legacy-era 400s and
183
+
// generic HTTP failures still map to SdkHttpError above.
@@ -562,6 +572,8 @@ side: auto-fulfilment is on by default (`ClientOptions.inputRequired`, `maxRound
562
572
563
573
`Client.listTools()`, `listPrompts()`, `listResources()`, `listResourceTemplates()` called without a `cursor` now auto-aggregate every page and return the complete result (`nextCursor: undefined`); an explicit `{ cursor }` string still returns one page. Manual `do { … } while (cursor !== undefined)` loops keep working (the first call returns everything and the loop exits after one iteration) — replace them with the bare no-arg call. New `ClientOptions.listMaxPages` (default 64) caps the aggregate walk only; overrun throws `SdkError` (`SdkErrorCode.ListPaginationExceeded`).
564
574
575
+
Output-schema validator compilation is now lazy (first `callTool()` against the cached `tools/list` entry) and non-throwing (an uncompilable `outputSchema` is `console.warn`-ed and validation is skipped for that tool only); `listTools()` no longer throws on an uncompilable `outputSchema`. Applies on every era — the legacy-era `listTools()` path is unchanged at the wire level only.
576
+
565
577
### Server (Streamable HTTP transport)
566
578
567
579
No code changes required; these are wire-behavior notes:
Copy file name to clipboardExpand all lines: docs/migration.md
+19Lines changed: 19 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1121,6 +1121,25 @@ The entry performs no Origin/Host validation (see the origin-validation middlewa
1121
1121
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
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
1127
+
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 — `400BadRequest` with JSON-RPC `-32001` (`HeaderMismatch`). The legacy-era serving paths and the
1128
+
client's legacy-era `callTool`/`listTools` are unchanged at the wire level (see the validator-lifecycle note below for the one observable local change).
1129
+
1130
+
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
1131
+
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
1132
+
exchange still surfaces `400` as the existing `SdkHttpError`, so `einstanceofSdkHttpError&&e.status===400` callers are unchanged.
1133
+
1134
+
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
1135
+
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 non-stdio modern connection,
1136
+
`Client.listTools()` (and the client's internal `tools/list` cache) exclude tool definitions whose `x-mcp-header` declarations violate the spec's constraints, logging a warning naming the tool and the reason. Browser clients skip mirroring (dynamically named headers cannot be
1137
+
statically allow-listed for credentialed CORS); calling an `x-mcp-header` tool with a non-null designated argument from a browser against a conforming SEP-2243 server is therefore a known limitation.
1138
+
1139
+
**Output-schema validator lifecycle (every era):** validator compilation is now lazy — validators are compiled on the first `callTool()` against the cached `tools/list` entry, not eagerly inside `listTools()` — and non-throwing: an uncompilable `outputSchema` is `console.warn`-ed
1140
+
and validation is skipped for that tool only. In v1, `listTools()` threw on an uncompilable `outputSchema`; now it succeeds, and a pluggable `jsonSchemaValidator` provider observes compilation at `callTool` time, not `listTools` time. The legacy-era `listTools()` path is
1141
+
unchanged at the wire level but is observably different at the validator-lifecycle level.
1142
+
1124
1143
### Serving the 2026-07-28 draft revision on stdio: `serveStdio`
1125
1144
1126
1145
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
Copy file name to clipboardExpand all lines: docs/server.md
+5-1Lines changed: 5 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -100,7 +100,11 @@ const server = new McpServer(
100
100
101
101
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).
102
102
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.
0 commit comments