Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2d66453
feat(core): SEP-2243 Mcp-Param header codec — scan, encode/decode, va…
felixweinberger Jun 18, 2026
277816b
feat(client): SEP-2243 Mcp-Param-* mirroring on 2026-07-28 connections
felixweinberger Jun 18, 2026
9c7db34
feat(server): SEP-2243 Mcp-Param-* validation at the createMcpHandler…
felixweinberger Jun 18, 2026
fbe70d8
test(conformance): arm SEP-2243 fixtures and burn down http-custom-he…
felixweinberger Jun 18, 2026
e9233f7
fix(sep-2243): address review findings on Mcp-Param mirroring + valid…
felixweinberger Jun 19, 2026
c86daca
test(e2e): add SEP-2243 Mcp-Param-* roundtrip cell on the entryModern…
felixweinberger Jun 19, 2026
1bed404
fix(sep-2243): paginate -32001 recovery refresh; guard toolInputSchem…
felixweinberger Jun 19, 2026
6bf980e
fix(sep-2243): forward only signal/timeout to the -32001 recovery ref…
felixweinberger Jun 19, 2026
9c5f680
feat(core): SEP-2243 standard-header server validation — Mcp-Method/M…
felixweinberger Jun 18, 2026
bd9b281
feat(server): SEP-2243 standard-header validation at the createMcpHan…
felixweinberger Jun 18, 2026
f912882
test(conformance): burn down http-header-validation; add SEP-2243 sta…
felixweinberger Jun 18, 2026
099fb54
fix(core): guard MCP_NAME_HEADER_SOURCE lookup with Object.hasOwn
felixweinberger Jun 19, 2026
6adef72
test(e2e): add SEP-2243 Mcp-Method header/body mismatch rejection cel…
felixweinberger Jun 19, 2026
21625a8
fix(core): stamp std-header rejection cells on a dedicated pre-dispat…
felixweinberger Jun 19, 2026
9fde57e
docs(core): name standard-header-validation rung at call site + test …
felixweinberger Jun 19, 2026
6e8a521
test(server): align era-gating test title with its initialize body
felixweinberger Jun 19, 2026
21a751c
perf(server): skip the unused forwarding clone on the isLegacyRequest…
felixweinberger Jun 18, 2026
fd07235
chore(examples): exclude sse-polling from run:examples — replay asser…
felixweinberger Jun 19, 2026
49b42c2
fix(client): 2026-era Streamable HTTP cancels by closing the per-requ…
felixweinberger Jun 18, 2026
e230a8e
fix(server): ctx.mcpReq.log emits request-related so per-request host…
felixweinberger Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/client-http-stream-close-cancel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
---

Client request cancellation on a 2026-07-28 Streamable HTTP connection now closes that request's SSE response stream — the spec cancellation signal — instead of POSTing `notifications/cancelled`. Cancellation on a 2025-era connection, and on stdio at any era, still sends `notifications/cancelled` as before. Adds the optional `Transport.hasPerRequestStream` capability flag (set on `StreamableHTTPClientTransport`) for the protocol layer to route the per-transport cancel path.
6 changes: 6 additions & 0 deletions .changeset/sep-2243-mcp-param-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
---

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 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 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`) and surfaces an HTTP 400 carrying a JSON-RPC error response in-band as a `ProtocolError`; transports that share a single channel (stdio, in-memory) ignore it.
5 changes: 5 additions & 0 deletions .changeset/sep-2243-mcp-param-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': minor
---

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.
5 changes: 5 additions & 0 deletions .changeset/sep-2243-std-header-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': minor
---

SEP-2243 standard-header server-side validation (protocol revision 2026-07-28). On the modern (2026-07-28) serving path, `createMcpHandler` now enforces the required `Mcp-Method` and `Mcp-Name` standard request headers in addition to the existing `MCP-Protocol-Version` and `Mcp-Method` cross-checks: a modern request without an `Mcp-Method` header, a `tools/call` / `prompts/get` / `resources/read` request without an `Mcp-Name` header, an `Mcp-Name` header carrying an invalid `=?base64?…?=` sentinel, and an `Mcp-Name` header whose (decoded) value disagrees with the body's `params.name` / `params.uri` are all rejected with `400 Bad Request` and JSON-RPC `-32001` (`HeaderMismatch`). The 2025-era serving paths are unchanged.
5 changes: 5 additions & 0 deletions .changeset/server-ctx-log-request-related.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': patch
---

`ctx.mcpReq.log()` now emits its `notifications/message` notification request-related (like progress and `ctx.mcpReq.notify`), so handler-emitted log messages are delivered when the server is hosted per request via `createMcpHandler` instead of being silently dropped. On a 2026-07-28 request the level filter consults the per-request `_meta` `io.modelcontextprotocol/logLevel` key (the modern equivalent of `logging/setLevel`). The session-scoped `Server.sendLoggingMessage()` API is unchanged.
9 changes: 9 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,15 @@ const result = await client.callTool(
console.log(result.content);
```

### `x-mcp-header` parameter mirroring (2026-07-28 draft)

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
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,
when a conforming server rejects it with `-32001` (`HeaderMismatch`), `callTool()` refreshes the definition cache once and retries.

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
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.

## Resources

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).
Expand Down
4 changes: 4 additions & 0 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,10 @@ side: auto-fulfilment is on by default (`ClientOptions.inputRequired`, `maxRound

`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` now return empty results when the server lacks the corresponding capability (instead of sending the request). Set `enforceStrictCapabilities: true` in `ClientOptions` to throw an error instead.

No code changes required; wire-behavior note: on a 2026-07-28 Streamable HTTP connection, aborting an in-flight client request (caller `signal` / timeout) closes that request's SSE response stream as the spec cancellation signal — `notifications/cancelled` is no longer POSTed
there. 2025-era connections and stdio at any era still send `notifications/cancelled`. Custom `Transport` implementations that open one underlying request per outbound message and honor `TransportSendOptions.requestSignal` may declare `readonly hasPerRequestStream = true` to opt
into the same routing.

### Server (Streamable HTTP transport)

No code changes required; these are wire-behavior notes:
Expand Down
43 changes: 33 additions & 10 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1030,16 +1030,16 @@ versionNegotiation: {
}
```

`maxRetries` governs timeout re-sends only (the spec-mandated `-32004` corrective continuation — select-and-continue with a mutual version — is a separate negotiation step and is never counted against it). Negotiation can also be configured pre-connect on an
already-constructed instance via `client.setVersionNegotiation(options)` (equivalent to the constructor option; throws after connecting).
`maxRetries` governs timeout re-sends only (the spec-mandated `-32004` corrective continuation — select-and-continue with a mutual version — is a separate negotiation step and is never counted against it). Negotiation can also be configured pre-connect on an already-constructed
instance via `client.setVersionNegotiation(options)` (equivalent to the constructor option; throws after connecting).

Once a modern era is negotiated, the client **automatically attaches the per-request `_meta` envelope** (the reserved protocol-version / client-info / client-capabilities keys) to every outgoing request and notification — you never set it by hand. Any `_meta` keys you pass
in a request are preserved over the auto-attached ones. After connect, `client.getProtocolEra()` returns `'legacy'` or `'modern'` and `client.getNegotiatedProtocolVersion()` the exact revision.
Once a modern era is negotiated, the client **automatically attaches the per-request `_meta` envelope** (the reserved protocol-version / client-info / client-capabilities keys) to every outgoing request and notification — you never set it by hand. Any `_meta` keys you pass in a
request are preserved over the auto-attached ones. After connect, `client.getProtocolEra()` returns `'legacy'` or `'modern'` and `client.getNegotiatedProtocolVersion()` the exact revision.

On the server side, `server/discover` (advertising only the modern revisions) is served by instances hosted through one of the 2026-era serving entries; a hand-constructed `Server`/`McpServer` is byte-identical to before (it keeps answering `-32601`, and the `initialize`
handshake only ever negotiates 2025-era versions — a 2026-era revision is never accepted or counter-offered there). Serving the 2026 revision to ordinary HTTP traffic is done with the `createMcpHandler` entry point described in the next section; serving it on stdio (and
other long-lived connections) is the `serveStdio` entry point described after that. The client can also issue `client.discover()` directly on a 2026-era connection; on a 2025-era connection the method is rejected locally with a typed error, since it does not exist on that
protocol revision.
handshake only ever negotiates 2025-era versions — a 2026-era revision is never accepted or counter-offered there). Serving the 2026 revision to ordinary HTTP traffic is done with the `createMcpHandler` entry point described in the next section; serving it on stdio (and other
long-lived connections) is the `serveStdio` entry point described after that. The client can also issue `client.discover()` directly on a 2026-era connection; on a 2025-era connection the method is rejected locally with a typed error, since it does not exist on that protocol
revision.

### Serving the 2026-07-28 draft revision over HTTP: `createMcpHandler`

Expand Down Expand Up @@ -1099,6 +1099,21 @@ The entry performs no Origin/Host validation (see the origin-validation middlewa
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
(`const { fetch } = handler`).

### `Mcp-Param-*` request-metadata headers (SEP-2243, 2026-07-28 draft)

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
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
client's legacy-era `callTool`/`listTools` are unchanged.

Two additive options support this: `CallToolRequestOptions.toolDefinition` (pass the tool definition directly so mirroring runs without a prior `tools/list`) and `TransportSendOptions.headers` (per-request HTTP headers; the Streamable HTTP transport 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, `Client.listTools()` excludes
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 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.

On the modern path, `createMcpHandler` also validates the SEP-2243 **standard** request-metadata headers against the body and rejects with the same `400` / `-32001` (`HeaderMismatch`) when the `MCP-Protocol-Version` or `Mcp-Method` header disagrees with the body, when the
required `Mcp-Method` header is absent, when the required `Mcp-Name` header is absent on a `tools/call` / `prompts/get` / `resources/read` request, and when the (Base64-sentinel-decoded) `Mcp-Name` value disagrees with `params.name` / `params.uri`. These checks only fire on the
modern (2026-07-28) serving path — 2025-era traffic is unchanged — and a hand-built modern HTTP request must carry the `Mcp-Method` (and where applicable `Mcp-Name`) header; the SDK client already sends them.

### Serving the 2026-07-28 draft revision on stdio: `serveStdio`

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
Expand Down Expand Up @@ -1172,9 +1187,17 @@ const handler = createMcpHandler(() => buildServer());
handler.notify.toolsChanged();
```

**Client side.** `ClientOptions.listChanged` keeps working: on a 2026-07-28 connection the SDK auto-opens a `subscriptions/listen` stream whose filter is the intersection of the configured sub-options and the server-advertised `listChanged` capabilities, so the same handlers
fire on every published change (the auto-opened subscription is exposed at `client.autoOpenedSubscription` for `close()`; when the intersection is empty auto-open is skipped and `autoOpenedSubscription` stays `undefined`). `client.listen(filter)` opens a stream explicitly and resolves once the server's acknowledged notification arrives with `{ honoredFilter, close(), closed }` (where `closed` is a `Promise<'local' | 'remote'>` that resolves once on termination — `'remote'` means the server cancelled, the stream ended, or the transport dropped, so re-listen if you still want events); change notifications dispatch to the existing `setNotificationHandler`
registrations. `resources/subscribe` is 2025-only — on a 2026-07-28 connection, request `notifications/resources/updated` via the `resourceSubscriptions` field of the listen filter instead.
**Client side.** `ClientOptions.listChanged` keeps working: on a 2026-07-28 connection the SDK auto-opens a `subscriptions/listen` stream whose filter is the intersection of the configured sub-options and the server-advertised `listChanged` capabilities, so the same handlers fire
on every published change (the auto-opened subscription is exposed at `client.autoOpenedSubscription` for `close()`; when the intersection is empty auto-open is skipped and `autoOpenedSubscription` stays `undefined`). `client.listen(filter)` opens a stream explicitly and resolves
once the server's acknowledged notification arrives with `{ honoredFilter, close(), closed }` (where `closed` is a `Promise<'local' | 'remote'>` that resolves once on termination — `'remote'` means the server cancelled, the stream ended, or the transport dropped, so re-listen if
you still want events); change notifications dispatch to the existing `setNotificationHandler` registrations. `resources/subscribe` is 2025-only — on a 2026-07-28 connection, request `notifications/resources/updated` via the `resourceSubscriptions` field of the listen filter
instead.

### Client cancellation on Streamable HTTP (2026-07-28): stream-close is the signal

On a 2026-07-28 Streamable HTTP connection, aborting an in-flight client request (caller `signal` or timeout) now closes that request's SSE response stream — the spec cancellation signal for this transport — instead of POSTing a `notifications/cancelled` message. Nothing to change in calling code: `RequestOptions.signal` and `timeout` behave exactly as before. Cancellation on a 2025-era
connection, and on stdio at any era, is unchanged and still sends `notifications/cancelled`. Custom `Transport` implementations that open one underlying request per outbound JSON-RPC request and honor `TransportSendOptions.requestSignal` may opt into the same routing by declaring
`readonly hasPerRequestStream = true`.

### Multi round-trip requests (2026-07-28): write-once handlers and the client auto-fulfilment driver

Expand Down
6 changes: 5 additions & 1 deletion docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ const server = new McpServer(

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).

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:
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.

> 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
> `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
> spec's constraints. The 2025-era serving paths and the low-level `Server` factory shape are unchanged.

```ts source="../examples/guides/serverGuide.examples.ts#registerTool_basic"
server.registerTool(
Expand Down
3 changes: 2 additions & 1 deletion examples/json-response/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ runClient('json-response', async () => {
headers: {
'content-type': 'application/json',
accept: 'application/json, text/event-stream',
'mcp-protocol-version': '2026-07-28'
'mcp-protocol-version': '2026-07-28',
'mcp-method': 'tools/list'
},
body: JSON.stringify({
jsonrpc: '2.0',
Expand Down
1 change: 1 addition & 0 deletions examples/sse-polling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"era": "legacy",
"path": "/mcp",
"timeoutMs": 20000,
"excluded": "replay assertion is timing-sensitive on CI; revisit",
"//": "SEP-1699 closeSSE/eventStore/retryInterval live on the sessionful-2025 transport; the client is era-blind so dual would duplicate."
}
}
Loading
Loading