diff --git a/.changeset/envelope-auto-emission.md b/.changeset/envelope-auto-emission.md index d832c64d8..9ae614a84 100644 --- a/.changeset/envelope-auto-emission.md +++ b/.changeset/envelope-auto-emission.md @@ -8,4 +8,4 @@ Per-request `_meta` envelope auto-emission on modern-era connections: once a cli (the default, and the `'auto'`-mode fallback) never gain these keys, so 2025-era outbound traffic is byte-identical to before. Adds `Client.getProtocolEra()` (`'legacy' | 'modern' | undefined`), the `ProtocolEra` type, `Client.setVersionNegotiation()` for configuring negotiation pre-connect on an already-constructed instance, and the `probe.maxRetries` knob (default `0`) which governs probe-timeout -re-sends only — the spec-mandated `-32004` corrective continuation is never counted against it. The `versionNegotiation` default remains `'legacy'`: absent (or `mode: 'legacy'`), `connect()` runs the plain 2025 sequence, byte-identical to a v1.x client. +re-sends only — the spec-mandated `-32022` corrective continuation is never counted against it. The `versionNegotiation` default remains `'legacy'`: absent (or `mode: 'legacy'`), `connect()` runs the plain 2025 sequence, byte-identical to a v1.x client. diff --git a/.changeset/missing-client-capability-error.md b/.changeset/missing-client-capability-error.md index 9653679e0..842e44073 100644 --- a/.changeset/missing-client-capability-error.md +++ b/.changeset/missing-client-capability-error.md @@ -4,4 +4,4 @@ '@modelcontextprotocol/server': minor --- -Add `MissingRequiredClientCapabilityError`, the typed error class for the 2026-07-28 `-32003` protocol error (processing a request requires a capability the client did not declare). Its `data.requiredCapabilities` lists the missing capabilities and `ProtocolError.fromError` recognizes the code/data shape. The 2026-07-28 HTTP entry gains a pre-dispatch gate that refuses a request requiring an undeclared client capability with this error and HTTP status `400`; no method served on the 2026-07-28 registry currently carries such a requirement, so observable behavior is unchanged until methods with capability requirements exist. +Add `MissingRequiredClientCapabilityError`, the typed error class for the 2026-07-28 `-32021` protocol error (processing a request requires a capability the client did not declare). Its `data.requiredCapabilities` lists the missing capabilities and `ProtocolError.fromError` recognizes the code/data shape. The 2026-07-28 HTTP entry gains a pre-dispatch gate that refuses a request requiring an undeclared client capability with this error and HTTP status `400`; no method served on the 2026-07-28 registry currently carries such a requirement, so observable behavior is unchanged until methods with capability requirements exist. diff --git a/.changeset/mrtr-server-seam.md b/.changeset/mrtr-server-seam.md index 861d7df9b..51f7fa06b 100644 --- a/.changeset/mrtr-server-seam.md +++ b/.changeset/mrtr-server-seam.md @@ -6,7 +6,7 @@ Add the server side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). Handlers for `tools/call`, `prompts/get`, and `resources/read` can return the value built by `inputRequired()` (exported from the server package together with `acceptedContent()`) to request additional client input in-band; the structured-content requirement and the tools/call result-schema validation are skipped for that return, the encode seam emits it as `resultType: 'input_required'`, and the handler reads the responses on re-entry from `ctx.mcpReq.inputResponses` (with non-bare entries reported via `ctx.mcpReq.droppedInputResponseKeys`). The seam re-checks the at-least-one rule for hand-built results, checks every embedded request against the capabilities the client declared on that request's envelope -(answering the typed `-32003` error on violation), and fails loudly — never emitting a mis-typed result — when an input-required value is returned from any other method or toward a 2025-era request. A `UrlElicitationRequiredError` escaping a handler on a 2026-era request +(answering the typed `-32021` error on violation), and fails loudly — never emitting a mis-typed result — when an input-required value is returned from any other method or toward a 2025-era request. A `UrlElicitationRequiredError` escaping a handler on a 2026-era request fails as an internal error with a clear steer to `inputRequired.elicitUrl(...)`, so the `-32042` error never reaches the 2026-07-28 wire; 2025-era serving keeps today's `-32042` behavior exactly. The typed local error raised when push-style server-to-client request APIs are used while serving a 2026-era request now steers to `inputRequired(...)`. Tool, prompt, and resource callback types accept the new return alongside their existing result types; 2025-era wire behavior is unchanged. An optional `ServerOptions.requestState.verify` hook lets a server integrity-check the echoed `requestState` before the handler runs — a throw answers the wire-level `-32602` Invalid Params error with `data.reason: 'invalid_request_state'`; the SDK provides no default verification. diff --git a/.changeset/pin-modern-rejection-codes.md b/.changeset/pin-modern-rejection-codes.md index f54bdd978..9638c7eac 100644 --- a/.changeset/pin-modern-rejection-codes.md +++ b/.changeset/pin-modern-rejection-codes.md @@ -3,4 +3,4 @@ '@modelcontextprotocol/server': patch --- -Pin the modern (2026-07-28) HTTP serving path's rejection codes to the assignments the published conformance suite asserts: a header/body cross-check mismatch (`MCP-Protocol-Version` or `Mcp-Method` disagreeing with the request body) is now rejected with `-32001` (HeaderMismatch), and a request whose protocol-version header names a modern revision but whose body is missing the `_meta` envelope (or its required protocol-version key) is rejected with `-32602` invalid params naming the missing key(s). Both keep HTTP 400. These cells previously emitted a provisional `-32004` while the upstream error-code discussion was open. The envelope-less rejection on a modern-only endpoint (`-32004` with the supported-versions list), the 2025-era serving paths, and the client-side probe handling are unchanged. +Pin the modern (2026-07-28) HTTP serving path's rejection codes to the assignments the published conformance suite asserts: a header/body cross-check mismatch (`MCP-Protocol-Version` or `Mcp-Method` disagreeing with the request body) is now rejected with `-32020` (HeaderMismatch), and a request whose protocol-version header names a modern revision but whose body is missing the `_meta` envelope (or its required protocol-version key) is rejected with `-32602` invalid params naming the missing key(s). Both keep HTTP 400. These cells previously emitted a provisional `-32022` while the upstream error-code discussion was open. The envelope-less rejection on a modern-only endpoint (`-32022` with the supported-versions list), the 2025-era serving paths, and the client-side probe handling are unchanged. diff --git a/.changeset/sep-2243-mcp-param-server.md b/.changeset/sep-2243-mcp-param-server.md index 796b42ec5..fc0316cfb 100644 --- a/.changeset/sep-2243-mcp-param-server.md +++ b/.changeset/sep-2243-mcp-param-server.md @@ -2,4 +2,4 @@ '@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. +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 `-32020` (`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. diff --git a/.changeset/sep-2243-std-header-server.md b/.changeset/sep-2243-std-header-server.md index dd2cd3796..8e3c250ce 100644 --- a/.changeset/sep-2243-std-header-server.md +++ b/.changeset/sep-2243-std-header-server.md @@ -2,4 +2,4 @@ '@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. +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 `-32020` (`HeaderMismatch`). The 2025-era serving paths are unchanged. diff --git a/.changeset/spec-2907-error-code-renumber.md b/.changeset/spec-2907-error-code-renumber.md new file mode 100644 index 000000000..aeed3f11e --- /dev/null +++ b/.changeset/spec-2907-error-code-renumber.md @@ -0,0 +1,7 @@ +--- +'@modelcontextprotocol/core': minor +'@modelcontextprotocol/client': minor +'@modelcontextprotocol/server': minor +--- + +Align the 2026-07-28 protocol error codes to the spec renumber: `HeaderMismatch` is now `-32020` (was `-32001`), `MissingRequiredClientCapability` is now `-32021` (was `-32003`), and `UnsupportedProtocolVersion` is now `-32022` (was `-32004`). These codes are part of the draft 2026-07-28 protocol revision only and have never appeared on a 2025-era wire — the 2025 serving paths and the SDK-conventional `-32001` (`Session not found`) on the stateful Streamable HTTP transport are unchanged. `ProtocolErrorCode.MissingRequiredClientCapability`, `ProtocolErrorCode.UnsupportedProtocolVersion`, the `HEADER_MISMATCH_ERROR_CODE` constant, and the `HEADER_MISMATCH` / `MISSING_REQUIRED_CLIENT_CAPABILITY` / `UNSUPPORTED_PROTOCOL_VERSION` spec-type constants now carry the renumbered values; the `UnsupportedProtocolVersionError` and `MissingRequiredClientCapabilityError` classes (and `ProtocolError.fromError` recognition) follow. The client probe classifier recognizes `-32022` for the corrective continuation and the SEP-2243 one-refresh-on-miss retry triggers on `-32020`. diff --git a/.changeset/spec-anchor-repin-2fb207da.md b/.changeset/spec-anchor-repin-2fb207da.md index b021ff4d7..feaf90002 100644 --- a/.changeset/spec-anchor-repin-2fb207da.md +++ b/.changeset/spec-anchor-repin-2fb207da.md @@ -8,6 +8,6 @@ Re-pin the 2026-07-28 draft references (spec reference types, vendored schema.js - `notifications/elicitation/complete` is no longer part of the 2026-07-28 wire registry (the draft removed the notification together with `elicitationId` on URL-mode elicitation). On connections negotiated at 2026-07-28, sending it — including via `Server.createElicitationCompletionNotifier()` — now fails locally with `SdkErrorCode.MethodNotSupportedByProtocolVersion`, and inbound copies are dropped as unknown notifications. Both remain fully supported on 2025-11-25. - `notifications/cancelled` on 2026-era connections now parses with a revision-exact schema that requires `requestId` (the draft made it required); the notification `_meta` shape types the `io.modelcontextprotocol/subscriptionId` key. 2025-era parsing is unchanged. -- The error code `-32001` emitted for HTTP header/body mismatches is now defined by the draft schema (`HEADER_MISMATCH`); the emitted behavior is unchanged (documentation only). +- The error code `-32020` emitted for HTTP header/body mismatches is now defined by the draft schema (`HEADER_MISMATCH`); the emitted behavior is unchanged (documentation only). No public API surface changes; the regenerated reference artifacts are internal/test-only. diff --git a/.changeset/spec-reference-types-2026-07-28.md b/.changeset/spec-reference-types-2026-07-28.md index df0101e05..36f0a1f67 100644 --- a/.changeset/spec-reference-types-2026-07-28.md +++ b/.changeset/spec-reference-types-2026-07-28.md @@ -3,4 +3,4 @@ '@modelcontextprotocol/codemod': patch --- -Add per-revision spec reference types (2025-11-25 and 2026-07-28) with split comparison tests, and the 2026-07-28 wire contract surface: request-meta key constants, `RequestMetaEnvelopeSchema`, `server/discover` shapes, the typed `-32004` error, the `-32003` code constant, and a `resultType` passthrough on the base result. Types and constants only — no behavior changes. +Add per-revision spec reference types (2025-11-25 and 2026-07-28) with split comparison tests, and the 2026-07-28 wire contract surface: request-meta key constants, `RequestMetaEnvelopeSchema`, `server/discover` shapes, the typed `-32022` error, the `-32021` code constant, and a `resultType` passthrough on the base result. Types and constants only — no behavior changes. diff --git a/docs/client.md b/docs/client.md index 92f39a78c..377644ec5 100644 --- a/docs/client.md +++ b/docs/client.md @@ -311,7 +311,7 @@ console.log(result.content); 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. +when a conforming server rejects it with `-32020` (`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. diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index d67a551b9..2ad1a6347 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -574,6 +574,8 @@ No code changes required; these are wire-behavior notes: longer enable it. Behavior for all currently supported protocol versions is unchanged. - Session-ID mismatch still responds `404 Not Found` with JSON-RPC error code `-32001` (`Session not found`), unchanged from v1. This `-32001` usage is an SDK convention, not a spec-assigned code, and may be re-derived as 2026 protocol revision error handling is adopted — migrated client code should key off the HTTP `404` status, not the `-32001` code. +- The 2026-07-28 draft error codes were renumbered between v2 alphas: `HeaderMismatch` `-32001`→`-32020`, `MissingRequiredClientCapability` `-32003`→`-32021`, `UnsupportedProtocolVersion` `-32004`→`-32022`. No v1.x→v2 impact (these codes never existed in v1); v2-alpha code that + hard-coded the old literals must update — prefer `ProtocolErrorCode.*` / `HEADER_MISMATCH_ERROR_CODE`. ### Server (deprecated accessors and app-factory Origin validation) @@ -595,7 +597,7 @@ New in 2.0 — v1 has no equivalent API. How v1 Streamable HTTP hosting maps ont - An existing sessionful v1 Streamable HTTP setup (a `StreamableHTTPServerTransport` wiring with session IDs) keeps serving 2025 clients by routing in user land in front of a strict entry: `if (await isLegacyRequest(request)) return myExistingLegacyHandler(request); return strictHandler.fetch(request)` where `strictHandler = createMcpHandler(factory, { legacy: 'reject' })`. - `isLegacyRequest(request: Request, parsedBody?: unknown): Promise` from `@modelcontextprotocol/server` is the entry's own classification step. Returns `true` only for requests with no per-request `_meta` envelope claim (claim-less POSTs including `initialize`, - GET/DELETE session operations, all-legacy batches, posted responses, non-JSON bodies). Returns `false` for envelope-claiming requests AND for malformed/incomplete modern claims (the modern path answers those with `-32602`/`-32001`) — route `false` traffic to the modern handler, + GET/DELETE session operations, all-legacy batches, posted responses, non-JSON bodies). Returns `false` for envelope-claiming requests AND for malformed/incomplete modern claims (the modern path answers those with `-32602`/`-32020`) — route `false` traffic to the modern handler, never to a legacy handler. The predicate classifies a clone (the body stays readable); pass the parsed body as the second argument when the stream was already consumed. - `legacyStatelessFallback(factory)` is exported as a standalone fetch-shaped handler producing the same stateless legacy serving as the default. - The handler is web-standards-only (`{ fetch, close, notify, bus }`). On Workers / Bun / Deno, `export default handler` works directly. On Node frameworks (Express, Fastify, plain `node:http`), wrap once with `toNodeHandler(handler, { onerror? })` from diff --git a/docs/migration.md b/docs/migration.md index 89855c0ec..804d3db82 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -942,7 +942,7 @@ The protocol layer enforces the same boundary at runtime: The wire layer is now split into per-revision codecs inside the (private, bundled) core: one codec serves every 2025-era protocol version (2024-10-07 … 2025-11-25) and one serves 2026-07-28. The codec is selected by the negotiated protocol version, which is connection state on the `Client`/`Server` instance: the client stores it when its initialize handshake completes, the server stores it when it answers `initialize`, and instances with no negotiated version default to the 2025 era (with the pre-negotiation lifecycle messages routed by method: `initialize`/`notifications/initialized` are 2025-era vocabulary, `server/discover` is 2026-era vocabulary). An edge classification (`MessageExtraInfo.classification`) no longer switches the era per message — it is validated against the instance era, and a mismatch is rejected as -an entry/routing error (`-32004 Unsupported protocol version` for requests, a drop plus `onerror` for notifications). Methods deleted by a protocol revision are now PHYSICALLY absent from that era's registry: an inbound `tasks/get` on a 2026-era connection gets `-32601` even if a +an entry/routing error (`-32022 Unsupported protocol version` for requests, a drop plus `onerror` for notifications). Methods deleted by a protocol revision are now PHYSICALLY absent from that era's registry: an inbound `tasks/get` on a 2026-era connection gets `-32601` even if a handler is registered, and sending an era-mismatched spec method (for example `server/discover` toward a 2025-era peer, or any `tasks/*` method toward a 2026-era peer) throws a typed local error — `SdkError` with the new code `SdkErrorCode.MethodNotSupportedByProtocolVersion` — before anything reaches the transport. @@ -1030,7 +1030,7 @@ 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 +`maxRetries` governs timeout re-sends only (the spec-mandated `-32022` 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 @@ -1069,7 +1069,7 @@ How the `legacy` option behaves: - **omitted / `legacy: 'stateless'`** (the default) — 2025-era (non-envelope) traffic is served per request through the established stateless idiom: a fresh instance from the same factory and a streamable HTTP transport constructed with only `sessionIdGenerator: undefined`. Because this serving is per-request and stateless, GET and DELETE (2025 session operations) are answered `405` / `Method not allowed.`, exactly like the canonical stateless example. The exported `legacyStatelessFallback(factory)` is the same serving as a standalone fetch-shaped handler for hand-wired compositions. -- **`legacy: 'reject'`** — modern-only strict. 2026-07-28 (per-request `_meta` envelope) requests are served; 2025-era requests are rejected with `-32004` naming the supported revisions, and 2025-era notifications are acknowledged with `202` and dropped. **There is no 2025 +- **`legacy: 'reject'`** — modern-only strict. 2026-07-28 (per-request `_meta` envelope) requests are served; 2025-era requests are rejected with `-32022` naming the supported revisions, and 2025-era notifications are acknowledged with `202` and dropped. **There is no 2025 serving in this mode.** > **If you have an existing sessionful 1.x Streamable HTTP setup** (a `StreamableHTTPServerTransport` wiring with session IDs that your deployed 2025-era clients depend on), keep that handler serving 2025 traffic and route it in front of a strict (`legacy: 'reject'`) entry with @@ -1106,7 +1106,7 @@ 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 +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 `-32020` (`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 @@ -1114,7 +1114,7 @@ standard/auth header names so a per-request header cannot override `mcp-protocol 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 +On the modern path, `createMcpHandler` also validates the SEP-2243 **standard** request-metadata headers against the body and rejects with the same `400` / `-32020` (`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. @@ -1275,12 +1275,16 @@ try { } ``` -### Typed `-32003` missing-client-capability error +### Typed `-32021` missing-client-capability error -`MissingRequiredClientCapabilityError` is the typed error class for the 2026-07-28 `-32003` protocol error: processing a request requires a capability the client did not declare in the request's `clientCapabilities`. Its `data.requiredCapabilities` lists the missing capabilities, +`MissingRequiredClientCapabilityError` is the typed error class for the 2026-07-28 `-32021` protocol error: processing a request requires a capability the client did not declare in the request's `clientCapabilities`. Its `data.requiredCapabilities` lists the missing capabilities, and `ProtocolError.fromError` recognizes the code/data shape (recognize peers' errors by their code and `error.data`, not by `instanceof`). When the HTTP entry refuses such a request, the response uses HTTP status `400` as the specification requires. The multi-round-trip seam answers with the same error when a handler embeds an input request (for example an elicitation) that the request's declared client capabilities do not cover. +> **Draft-only renumber**: the 2026-07-28 protocol error codes (`HeaderMismatch`, `MissingRequiredClientCapability`, `UnsupportedProtocolVersion`) were renumbered upstream from `-32001`/`-32003`/`-32004` to `-32020`/`-32021`/`-32022` between v2 alpha builds. These codes have only +> ever appeared on the draft 2026-07-28 wire, so there is **no v1.x → v2 migration impact** — but code written against an earlier v2 alpha that hard-coded the old numeric values must update to the new ones (or, preferably, use the exported `ProtocolErrorCode` enum members / +> `HEADER_MISMATCH_ERROR_CODE` constant). + ### Client identity accessors deprecated in favor of per-request context `Server.getClientCapabilities()`, `Server.getClientVersion()` and `Server.getNegotiatedProtocolVersion()` are deprecated (they remain functional). On 2026-07-28 requests the client's identity travels with each request in the validated `_meta` envelope and is available to handlers diff --git a/docs/server.md b/docs/server.md index e9e6ba4c4..47d866aa6 100644 --- a/docs/server.md +++ b/docs/server.md @@ -103,7 +103,7 @@ Tools let clients invoke actions on your server — they are usually the main wa 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 +> `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 `-32020` (`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" diff --git a/packages/client/src/client/client.ts b/packages/client/src/client/client.ts index 65e91740e..a2c8ded79 100644 --- a/packages/client/src/client/client.ts +++ b/packages/client/src/client/client.ts @@ -198,7 +198,7 @@ export type ClientOptions = ProtocolOptions & { * Probe policy lives under `probe: { timeoutMs?, maxRetries? }`; the probe * inherits the client's standard request timeout unless overridden, and * `maxRetries` (default `0`) governs timeout re-sends only — the - * spec-mandated `-32004` corrective continuation is never counted against it. + * spec-mandated `-32022` corrective continuation is never counted against it. * * Once a modern era is negotiated, the client automatically attaches the * per-request `_meta` envelope (the reserved protocol-version / client-info / @@ -366,7 +366,7 @@ export class Client extends Protocol { * keyed by tool name. Replaced on every fresh (cursor-less) list and * accumulated across pages of a paginated list; cleared on reconnect. * Freshness is NOT enforced: a stale schema is recovered through the - * `-32001` → refresh-and-retry path in {@linkcode callTool}. Only + * `-32020` → refresh-and-retry path in {@linkcode callTool}. Only * consumed on a modern connection. */ private _cachedToolDefinitions: Map = new Map(); @@ -1723,7 +1723,7 @@ export class Client extends Protocol { try { result = await this.request({ method: 'tools/call', params }, buildSendOptions()); } catch (error) { - // SEP-2243 one-refresh-on-miss: a `-32001` (HeaderMismatch) + // SEP-2243 one-refresh-on-miss: a `-32020` (HeaderMismatch) // rejection on a modern connection means the server enforced an // `Mcp-Param-*` header we did not (or could not) send. Refresh the // tool-definition cache once and retry with the now-known schema — @@ -1809,7 +1809,7 @@ export class Client extends Protocol { * `toolDefinition` escape hatch wins; otherwise the most recent * `tools/list` result is used as-is. Freshness is NOT enforced — the * cached schema is the best information available regardless of age, and - * a stale schema is recovered through the `-32001` → refresh-and-retry + * a stale schema is recovered through the `-32020` → refresh-and-retry * path in {@linkcode callTool}. On a miss the call proceeds without * `Mcp-Param-*` headers (the spec's "client SHOULD send without custom * headers" guidance) and relies on the same refresh-and-retry recovery. @@ -1826,7 +1826,7 @@ export class Client extends Protocol { * `_cachedToolDefinitions` is rebuilt completely. Only the caller's * `signal`/`timeout` are forwarded to each page request — `tools/call` * options like `toolDefinition`, `headers`, or `onprogress` do not apply - * to `tools/list`. The `-32001` retry path + * to `tools/list`. The `-32020` retry path * in {@linkcode callTool} uses this rather than a bare cursor-less * `listTools()`, which would only fetch page 1 and (because page 1 clears * the merged cache) drop the page-≥2 scans the application accumulated via diff --git a/packages/client/src/client/probeClassifier.ts b/packages/client/src/client/probeClassifier.ts index 25dd025a3..0c164fdc9 100644 --- a/packages/client/src/client/probeClassifier.ts +++ b/packages/client/src/client/probeClassifier.ts @@ -1,7 +1,7 @@ /** * Probe outcome classifier (pure module): maps the outcome of the connect-time * `server/discover` probe onto one of four verdicts — modern era, the - * spec-mandated `-32004` corrective continuation, legacy fallback (the plain + * spec-mandated `-32022` corrective continuation, legacy fallback (the plain * 2025 `initialize` handshake on the same connection), or a typed connect error. * * The classifier is deliberately conservative: anything it does not positively @@ -57,7 +57,7 @@ export interface ProbeClassifierContext { * Whether a legacy `initialize` fallback is possible — `false` for a * modern-only client and for `pin` mode. Without a fallback, rows carrying * modern evidence but no usable version overlap — a `DiscoverResult` with - * no overlapping version, or a `-32004` whose `data.supported` lists only + * no overlapping version, or a `-32022` whose `data.supported` lists only * legacy revisions — yield a typed `UnsupportedProtocolVersionError` built * from that evidence; the remaining rows that would have fallen back still * classify as `legacy`, and the caller reports them as a typed negotiation @@ -74,7 +74,7 @@ export type ProbeVerdict = /** Definitive modern evidence: select `version` and continue without `initialize`. */ | { kind: 'modern'; version: string; discover: DiscoverResult } /** - * `-32004` with a mutual modern version: re-send the probe at `version`. + * `-32022` with a mutual modern version: re-send the probe at `version`. * Spec-mandated select-and-continue — the caller runs it exactly once and * arms a loop guard on the second rejection, throwing `error`. */ @@ -84,14 +84,16 @@ export type ProbeVerdict = /** Typed connect error — never converted to an era verdict. */ | { kind: 'error'; error: Error }; -/** The `-32004` UnsupportedProtocolVersion protocol error code (negotiation-phase recognition). */ -const UNSUPPORTED_PROTOCOL_VERSION = -32_004; +/** The `-32022` UnsupportedProtocolVersion protocol error code (negotiation-phase recognition). */ +const UNSUPPORTED_PROTOCOL_VERSION = -32_022; /** * Deliberately not probe-recognized in either direction: deployed servers - * overload `-32001` and the error-code ladder for these cells is still being - * derived upstream, so both fall into the conservative legacy default. + * overload `-32001` (the SDK-conventional `Session not found` body on a 2025 + * stateful server), and the spec-assigned `-32020` (`HeaderMismatch`) / + * `-32021` (`MissingRequiredClientCapability`) are not era evidence — all + * fall into the conservative legacy default. */ -const NOT_PROBE_RECOGNIZED = new Set([-32_001, -32_003]); +const NOT_PROBE_RECOGNIZED = new Set([-32_001, -32_020, -32_021]); /** * Classify a single probe outcome. Pure: no I/O, no state — loop-guard and @@ -159,7 +161,7 @@ function classifyRpcError(outcome: { code: number; message: string; data?: unkno if (code === UNSUPPORTED_PROTOCOL_VERSION) { const supported = parseSupportedList(data); if (supported === undefined) { - // -32004 without a valid data.supported list is not actionable modern evidence. + // -32022 without a valid data.supported list is not actionable modern evidence. return { kind: 'legacy' }; } const requested = parseRequested(data) ?? context.requestedVersion; diff --git a/packages/client/src/client/versionNegotiation.ts b/packages/client/src/client/versionNegotiation.ts index 710b4d399..2e587f9ca 100644 --- a/packages/client/src/client/versionNegotiation.ts +++ b/packages/client/src/client/versionNegotiation.ts @@ -53,7 +53,7 @@ export interface VersionNegotiationProbeOptions { /** * Number of times to re-send the probe after a timeout before reaching the * timeout verdict. Governs timeout re-sends only — the spec-mandated - * `-32004` corrective continuation (select-and-continue with a mutual + * `-32022` corrective continuation (select-and-continue with a mutual * version) is a separate negotiation step and is never counted against * `maxRetries`. * @@ -342,7 +342,7 @@ export async function negotiateEra( const probe = async (): Promise => { let requestedVersion = clientModernVersions[0]!; - // The -32004 corrective continuation runs exactly once (even when the + // The -32022 corrective continuation runs exactly once (even when the // mutual version equals the just-rejected one); the loop guard arms on // the second rejection. let correctiveUsed = false; diff --git a/packages/client/test/client/mcpParamMirroring.test.ts b/packages/client/test/client/mcpParamMirroring.test.ts index e2970a1c5..ac32b400e 100644 --- a/packages/client/test/client/mcpParamMirroring.test.ts +++ b/packages/client/test/client/mcpParamMirroring.test.ts @@ -5,7 +5,7 @@ * `Mcp-Param-*` header construction from cached definitions and the * `toolDefinition` escape hatch; era-parity (legacy `callTool` byte-untouched); * stdio MAY-ignore (no headers on a single-channel transport); the - * one-refresh-on-`-32001` retry. + * one-refresh-on-`-32020` retry. */ import type { JSONRPCMessage, JSONRPCRequest, Tool, TransportSendOptions } from '@modelcontextprotocol/core'; import { encodeMcpParamValue, HEADER_MISMATCH_ERROR_CODE, InMemoryTransport, PROTOCOL_VERSION_META_KEY } from '@modelcontextprotocol/core'; @@ -134,13 +134,13 @@ describe('SEP-2243 Mcp-Param-* mirroring (modern era)', () => { expect(callHeaders[0]).toEqual({ 'Mcp-Param-Region': 'eu' }); }); - it('callTool() refreshes once and retries on a -32001 (HeaderMismatch) rejection', async () => { + it('callTool() refreshes once and retries on a -32020 (HeaderMismatch) rejection', async () => { const { clientTx, callHeaders, listCount } = await scriptedModernServer([REGION_TOOL], /* rejectFirstCall */ true); const client = modernClient(); await client.connect(clientTx); // No prior listTools — first send carries no param headers, server - // rejects -32001, client refreshes and retries with the headers. + // rejects -32020, client refreshes and retries with the headers. const result = await client.callTool({ name: 'route', arguments: { region: 'ap' } }); expect(result.content?.[0]).toEqual({ type: 'text', text: 'ok' }); expect(listCount()).toBe(1); @@ -202,9 +202,9 @@ describe('SEP-2243 Mcp-Param-* mirroring (modern era)', () => { expect(callHeaders[0]).toEqual({ 'Mcp-Param-Region': 'us-west1' }); }); - it('-32001 recovery refresh paginates: a page-2 x-mcp-header tool is recovered and page-2 scans are not wiped', async () => { + it('-32020 recovery refresh paginates: a page-2 x-mcp-header tool is recovered and page-2 scans are not wiped', async () => { // Page 1: `echo` (no declarations). Page 2: `route` (x-mcp-header on - // page ≥ 2). The first call is rejected -32001; the internal refresh + // page ≥ 2). The first call is rejected -32020; the internal refresh // must walk BOTH pages so the retry carries `Mcp-Param-Region` and the // application's page-2 scan is not lost. const PAGE1_TOOL: Tool = { name: 'echo', inputSchema: { type: 'object', properties: {} } }; diff --git a/packages/client/test/client/probeClassifier.test.ts b/packages/client/test/client/probeClassifier.test.ts index f442f65fb..2991acd3d 100644 --- a/packages/client/test/client/probeClassifier.test.ts +++ b/packages/client/test/client/probeClassifier.test.ts @@ -80,22 +80,22 @@ describe('row: DiscoverResult with NO overlap → initialize on the same connect }); }); -describe('row: -32004 + valid data.supported with a mutual modern version → select-and-continue, MUST NOT fall back', () => { - test('in-band -32004 yields a corrective verdict (never legacy)', () => { +describe('row: -32022 + valid data.supported with a mutual modern version → select-and-continue, MUST NOT fall back', () => { + test('in-band -32022 yields a corrective verdict (never legacy)', () => { const verdict = classify({ kind: 'rpc-error', - code: -32_004, + code: -32_022, message: 'Unsupported protocol version', data: { supported: [MODERN], requested: '2027-01-01' } }); expect(verdict).toMatchObject({ kind: 'corrective', version: MODERN }); }); - test('HTTP 400-bodied -32004 yields the same corrective verdict', () => { + test('HTTP 400-bodied -32022 yields the same corrective verdict', () => { const verdict = classify({ kind: 'http-error', status: 400, - body: httpErrorBody(-32_004, 'Unsupported protocol version', { supported: [MODERN], requested: MODERN }) + body: httpErrorBody(-32_022, 'Unsupported protocol version', { supported: [MODERN], requested: MODERN }) }); expect(verdict).toMatchObject({ kind: 'corrective', version: MODERN }); }); @@ -103,7 +103,7 @@ describe('row: -32004 + valid data.supported with a mutual modern version → se test('corrective even when the mutual version equals the just-rejected one (T2/A6 — caller runs it exactly once)', () => { const verdict = classify({ kind: 'rpc-error', - code: -32_004, + code: -32_022, message: 'Unsupported protocol version', data: { supported: [MODERN], requested: MODERN } }); @@ -114,11 +114,11 @@ describe('row: -32004 + valid data.supported with a mutual modern version → se }); }); -describe('row: -32004 with a disjoint-but-modern list → typed error, never initialize', () => { +describe('row: -32022 with a disjoint-but-modern list → typed error, never initialize', () => { test('no mutual modern version but the list is modern', () => { const verdict = classify({ kind: 'rpc-error', - code: -32_004, + code: -32_022, message: 'Unsupported protocol version', data: { supported: ['2027-12-31'], requested: MODERN } }); @@ -130,11 +130,11 @@ describe('row: -32004 with a disjoint-but-modern list → typed error, never ini }); }); -describe('row: -32004 with a legacy-only list → initialize; modern-only client → typed error carrying data.supported', () => { +describe('row: -32022 with a legacy-only list → initialize; modern-only client → typed error carrying data.supported', () => { test('legacy-only list with fallback available → legacy', () => { const verdict = classify({ kind: 'rpc-error', - code: -32_004, + code: -32_022, message: 'Unsupported protocol version', data: { supported: [LEGACY, '2025-06-18'] } }); @@ -143,7 +143,7 @@ describe('row: -32004 with a legacy-only list → initialize; modern-only client test('legacy-only list, modern-only client → typed error carrying data.supported', () => { const verdict = classify( - { kind: 'rpc-error', code: -32_004, message: 'Unsupported protocol version', data: { supported: [LEGACY] } }, + { kind: 'rpc-error', code: -32_022, message: 'Unsupported protocol version', data: { supported: [LEGACY] } }, { fallbackAvailable: false } ); expect(verdict.kind).toBe('error'); @@ -153,11 +153,11 @@ describe('row: -32004 with a legacy-only list → initialize; modern-only client } }); - test('-32004 with malformed data (no valid supported list) → conservative legacy', () => { - expect(classify({ kind: 'rpc-error', code: -32_004, message: 'nope', data: { supported: 'not-a-list' } })).toEqual({ + test('-32022 with malformed data (no valid supported list) → conservative legacy', () => { + expect(classify({ kind: 'rpc-error', code: -32_022, message: 'nope', data: { supported: 'not-a-list' } })).toEqual({ kind: 'legacy' }); - expect(classify({ kind: 'rpc-error', code: -32_004, message: 'nope' })).toEqual({ kind: 'legacy' }); + expect(classify({ kind: 'rpc-error', code: -32_022, message: 'nope' })).toEqual({ kind: 'legacy' }); }); }); @@ -233,16 +233,23 @@ describe('row: plain-text/unparseable 400, code 0, empty body, 406, any unrecogn }); }); -describe('row: -32001 / -32003 are NEVER probe-recognized → fall into unrecognized → legacy', () => { - test('-32001 (session-404 overload on deployed servers; the spec-assigned HeaderMismatch code is still never probe evidence)', () => { +describe('row: -32001 / -32020 / -32021 are NEVER probe-recognized → fall into unrecognized → legacy', () => { + test('-32001 (session-404 overload on deployed servers — the SDK-conventional code, never probe evidence)', () => { expect(classify({ kind: 'rpc-error', code: -32_001, message: 'Session not found' })).toEqual({ kind: 'legacy' }); expect(classify({ kind: 'http-error', status: 404, body: httpErrorBody(-32_001, 'Session not found') })).toEqual({ kind: 'legacy' }); }); - test('-32003 with data is NOT modern evidence', () => { - expect(classify({ kind: 'rpc-error', code: -32_003, message: 'Capability required', data: { capability: 'sampling' } })).toEqual({ + test('-32020 (the spec-assigned HeaderMismatch code is still never probe evidence)', () => { + expect(classify({ kind: 'rpc-error', code: -32_020, message: 'Header mismatch' })).toEqual({ kind: 'legacy' }); + expect(classify({ kind: 'http-error', status: 400, body: httpErrorBody(-32_020, 'Header mismatch') })).toEqual({ + kind: 'legacy' + }); + }); + + test('-32021 with data is NOT modern evidence', () => { + expect(classify({ kind: 'rpc-error', code: -32_021, message: 'Capability required', data: { capability: 'sampling' } })).toEqual({ kind: 'legacy' }); }); diff --git a/packages/client/test/client/probeFixtureCorpus.test.ts b/packages/client/test/client/probeFixtureCorpus.test.ts index 2d4c8a16f..bb02c76fa 100644 --- a/packages/client/test/client/probeFixtureCorpus.test.ts +++ b/packages/client/test/client/probeFixtureCorpus.test.ts @@ -118,25 +118,25 @@ const CORPUS: CorpusRow[] = [ expected: 'legacy' }, { - name: 'recognizer: -32004 with a structured supported list naming a mutual modern version → corrective continuation', + name: 'recognizer: -32022 with a structured supported list naming a mutual modern version → corrective continuation', outcome: { kind: 'rpc-error', - code: -32_004, + code: -32_022, message: 'Unsupported protocol version', data: { supported: [MODERN, LATEST_PROTOCOL_VERSION], requested: '2027-01-01' } }, expected: 'corrective' }, { - name: 'recognizer: -32004 without a parsable data.supported list is not actionable modern evidence → legacy', - outcome: { kind: 'rpc-error', code: -32_004, message: 'Unsupported protocol version' }, + name: 'recognizer: -32022 without a parsable data.supported list is not actionable modern evidence → legacy', + outcome: { kind: 'rpc-error', code: -32_022, message: 'Unsupported protocol version' }, expected: 'legacy' }, { - name: 'recognizer: -32004 with a legacy-only supported list is a definitive legacy signal → legacy', + name: 'recognizer: -32022 with a legacy-only supported list is a definitive legacy signal → legacy', outcome: { kind: 'rpc-error', - code: -32_004, + code: -32_022, message: 'Unsupported protocol version', data: { supported: [LATEST_PROTOCOL_VERSION], requested: MODERN } }, diff --git a/packages/client/test/client/versionNegotiation.test.ts b/packages/client/test/client/versionNegotiation.test.ts index 9187fa141..b3f9d8df7 100644 --- a/packages/client/test/client/versionNegotiation.test.ts +++ b/packages/client/test/client/versionNegotiation.test.ts @@ -438,11 +438,11 @@ describe('probe timeout policy (transport-aware)', () => { }); /* ------------------------------------------------------------------------- * - * -32004 corrective continuation — exactly once; loop guard on second + * -32022 corrective continuation — exactly once; loop guard on second * rejection. * ------------------------------------------------------------------------- */ -describe('-32004 corrective continuation', () => { +describe('-32022 corrective continuation', () => { test('select-and-continue runs exactly once, even when the mutual version equals the just-rejected one', async () => { let discoverCalls = 0; const script: Script = (message, t) => { @@ -455,7 +455,7 @@ describe('-32004 corrective continuation', () => { jsonrpc: '2.0', id: message.id, error: { - code: -32_004, + code: -32_022, message: 'Unsupported protocol version', data: { supported: [MODERN], requested: MODERN } } @@ -486,7 +486,7 @@ describe('-32004 corrective continuation', () => { t.reply({ jsonrpc: '2.0', id: message.id, - error: { code: -32_004, message: 'Unsupported protocol version', data: { supported: [MODERN], requested: MODERN } } + error: { code: -32_022, message: 'Unsupported protocol version', data: { supported: [MODERN], requested: MODERN } } }); }; @@ -498,13 +498,13 @@ describe('-32004 corrective continuation', () => { expect(transport.sent.some(m => 'method' in m && m.method === 'initialize')).toBe(false); }); - test('-32004 with a disjoint-but-modern list: typed error, never initialize', async () => { + test('-32022 with a disjoint-but-modern list: typed error, never initialize', async () => { const script: Script = (message, t) => { if (!isJSONRPCRequest(message)) return; t.reply({ jsonrpc: '2.0', id: message.id, - error: { code: -32_004, message: 'Unsupported protocol version', data: { supported: ['2027-12-31'] } } + error: { code: -32_022, message: 'Unsupported protocol version', data: { supported: ['2027-12-31'] } } }); }; @@ -515,14 +515,14 @@ describe('-32004 corrective continuation', () => { expect(transport.sent.some(m => 'method' in m && m.method === 'initialize')).toBe(false); }); - test('-32004 with a legacy-only list: definitive legacy signal, initialize on the same connection', async () => { + test('-32022 with a legacy-only list: definitive legacy signal, initialize on the same connection', async () => { const script: Script = (message, t) => { if (!isJSONRPCRequest(message)) return; if (message.method === 'server/discover') { t.reply({ jsonrpc: '2.0', id: message.id, - error: { code: -32_004, message: 'Unsupported protocol version', data: { supported: ['2025-11-25'] } } + error: { code: -32_022, message: 'Unsupported protocol version', data: { supported: ['2025-11-25'] } } }); } else { legacyServerScript(message, t); @@ -537,13 +537,13 @@ describe('-32004 corrective continuation', () => { await client.close(); }); - test('modern-only client + legacy-only -32004 list: typed error carrying data.supported', async () => { + test('modern-only client + legacy-only -32022 list: typed error carrying data.supported', async () => { const script: Script = (message, t) => { if (!isJSONRPCRequest(message)) return; t.reply({ jsonrpc: '2.0', id: message.id, - error: { code: -32_004, message: 'Unsupported protocol version', data: { supported: ['2025-11-25'] } } + error: { code: -32_022, message: 'Unsupported protocol version', data: { supported: ['2025-11-25'] } } }); }; diff --git a/packages/core/src/shared/clientCapabilityRequirements.ts b/packages/core/src/shared/clientCapabilityRequirements.ts index 4f8fa6561..59b3d5086 100644 --- a/packages/core/src/shared/clientCapabilityRequirements.ts +++ b/packages/core/src/shared/clientCapabilityRequirements.ts @@ -6,7 +6,7 @@ * request (`io.modelcontextprotocol/clientCapabilities`), and a server MUST * NOT rely on capabilities the client did not declare: when processing a * request requires an undeclared capability, the server answers - * `MissingRequiredClientCapabilityError` (`-32003`) with + * `MissingRequiredClientCapabilityError` (`-32021`) with * `data.requiredCapabilities` listing what is missing — HTTP status `400` on * HTTP transports. * @@ -112,7 +112,7 @@ export function requiredClientCapabilitiesForInputRequest(entry: { * declare. Returns `undefined` when every required capability is declared; * otherwise returns an object in the `ClientCapabilities` shape containing * exactly the missing capabilities (suitable for - * `data.requiredCapabilities` on the `-32003` error). + * `data.requiredCapabilities` on the `-32021` error). * * A capability counts as declared when its top-level key is present on the * declared capabilities; when the requirement names nested members (for diff --git a/packages/core/src/shared/inboundClassification.ts b/packages/core/src/shared/inboundClassification.ts index 63321bbca..f06675373 100644 --- a/packages/core/src/shared/inboundClassification.ts +++ b/packages/core/src/shared/inboundClassification.ts @@ -31,7 +31,7 @@ * posture, not a spec requirement: the spec leaves header rules for posted * notifications undefined (core client notifications do not occur over * Streamable HTTP); applying the request rules symmetrically is what an - * ecosystem custom-notification POST expects, and the −32001 cells stay + * ecosystem custom-notification POST expects, and the −32020 cells stay * passing for them. * - `GET`/`DELETE` (and any other non-`POST` method) are body-less 2025-era * session operations: the modern era is `POST`-only, so they are routed to @@ -52,7 +52,7 @@ * * - A header/body cross-check mismatch (the `MCP-Protocol-Version` header * disagreeing with the body, or the `Mcp-Method` header disagreeing with the - * body method) is rejected with `-32001` (`HeaderMismatch`) on HTTP 400. + * body method) is rejected with `-32020` (`HeaderMismatch`) on HTTP 400. * - A request whose protocol-version header names a modern revision but whose * body carries no `_meta` envelope claim — including an envelope present but * missing the required protocol-version key — is rejected with `-32602` @@ -208,13 +208,13 @@ export type InboundClassificationOutcome = InboundLegacyRoute | InboundModernRou * with the body's classification), and the `Mcp-Method` header disagreeing * with the body method. * - * `-32001` is the draft schema's `HEADER_MISMATCH` constant (the SEP-2243 + * `-32020` is the draft schema's `HEADER_MISMATCH` constant (the SEP-2243 * `HeaderMismatch` code; the spec requires HTTP 400 for it), as also asserted * by the published conformance suite for header-validation failures. It has no * {@linkcode ProtocolErrorCode} member because it is not part of the 2025-era * wire vocabulary; the validation ladder is its only emitter. */ -export const HEADER_MISMATCH_ERROR_CODE = -32_001; +export const HEADER_MISMATCH_ERROR_CODE = -32_020; /* ------------------------------------------------------------------------ * * The validation ladder as data @@ -281,7 +281,7 @@ export const INBOUND_VALIDATION_LADDER: readonly InboundValidationRungDescriptor conformance: ['server-stateless', 'http-header-validation', 'http-custom-header-server-validation'], rationale: 'Body-primary era classification with the protocol-version header as a cross-check; a header/body disagreement is rejected ' + - 'with -32001 (HeaderMismatch), and an envelope-less request on a modern-only endpoint is answered with the ' + + 'with -32020 (HeaderMismatch), and an envelope-less request on a modern-only endpoint is answered with the ' + 'unsupported-protocol-version error naming the supported revisions.' }, { @@ -351,7 +351,7 @@ export const INBOUND_VALIDATION_LADDER: readonly InboundValidationRungDescriptor rationale: 'SEP-2243 `Mcp-Param-*` headers are validated against the named tool’s `x-mcp-header` declarations and the body ' + '`arguments` after the tool registry is known and before dispatch reaches the handler; a missing/disagreeing/malformed ' + - 'header is rejected 400 / -32001 with the same shape as the standard-header cross-checks.' + 'header is rejected 400 / -32020 with the same shape as the standard-header cross-checks.' } ]; @@ -449,7 +449,7 @@ export const MCP_NAME_HEADER_SOURCE: Readonly> = * entry on a modern-classified request immediately after * {@linkcode classifyInboundRequest} returns a modern route. * - * Returns the `-32001` (`HeaderMismatch`) ladder rejection (HTTP `400`, + * Returns the `-32020` (`HeaderMismatch`) ladder rejection (HTTP `400`, * `standard-header-validation` rung — the same shape * {@linkcode classifyInboundRequest} already emits on the edge * `era-classification` rung for the `MCP-Protocol-Version` and diff --git a/packages/core/src/shared/mcpParamHeaders.ts b/packages/core/src/shared/mcpParamHeaders.ts index 9e60ec5be..9931f0770 100644 --- a/packages/core/src/shared/mcpParamHeaders.ts +++ b/packages/core/src/shared/mcpParamHeaders.ts @@ -10,13 +10,13 @@ * * The standard-header half (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`) * lives with the inbound classifier — this module is the custom-header half - * only, and it consumes the same `-32001` (`HeaderMismatch`) emission shape the + * only, and it consumes the same `-32020` (`HeaderMismatch`) emission shape the * classifier established for header/body cross-check failures. * * Spec text at the implementation's spec pin: * - draft/basic/transports/streamable-http.mdx § "Custom Headers from Tool Parameters" * (constraints, value encoding, the 5-step client algorithm, the - * server-behavior table, the `400` + `-32001` rejection) + * server-behavior table, the `400` + `-32020` rejection) * - draft/server/tools.mdx § "x-mcp-header" (the schema-extension property and * its constraints) */ @@ -272,7 +272,7 @@ export function buildMcpParamHeaders( * `42.0` and `42` are equal); everything else is compared as decoded strings. * * Returns `undefined` when every check passes, or an - * {@linkcode InboundLadderRejection} carrying the same `-32001` + * {@linkcode InboundLadderRejection} carrying the same `-32020` * (`HeaderMismatch`) shape the inbound classifier emits for the * standard-header cross-checks — `400 Bad Request` with the disagreeing pair * in `data.mismatch`. @@ -336,7 +336,7 @@ export function validateMcpParamHeaders( } /** - * Build the `-32001` (`HeaderMismatch`) rejection for an `Mcp-Param-*` + * Build the `-32020` (`HeaderMismatch`) rejection for an `Mcp-Param-*` * disagreement. Same shape as the inbound classifier's standard-header * cross-check mismatch (HTTP `400`, `data.mismatch` naming the disagreeing * pair, `settled: true`); only the rung differs because this check runs at the diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index a8e96bb8a..57ba650ac 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -887,7 +887,7 @@ export abstract class Protocol { // Edge→instance handoff check: a classification that disagrees with // the instance era means the entry routed another era's traffic onto // this instance. That is a routing error: answer with the typed era - // error (−32004 Unsupported protocol version) and surface it out of + // error (−32022 Unsupported protocol version) and surface it out of // band — never serve the request on a guessed era. if (extra?.classification !== undefined) { const classified = classifiedWireEra(extra.classification); diff --git a/packages/core/src/types/enums.ts b/packages/core/src/types/enums.ts index af7314ed3..be4649e8f 100644 --- a/packages/core/src/types/enums.ts +++ b/packages/core/src/types/enums.ts @@ -27,11 +27,11 @@ export enum ProtocolErrorCode { * Processing the request requires a capability the client did not declare * in the request's `clientCapabilities` (protocol revision 2026-07-28). */ - MissingRequiredClientCapability = -32_003, + MissingRequiredClientCapability = -32_021, /** * The request's protocol version is unknown to the server or unsupported * by it (protocol revision 2026-07-28). */ - UnsupportedProtocolVersion = -32_004, + UnsupportedProtocolVersion = -32_022, UrlElicitationRequired = -32_042 } diff --git a/packages/core/src/types/errors.ts b/packages/core/src/types/errors.ts index b30c38657..468b3269e 100644 --- a/packages/core/src/types/errors.ts +++ b/packages/core/src/types/errors.ts @@ -112,7 +112,7 @@ export class UrlElicitationRequiredError extends ProtocolError { } /** - * Error type for the `-32004` UnsupportedProtocolVersion protocol error (protocol + * Error type for the `-32022` UnsupportedProtocolVersion protocol error (protocol * revision 2026-07-28): the request's protocol version is unknown to the server or * unsupported by it. * @@ -141,7 +141,7 @@ export class UnsupportedProtocolVersionError extends ProtocolError { } /** - * Error type for the `-32003` MissingRequiredClientCapability protocol error + * Error type for the `-32021` MissingRequiredClientCapability protocol error * (protocol revision 2026-07-28): processing the request requires a capability * the client did not declare in the request's `clientCapabilities`. * diff --git a/packages/core/src/types/spec.types.2026-07-28.ts b/packages/core/src/types/spec.types.2026-07-28.ts index bf5ad3e09..198e32a39 100644 --- a/packages/core/src/types/spec.types.2026-07-28.ts +++ b/packages/core/src/types/spec.types.2026-07-28.ts @@ -321,7 +321,7 @@ export interface InvalidRequestError extends Error { * * In MCP, a server returns this error when a client invokes a method the server does not implement — either a genuinely unknown method, or one gated behind a server capability the server did not advertise (e.g., calling `prompts/list` when the `prompts` capability was not advertised). * - * A request that requires a client capability the client did not declare is signalled instead by {@link MissingRequiredClientCapabilityError} (`-32003`). + * A request that requires a client capability the client did not declare is signalled instead by {@link MissingRequiredClientCapabilityError} (`-32021`). * * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} * @@ -387,7 +387,7 @@ export interface InternalError extends Error { * * @category Errors */ -export const HEADER_MISMATCH = -32001; +export const HEADER_MISMATCH = -32020; /** * Error code returned when a server requires a client capability that was @@ -395,7 +395,7 @@ export const HEADER_MISMATCH = -32001; * * @category Errors */ -export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32003; +export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32021; /** * Error code returned when the request's protocol version is not supported @@ -403,7 +403,7 @@ export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32003; * * @category Errors */ -export const UNSUPPORTED_PROTOCOL_VERSION = -32004; +export const UNSUPPORTED_PROTOCOL_VERSION = -32022; /** * Returned when a server rejects a request because the values in the HTTP diff --git a/packages/core/src/types/types.ts b/packages/core/src/types/types.ts index 6e9fcaf85..f4a045187 100644 --- a/packages/core/src/types/types.ts +++ b/packages/core/src/types/types.ts @@ -753,7 +753,7 @@ export interface InternalError extends JSONRPCErrorObject { } /** - * Data carried by a `-32003` MissingRequiredClientCapability protocol error + * Data carried by a `-32021` MissingRequiredClientCapability protocol error * (protocol revision 2026-07-28). */ export interface MissingRequiredClientCapabilityErrorData { @@ -766,7 +766,7 @@ export interface MissingRequiredClientCapabilityErrorData { } /** - * Data carried by a `-32004` UnsupportedProtocolVersion protocol error + * Data carried by a `-32022` UnsupportedProtocolVersion protocol error * (protocol revision 2026-07-28). */ export interface UnsupportedProtocolVersionErrorData { diff --git a/packages/core/test/shared/clientCapabilityRequirements.test.ts b/packages/core/test/shared/clientCapabilityRequirements.test.ts index 80758d391..ff25edc6e 100644 --- a/packages/core/test/shared/clientCapabilityRequirements.test.ts +++ b/packages/core/test/shared/clientCapabilityRequirements.test.ts @@ -1,5 +1,5 @@ /** - * The shared client-capability requirement helpers behind the `-32003` + * The shared client-capability requirement helpers behind the `-32021` * MissingRequiredClientCapability rule (protocol revision 2026-07-28). * * `missingClientCapabilities` is the single helper shared by the pre-dispatch diff --git a/packages/core/test/shared/errorHttpStatusMatrix.test.ts b/packages/core/test/shared/errorHttpStatusMatrix.test.ts index 7f505daec..fb5344a1b 100644 --- a/packages/core/test/shared/errorHttpStatusMatrix.test.ts +++ b/packages/core/test/shared/errorHttpStatusMatrix.test.ts @@ -13,7 +13,7 @@ * carries its own HTTP 400 and is the only invalid-params rejection that * maps to 400. * - * The header/body mismatch family is pinned to `-32001` (HeaderMismatch) and + * The header/body mismatch family is pinned to `-32020` (HeaderMismatch) and * the missing-envelope cells to `-32602`, the assignments asserted by the * published conformance suite. * @@ -35,7 +35,7 @@ describe('the status matrix — pinned cells', () => { }, { code: ProtocolErrorCode.UnsupportedProtocolVersion, status: 400, cell: 'unsupported protocol version' }, { code: ProtocolErrorCode.MissingRequiredClientCapability, status: 400, cell: 'missing required client capability' }, - { code: -32_001, status: 400, cell: 'header mismatch family (when emitted by the ladder)' }, + { code: -32_020, status: 400, cell: 'header mismatch family (when emitted by the ladder)' }, { code: ProtocolErrorCode.ParseError, status: 400, cell: 'unparseable request body' }, { code: ProtocolErrorCode.InvalidRequest, status: 400, cell: 'malformed JSON-RPC body / rejected batch' } ]; @@ -76,13 +76,13 @@ describe('the status matrix — pinned cells', () => { Object.keys(LADDER_ERROR_HTTP_STATUS) .map(Number) .sort((a, b) => a - b) - ).toEqual([-32_700, -32_601, -32_600, -32_004, -32_003, -32_001].sort((a, b) => a - b)); + ).toEqual([-32_700, -32_601, -32_600, -32_022, -32_021, -32_020].sort((a, b) => a - b)); }); }); describe('the status matrix — header/body mismatch family', () => { - test('the header/body mismatch family is pinned to -32001 (HeaderMismatch) and maps to HTTP 400', () => { - expect(HEADER_MISMATCH_ERROR_CODE).toBe(-32_001); + test('the header/body mismatch family is pinned to -32020 (HeaderMismatch) and maps to HTTP 400', () => { + expect(HEADER_MISMATCH_ERROR_CODE).toBe(-32_020); expect(LADDER_ERROR_HTTP_STATUS[HEADER_MISMATCH_ERROR_CODE]).toBe(400); expect(httpStatusForErrorCode(HEADER_MISMATCH_ERROR_CODE, 'ladder')).toBe(400); }); diff --git a/packages/core/test/shared/inboundClassification.test.ts b/packages/core/test/shared/inboundClassification.test.ts index d288fd21d..5108b8e6c 100644 --- a/packages/core/test/shared/inboundClassification.test.ts +++ b/packages/core/test/shared/inboundClassification.test.ts @@ -5,7 +5,7 @@ * cross-checks, notification routing, element-wise batch classification, and * the modern-only (strict) rejection mapping. * - * The header/body mismatch cells are pinned to `-32001` (HeaderMismatch) and + * The header/body mismatch cells are pinned to `-32020` (HeaderMismatch) and * the missing-envelope / missing-protocol-version cells to `-32602` (invalid * params naming the missing key(s)) — the assignments asserted by the * published conformance suite. @@ -61,9 +61,9 @@ const expectMismatch = (outcome: ReturnType, cell expect(outcome.rung).toBe('era-classification'); expect(outcome.httpStatus).toBe(400); // Pinned: a header/body disagreement is a header-validation failure and - // answers -32001 (HeaderMismatch), per the published conformance suite. + // answers -32020 (HeaderMismatch), per the published conformance suite. expect(outcome.settled).toBe(true); - expect(outcome.code).toBe(-32_001); + expect(outcome.code).toBe(-32_020); }; describe('envelope claim detection (claim = the reserved protocol-version key)', () => { @@ -211,7 +211,7 @@ describe('body-primary era predicate', () => { }); }); -describe('header cross-checks (-32001 HeaderMismatch) and the missing-envelope rejection (-32602)', () => { +describe('header cross-checks (-32020 HeaderMismatch) and the missing-envelope rejection (-32602)', () => { test('a body claim disagreeing with the protocol-version header is a mismatch outcome', () => { const outcome = classifyInboundRequest(post(modernToolsCall(), { protocolVersion: '2025-06-18' })); expectMismatch(outcome, 'header-body-version-mismatch'); @@ -426,7 +426,7 @@ describe('modern-only (strict) rejection mapping', () => { expect(rejectionOutcome).toMatchObject({ cell: 'modern-only-missing-envelope', httpStatus: 400, - code: -32_004, + code: -32_022, settled: true, data: { supported: SUPPORTED } }); @@ -437,12 +437,12 @@ describe('modern-only (strict) rejection mapping', () => { test('an envelope-less initialize names the version it requested', () => { const rejectionOutcome = modernOnlyStrictRejection(legacyRoute(initializeRequest('2025-06-18')), SUPPORTED); - expect(rejectionOutcome).toMatchObject({ code: -32_004, data: { supported: SUPPORTED, requested: '2025-06-18' } }); + expect(rejectionOutcome).toMatchObject({ code: -32_022, data: { supported: SUPPORTED, requested: '2025-06-18' } }); }); test('an envelope-less request echoes the protocol-version header it sent', () => { const rejectionOutcome = modernOnlyStrictRejection(legacyRoute(legacyToolsList(), { protocolVersion: '2025-03-26' }), SUPPORTED); - expect(rejectionOutcome).toMatchObject({ code: -32_004, data: { requested: '2025-03-26' } }); + expect(rejectionOutcome).toMatchObject({ code: -32_022, data: { requested: '2025-03-26' } }); }); test('batch and response POSTs are invalid requests on a modern-only endpoint', () => { diff --git a/packages/core/test/shared/inboundLadderCellSheet.test.ts b/packages/core/test/shared/inboundLadderCellSheet.test.ts index 6713e3bd4..87c28bec3 100644 --- a/packages/core/test/shared/inboundLadderCellSheet.test.ts +++ b/packages/core/test/shared/inboundLadderCellSheet.test.ts @@ -6,7 +6,7 @@ * status. The header/body mismatch and missing-envelope cells were originally * parameterized (asserted as candidate-set membership) while their error codes * were under discussion upstream; they are now pinned to the assignments the - * published conformance suite asserts (`-32001` HeaderMismatch for header/body + * published conformance suite asserts (`-32020` HeaderMismatch for header/body * disagreements, `-32602` invalid params naming the missing key(s) for a * missing envelope or missing protocol-version key). If a future published * conformance release changes an assignment, the affected rows are re-derived @@ -203,7 +203,7 @@ const SHEET: readonly SheetRow[] = [ conformance: ['server-stateless'], input: post(bare('tools/list')), strict: true, - reject: { rung: 'era-classification', httpStatus: 400, code: -32_004, settled: true }, + reject: { rung: 'era-classification', httpStatus: 400, code: -32_022, settled: true }, rationale: 'A modern-only endpoint answers envelope-less requests with the unsupported-protocol-version error and its supported list. ' + 'This cell shares its numeric code with the disputed mismatch family but is itself settled.' @@ -216,7 +216,7 @@ const SHEET: readonly SheetRow[] = [ reject: { rung: 'era-classification', httpStatus: 400, - code: -32_004, + code: -32_022, settled: true, data: { supported: [MODERN_REVISION], requested: '2025-06-18' } }, @@ -254,9 +254,9 @@ const SHEET: readonly SheetRow[] = [ cell: 'header-body-version-mismatch', conformance: ['server-stateless', 'http-header-validation', 'http-custom-header-server-validation'], input: post(enveloped('tools/call', { name: 'echo', arguments: {} }), { protocolVersion: '2025-06-18' }), - reject: { rung: 'era-classification', httpStatus: 400, code: -32_001, settled: true }, + reject: { rung: 'era-classification', httpStatus: 400, code: -32_020, settled: true }, rationale: - 'Header/body protocol-version disagreement is a header-validation failure: -32001 (HeaderMismatch) on HTTP 400, as ' + + 'Header/body protocol-version disagreement is a header-validation failure: -32020 (HeaderMismatch) on HTTP 400, as ' + 'asserted by the published conformance suite.' }, { @@ -274,10 +274,10 @@ const SHEET: readonly SheetRow[] = [ input: post(bare('initialize', { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'c', version: '1' } }), { protocolVersion: MODERN_REVISION }), - reject: { rung: 'era-classification', httpStatus: 400, code: -32_001, settled: true }, + reject: { rung: 'era-classification', httpStatus: 400, code: -32_020, settled: true }, rationale: 'An envelope-less initialize classifies legacy; a modern header on it is a header/body disagreement and answers the ' + - 'same -32001 (HeaderMismatch) as the rest of the mismatch family.' + 'same -32020 (HeaderMismatch) as the rest of the mismatch family.' }, { cell: 'method-header-mismatch', @@ -286,10 +286,10 @@ const SHEET: readonly SheetRow[] = [ protocolVersion: MODERN_REVISION, mcpMethod: 'tools/list' }), - reject: { rung: 'era-classification', httpStatus: 400, code: -32_001, settled: true }, + reject: { rung: 'era-classification', httpStatus: 400, code: -32_020, settled: true }, rationale: 'The Mcp-Method header must describe the body it accompanies; a disagreement is a header-validation failure and ' + - 'answers -32001 (HeaderMismatch) on HTTP 400.' + 'answers -32020 (HeaderMismatch) on HTTP 400.' }, { cell: 'notification-header-body-version-mismatch', @@ -298,10 +298,10 @@ const SHEET: readonly SheetRow[] = [ { jsonrpc: '2.0', method: 'notifications/progress', params: { _meta: { [PROTOCOL_VERSION_META_KEY]: MODERN_REVISION } } }, { protocolVersion: '2025-06-18' } ), - reject: { rung: 'era-classification', httpStatus: 400, code: -32_001, settled: true }, + reject: { rung: 'era-classification', httpStatus: 400, code: -32_020, settled: true }, rationale: 'A notification body claim disagreeing with the protocol-version header is the same header-validation failure as the ' + - 'request cells above and answers the same -32001 (HeaderMismatch).' + 'request cells above and answers the same -32020 (HeaderMismatch).' }, { cell: 'notification-method-header-mismatch', @@ -310,10 +310,10 @@ const SHEET: readonly SheetRow[] = [ { jsonrpc: '2.0', method: 'notifications/progress', params: { progressToken: 1, progress: 1 } }, { protocolVersion: MODERN_REVISION, mcpMethod: 'notifications/cancelled' } ), - reject: { rung: 'era-classification', httpStatus: 400, code: -32_001, settled: true }, + reject: { rung: 'era-classification', httpStatus: 400, code: -32_020, settled: true }, rationale: 'The Mcp-Method header must describe the notification body it accompanies (validated only when the notification ' + - 'classifies modern); a disagreement answers -32001 (HeaderMismatch).' + 'classifies modern); a disagreement answers -32020 (HeaderMismatch).' }, { cell: 'multi-fault-mismatched-claim-and-malformed-envelope', @@ -407,9 +407,9 @@ describe('HTTP status mapping for ladder-originated errors (origin-keyed)', () = [-32_700]: 400, [-32_601]: 404, [-32_600]: 400, - [-32_004]: 400, - [-32_003]: 400, - [-32_001]: 400 + [-32_022]: 400, + [-32_021]: 400, + [-32_020]: 400 }); }); @@ -419,15 +419,15 @@ describe('HTTP status mapping for ladder-originated errors (origin-keyed)', () = }); test('handler-originated errors stay in-band on HTTP 200, whatever their code', () => { - for (const code of [-32_603, -32_602, -32_601, -32_004, -32_002, -32_000, 1234]) { + for (const code of [-32_603, -32_602, -32_601, -32_022, -32_002, -32_000, 1234]) { expect(httpStatusForErrorCode(code, 'in-band')).toBe(200); } }); test('ladder-originated codes map to their HTTP statuses', () => { expect(httpStatusForErrorCode(-32_601, 'ladder')).toBe(404); - expect(httpStatusForErrorCode(-32_004, 'ladder')).toBe(400); - expect(httpStatusForErrorCode(-32_003, 'ladder')).toBe(400); - expect(httpStatusForErrorCode(-32_001, 'ladder')).toBe(400); + expect(httpStatusForErrorCode(-32_022, 'ladder')).toBe(400); + expect(httpStatusForErrorCode(-32_021, 'ladder')).toBe(400); + expect(httpStatusForErrorCode(-32_020, 'ladder')).toBe(400); }); }); diff --git a/packages/core/test/shared/mcpParamHeaders.test.ts b/packages/core/test/shared/mcpParamHeaders.test.ts index 55963a969..4f1303854 100644 --- a/packages/core/test/shared/mcpParamHeaders.test.ts +++ b/packages/core/test/shared/mcpParamHeaders.test.ts @@ -192,12 +192,12 @@ describe('validateMcpParamHeaders — server-behavior table', () => { }); // sep-2243-server-reject-missing-required — globally-untested manifest check, covered here. - test('body has the value but the header is absent → reject 400/-32001', () => { + test('body has the value but the header is absent → reject 400/-32020', () => { const r = validateMcpParamHeaders(DECLS, { region: 'us-west1' }, new Headers()); expect(r).toMatchObject({ kind: 'reject', httpStatus: 400, code: HEADER_MISMATCH_ERROR_CODE, cell: 'param-header-missing' }); }); - test('header present but disagreeing → reject 400/-32001 with the mismatch in data', () => { + test('header present but disagreeing → reject 400/-32020 with the mismatch in data', () => { const r = validateMcpParamHeaders(DECLS, { region: 'us-west1' }, new Headers({ [`${MCP_PARAM_HEADER_PREFIX}Region`]: 'eu' })); expect(r).toMatchObject({ kind: 'reject', @@ -208,7 +208,7 @@ describe('validateMcpParamHeaders — server-behavior table', () => { }); }); - test('invalid Base64 sentinel → reject 400/-32001', () => { + test('invalid Base64 sentinel → reject 400/-32020', () => { const r = validateMcpParamHeaders( DECLS, { region: 'Hello' }, @@ -238,8 +238,8 @@ describe('validateMcpParamHeaders — server-behavior table', () => { }); }); -describe('paramHeaderMismatchRejection — consumes the inbound-classifier −32001 shape verbatim', () => { - test('shape: 400 / -32001 / settled, with data.mismatch and the same message prefix', () => { +describe('paramHeaderMismatchRejection — consumes the inbound-classifier −32020 shape verbatim', () => { + test('shape: 400 / -32020 / settled, with data.mismatch and the same message prefix', () => { const r = paramHeaderMismatchRejection('param-header-mismatch', 'Mcp-Param-Region', 'body says us-west1'); expect(r).toEqual({ kind: 'reject', diff --git a/packages/core/test/shared/protocol.test.ts b/packages/core/test/shared/protocol.test.ts index 1103c2492..dbfc314d5 100644 --- a/packages/core/test/shared/protocol.test.ts +++ b/packages/core/test/shared/protocol.test.ts @@ -1127,7 +1127,7 @@ describe('inbound validation precedence: −32601 outranks envelope −32602', ( }); }); -describe('inbound protocol-version mismatch (−32004): the error data lists every supported version', () => { +describe('inbound protocol-version mismatch (−32022): the error data lists every supported version', () => { const flush = () => new Promise(resolve => setTimeout(resolve, 10)); test('a request classified for a protocol version this connection does not serve is rejected with the full supported list', async () => { @@ -1162,7 +1162,7 @@ describe('inbound protocol-version mismatch (−32004): the error data lists eve message: string; data?: { supported?: string[]; requested?: string }; }; - expect(error.code).toBe(-32004); + expect(error.code).toBe(-32022); expect(error.message).toContain('Unsupported protocol version'); expect(error.data?.supported).toEqual(supportedProtocolVersions); expect(error.data?.requested).toBe('2026-07-28'); diff --git a/packages/core/test/shared/standardHeaderValidation.test.ts b/packages/core/test/shared/standardHeaderValidation.test.ts index 97baadb40..935bd66ad 100644 --- a/packages/core/test/shared/standardHeaderValidation.test.ts +++ b/packages/core/test/shared/standardHeaderValidation.test.ts @@ -4,7 +4,7 @@ * * Evaluated by the HTTP entry on a modern-classified request immediately * after `classifyInboundRequest` returns a modern route: rejects `400` / - * `-32001` (`HeaderMismatch`) when the required `Mcp-Method` header is + * `-32020` (`HeaderMismatch`) 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, when the `Mcp-Name` header * carries an invalid Base64 sentinel, and when its (decoded) value disagrees @@ -56,7 +56,7 @@ function expectRejection(result: InboundLadderRejection | undefined, cell: strin expect(result?.cell).toBe(cell); expect(result?.rung).toBe('standard-header-validation'); expect(result?.httpStatus).toBe(400); - expect(result?.code).toBe(-32_001); + expect(result?.code).toBe(-32_020); expect(result?.settled).toBe(true); } diff --git a/packages/core/test/spec.types.2026-07-28.test.ts b/packages/core/test/spec.types.2026-07-28.test.ts index f88b8e11f..ada62d07e 100644 --- a/packages/core/test/spec.types.2026-07-28.test.ts +++ b/packages/core/test/spec.types.2026-07-28.test.ts @@ -838,8 +838,8 @@ describe('Spec Types (2026-07-28)', () => { it('pins the 2026-07-28 protocol version and the new error codes', () => { expect(LATEST_PROTOCOL_VERSION).toBe('2026-07-28'); - expect(MISSING_REQUIRED_CLIENT_CAPABILITY).toBe(-32003); - expect(UNSUPPORTED_PROTOCOL_VERSION).toBe(-32004); + expect(MISSING_REQUIRED_CLIENT_CAPABILITY).toBe(-32021); + expect(UNSUPPORTED_PROTOCOL_VERSION).toBe(-32022); expect(ProtocolErrorCode.MissingRequiredClientCapability).toBe(MISSING_REQUIRED_CLIENT_CAPABILITY); expect(ProtocolErrorCode.UnsupportedProtocolVersion).toBe(UNSUPPORTED_PROTOCOL_VERSION); }); diff --git a/packages/core/test/types/crossBundleErrorRecognition.test.ts b/packages/core/test/types/crossBundleErrorRecognition.test.ts index 35f69acba..c6f1377e9 100644 --- a/packages/core/test/types/crossBundleErrorRecognition.test.ts +++ b/packages/core/test/types/crossBundleErrorRecognition.test.ts @@ -38,7 +38,7 @@ class TestProtocol extends Protocol { * looks like to this copy: same name, same fields, different identity. */ class ForeignUnsupportedProtocolVersionError extends Error { - readonly code = -32_004; + readonly code = -32_022; readonly data = { supported: ['2025-11-25'], requested: '2099-01-01' }; constructor() { super('Unsupported protocol version: 2099-01-01'); @@ -47,7 +47,7 @@ class ForeignUnsupportedProtocolVersionError extends Error { } describe('cross-bundle typed-error recognition (data parse, never instanceof)', () => { - test('a -32004 error received over the wire materializes the typed class from code + data', async () => { + test('a -32022 error received over the wire materializes the typed class from code + data', async () => { // Full dispatch round trip: the peer answers with a plain JSON error // body — exactly what crosses a transport (and a bundle) boundary. const [clientTx, serverTx] = InMemoryTransport.createLinkedPair(); @@ -57,7 +57,7 @@ describe('cross-bundle typed-error recognition (data parse, never instanceof)', jsonrpc: '2.0', id: request.id, error: { - code: -32_004, + code: -32_022, message: 'Unsupported protocol version', data: { supported: ['2025-11-25', '2025-06-18'], requested: '2099-01-01' } } @@ -115,12 +115,12 @@ describe('cross-bundle typed-error recognition (data parse, never instanceof)', }); test('structurally invalid data falls back to the generic class — no guess, no throw', () => { - // -32004 with data that does not parse as UnsupportedProtocolVersionErrorData. + // -32022 with data that does not parse as UnsupportedProtocolVersionErrorData. for (const data of [undefined, null, 'nope', { supported: 'not-an-array', requested: '2099-01-01' }, { wrong: 'shape' }]) { - const recognized = ProtocolError.fromError(-32_004, 'unsupported', data); + const recognized = ProtocolError.fromError(-32_022, 'unsupported', data); expect(recognized).toBeInstanceOf(ProtocolError); expect(recognized).not.toBeInstanceOf(UnsupportedProtocolVersionError); - expect(recognized.code).toBe(-32_004); + expect(recognized.code).toBe(-32_022); } // -32042 with data missing the elicitations array. diff --git a/packages/core/test/types/errorSurfacePins.test.ts b/packages/core/test/types/errorSurfacePins.test.ts index a91bcd33f..0396abf9c 100644 --- a/packages/core/test/types/errorSurfacePins.test.ts +++ b/packages/core/test/types/errorSurfacePins.test.ts @@ -45,8 +45,8 @@ describe('ProtocolErrorCode', () => { InvalidParams: -32602, InternalError: -32603, ResourceNotFound: -32002, - MissingRequiredClientCapability: -32003, - UnsupportedProtocolVersion: -32004, + MissingRequiredClientCapability: -32021, + UnsupportedProtocolVersion: -32022, UrlElicitationRequired: -32042 }); }); @@ -112,13 +112,13 @@ describe('ProtocolError', () => { expect(urlError).toBeInstanceOf(UrlElicitationRequiredError); expect((urlError as UrlElicitationRequiredError).elicitations).toHaveLength(1); - const versionError = ProtocolError.fromError(-32004, 'unsupported', { supported: ['2025-11-25'], requested: '1999-01-01' }); + const versionError = ProtocolError.fromError(-32022, 'unsupported', { supported: ['2025-11-25'], requested: '1999-01-01' }); expect(versionError).toBeInstanceOf(UnsupportedProtocolVersionError); expect((versionError as UnsupportedProtocolVersionError).supported).toEqual(['2025-11-25']); expect((versionError as UnsupportedProtocolVersionError).requested).toBe('1999-01-01'); // Malformed/missing data falls back to the generic class instead of throwing. - const generic = ProtocolError.fromError(-32004, 'unsupported', { wrong: 'shape' }); + const generic = ProtocolError.fromError(-32022, 'unsupported', { wrong: 'shape' }); expect(generic).toBeInstanceOf(ProtocolError); expect(generic).not.toBeInstanceOf(UnsupportedProtocolVersionError); }); diff --git a/packages/core/test/types/errors.test.ts b/packages/core/test/types/errors.test.ts index b908dfb39..0b04ada84 100644 --- a/packages/core/test/types/errors.test.ts +++ b/packages/core/test/types/errors.test.ts @@ -6,10 +6,10 @@ import { ProtocolError, UnsupportedProtocolVersionError } from '../../src/types/ describe('UnsupportedProtocolVersionError', () => { const data = { supported: ['2025-11-25', '2025-06-18'], requested: '2026-07-28' }; - it('carries code -32004 and the supported/requested data', () => { + it('carries code -32022 and the supported/requested data', () => { const error = new UnsupportedProtocolVersionError(data); expect(error.code).toBe(ProtocolErrorCode.UnsupportedProtocolVersion); - expect(error.code).toBe(-32004); + expect(error.code).toBe(-32022); expect(error.supported).toEqual(['2025-11-25', '2025-06-18']); expect(error.requested).toBe('2026-07-28'); expect(error.data).toEqual(data); @@ -23,7 +23,7 @@ describe('UnsupportedProtocolVersionError', () => { }); it('is materialized by ProtocolError.fromError', () => { - const error = ProtocolError.fromError(-32004, 'Unsupported protocol version: 2026-07-28', data); + const error = ProtocolError.fromError(-32022, 'Unsupported protocol version: 2026-07-28', data); expect(error).toBeInstanceOf(UnsupportedProtocolVersionError); if (error instanceof UnsupportedProtocolVersionError) { expect(error.supported).toEqual(['2025-11-25', '2025-06-18']); @@ -34,10 +34,10 @@ describe('UnsupportedProtocolVersionError', () => { it('falls back to a generic ProtocolError when the data is missing or malformed', () => { for (const malformed of [undefined, {}, { supported: 'not-an-array', requested: '2026-07-28' }, { supported: ['2025-11-25'] }]) { - const error = ProtocolError.fromError(-32004, 'unsupported', malformed); + const error = ProtocolError.fromError(-32022, 'unsupported', malformed); expect(error).toBeInstanceOf(ProtocolError); expect(error).not.toBeInstanceOf(UnsupportedProtocolVersionError); - expect(error.code).toBe(-32004); + expect(error.code).toBe(-32022); expect(error.data).toEqual(malformed); } }); diff --git a/packages/core/test/types/missingClientCapabilityError.test.ts b/packages/core/test/types/missingClientCapabilityError.test.ts index 1d15ad1da..ce121ff8a 100644 --- a/packages/core/test/types/missingClientCapabilityError.test.ts +++ b/packages/core/test/types/missingClientCapabilityError.test.ts @@ -1,5 +1,5 @@ /** - * The `-32003` MissingRequiredClientCapability typed error. + * The `-32021` MissingRequiredClientCapability typed error. * * Recognition is data-parse based: a peer (or another bundled copy of the SDK) * is recognized by the error code plus the `data.requiredCapabilities` shape, @@ -11,10 +11,10 @@ import { ProtocolErrorCode } from '../../src/types/enums.js'; import { MissingRequiredClientCapabilityError, ProtocolError } from '../../src/types/errors.js'; describe('MissingRequiredClientCapabilityError', () => { - test('carries the -32003 code and the missing capabilities in data.requiredCapabilities', () => { + test('carries the -32021 code and the missing capabilities in data.requiredCapabilities', () => { const error = new MissingRequiredClientCapabilityError({ requiredCapabilities: { sampling: {}, elicitation: { url: {} } } }); expect(error.code).toBe(ProtocolErrorCode.MissingRequiredClientCapability); - expect(error.code).toBe(-32_003); + expect(error.code).toBe(-32_021); expect(error.requiredCapabilities).toEqual({ sampling: {}, elicitation: { url: {} } }); expect(error.data).toEqual({ requiredCapabilities: { sampling: {}, elicitation: { url: {} } } }); expect(error.message).toContain('sampling'); @@ -30,7 +30,7 @@ describe('MissingRequiredClientCapabilityError', () => { // Simulates an error received from the wire or from a separately // bundled SDK copy: plain code/message/data, no class identity. const wireShape = { - code: -32_003, + code: -32_021, message: 'Missing required client capabilities: sampling', data: { requiredCapabilities: { sampling: {} } } }; @@ -40,14 +40,14 @@ describe('MissingRequiredClientCapabilityError', () => { }); test('fromError falls back to the generic ProtocolError when the data shape does not match', () => { - expect(ProtocolError.fromError(-32_003, 'missing', undefined)).not.toBeInstanceOf(MissingRequiredClientCapabilityError); - expect(ProtocolError.fromError(-32_003, 'missing', { requiredCapabilities: ['sampling'] })).not.toBeInstanceOf( + expect(ProtocolError.fromError(-32_021, 'missing', undefined)).not.toBeInstanceOf(MissingRequiredClientCapabilityError); + expect(ProtocolError.fromError(-32_021, 'missing', { requiredCapabilities: ['sampling'] })).not.toBeInstanceOf( MissingRequiredClientCapabilityError ); - expect(ProtocolError.fromError(-32_003, 'missing', { somethingElse: true })).not.toBeInstanceOf( + expect(ProtocolError.fromError(-32_021, 'missing', { somethingElse: true })).not.toBeInstanceOf( MissingRequiredClientCapabilityError ); - expect(ProtocolError.fromError(-32_003, 'missing', { requiredCapabilities: { sampling: {} } })).toBeInstanceOf( + expect(ProtocolError.fromError(-32_021, 'missing', { requiredCapabilities: { sampling: {} } })).toBeInstanceOf( MissingRequiredClientCapabilityError ); }); @@ -57,7 +57,7 @@ describe('MissingRequiredClientCapabilityError', () => { requiredCapabilities: { sampling: {} } }); const looksLikeMissingCapability = - fromAnotherBundle.code === -32_003 && + fromAnotherBundle.code === -32_021 && typeof (fromAnotherBundle.data as { requiredCapabilities?: unknown } | undefined)?.requiredCapabilities === 'object'; expect(looksLikeMissingCapability).toBe(true); }); diff --git a/packages/core/test/wire/encodeContract.test.ts b/packages/core/test/wire/encodeContract.test.ts index 83c06223b..16025ecf5 100644 --- a/packages/core/test/wire/encodeContract.test.ts +++ b/packages/core/test/wire/encodeContract.test.ts @@ -209,7 +209,7 @@ describe('the error half of the encode seam — encodeErrorCode', () => { }); test('every other code passes through identically on both eras', () => { - for (const code of [-32_700, -32_600, -32_601, -32_602, -32_603, -32_000, -32_001, -32_003, -32_004, -32_042, -1, 0]) { + for (const code of [-32_700, -32_600, -32_601, -32_602, -32_603, -32_000, -32_020, -32_021, -32_022, -32_042, -1, 0]) { expect(rev2026Codec.encodeErrorCode(code)).toBe(code); expect(rev2025Codec.encodeErrorCode(code)).toBe(code); } diff --git a/packages/core/test/wire/eraGates.test.ts b/packages/core/test/wire/eraGates.test.ts index 8776ee69c..02fa30546 100644 --- a/packages/core/test/wire/eraGates.test.ts +++ b/packages/core/test/wire/eraGates.test.ts @@ -29,7 +29,7 @@ * `MessageExtraInfo.classification` (INJECTED here; the production * classifier is the entry/edge's job) no longer selects the era per message: * the funnel VALIDATES it against the instance era — a mismatch is an - * entry/routing error (typed −32004 rejection / notification drop, plus + * entry/routing error (typed −32022 rejection / notification drop, plus * onerror), and unclassified traffic on a legacy instance behaves exactly as * before the codec split (the B-2 rule). */ @@ -377,7 +377,7 @@ describe('encode-side deleted-field strictness (Q1-SD3 iii)', () => { }); describe('the edge→instance handoff — classification is validated, never an era switch', () => { - test('a modern-classified request on a legacy-era instance is an entry/routing error: typed −32004, handler never runs', async () => { + test('a modern-classified request on a legacy-era instance is an entry/routing error: typed −32022, handler never runs', async () => { let handlerRan = false; const h = await harness({ setup: receiver => { @@ -394,7 +394,7 @@ describe('the edge→instance handoff — classification is validated, never an expect(handlerRan).toBe(false); expect(h.sent).toHaveLength(1); const error = errorOf(h.sent[0]); - expect(error?.code).toBe(-32004); + expect(error?.code).toBe(-32022); expect(error?.message).toContain('Unsupported protocol version'); // Surfaced out of band too: the mismatch is the entry's bug, not the peer's. expect(h.errors.some(e => e.message.includes('Era mismatch'))).toBe(true); @@ -414,7 +414,7 @@ describe('the edge→instance handoff — classification is validated, never an }); await h.flush(); - expect(errorOf(h.sent[0])).toMatchObject({ code: -32004 }); + expect(errorOf(h.sent[0])).toMatchObject({ code: -32022 }); expect(h.errors.some(e => e.message.includes('Era mismatch'))).toBe(true); }); @@ -433,7 +433,7 @@ describe('the edge→instance handoff — classification is validated, never an await h.flush(); const error = errorOf(h.sent[0]) as { code: number; data?: { requested?: string } } | undefined; - expect(error?.code).toBe(-32004); + expect(error?.code).toBe(-32022); expect(error?.data?.requested).toBe('2025-06-18'); }); diff --git a/packages/server/src/server/createMcpHandler.ts b/packages/server/src/server/createMcpHandler.ts index 691bbca1f..c1b065687 100644 --- a/packages/server/src/server/createMcpHandler.ts +++ b/packages/server/src/server/createMcpHandler.ts @@ -502,7 +502,7 @@ async function classifyEntryRequest(request: Request, providedParsedBody?: unkno * answers it with the unsupported-protocol-version error), a malformed * envelope behind a present claim (answered `-32602`), a request whose * `MCP-Protocol-Version` header names a modern revision but that lacks the - * envelope (`-32602`), and header/body mismatches (`-32001`). Consumers + * envelope (`-32602`), and header/body mismatches (`-32020`). Consumers * routing on the predicate must send `false` traffic to the modern handler, * never to a legacy handler — the modern path owns those error answers. * - `server/discover` probes sent by negotiating clients always carry the @@ -637,7 +637,7 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa // `classifyInboundRequest` on the edge `era-classification` rung). // Evaluated after the supported-revision // gate so an envelope naming a revision this endpoint does not serve - // is still answered with `-32004` (the supported list is the more + // is still answered with `-32022` (the supported list is the more // useful answer to a client speaking the wrong revision); evaluated // before the capability gate, the factory call, and the // `Mcp-Param-*` rung so a request that fails several rungs is @@ -712,7 +712,7 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa // declaration against the request's `Mcp-Param-{Name}` headers and the // body `arguments`. A mismatch (or a missing header for a present body // value, or an invalid Base64 sentinel) emits the same `400` / - // `-32001` (`HeaderMismatch`) shape the edge cross-checks use. Only + // `-32020` (`HeaderMismatch`) shape the edge cross-checks use. Only // applied when the factory returns an `McpServer` (the registry is the // schema source); a low-level `Server` factory has no registry, so // there is nothing to validate against. diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index 6e3fffca0..f80c22927 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -509,7 +509,7 @@ export class Server extends Protocol { * must satisfy the at-least-one rule (`inputRequests` or * `requestState`), and every embedded request must be covered by the * capabilities the client declared on this request's envelope - * (violations answer with the typed `-32003` error). + * (violations answer with the typed `-32021` error). */ private async _invokeInputRequiredCapableHandler( method: string, @@ -601,7 +601,7 @@ export class Server extends Protocol { } // Per-embedded-request capability check against the capabilities the - // client declared on THIS request's envelope (-32003 on violation). + // client declared on THIS request's envelope (-32021 on violation). if (hasInputRequests) { const declared = ctx.mcpReq.envelope?.[CLIENT_CAPABILITIES_META_KEY] as ClientCapabilities | undefined; for (const [key, entry] of Object.entries(inputRequests)) { diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index fb6ffc2b0..ca5bd7ad0 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -1002,15 +1002,18 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { const stream = this._streamMapping.get(streamId); - if (!this._enableJsonResponse && stream?.controller && stream?.encoder) { - // For SSE responses, generate event ID if event store is provided + if (!this._enableJsonResponse) { + // Store FIRST so request-related events emitted while the per-request + // stream is disconnected (e.g. after `closeSSE()`) are replayed on + // reconnect — same store-first semantics as the standalone path above. let eventId: string | undefined; - if (this._eventStore) { eventId = await this._eventStore.storeEvent(streamId, message); } - // Write the event to the response stream - this.writeSSEEvent(stream.controller, stream.encoder, message, eventId); + if (stream?.controller && stream?.encoder) { + // Write the event to the response stream + this.writeSSEEvent(stream.controller, stream.encoder, message, eventId); + } } if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { diff --git a/packages/server/test/server/createMcpHandler.test.ts b/packages/server/test/server/createMcpHandler.test.ts index 59e98b6a1..fca9ac41a 100644 --- a/packages/server/test/server/createMcpHandler.test.ts +++ b/packages/server/test/server/createMcpHandler.test.ts @@ -211,14 +211,14 @@ describe('createMcpHandler — modern path', () => { ); expect(response.status).toBe(400); const body = (await response.json()) as JSONRPCErrorBody; - expect(body.error.code).toBe(-32_004); + expect(body.error.code).toBe(-32_022); expect(body.error.data?.['supported']).toEqual([MODERN_REVISION]); expect(body.error.data?.['requested']).toBe('2030-01-01'); expect(body.id).toBe(1); expect(state.contexts).toHaveLength(0); }); - it('rejects a header/body protocol-version mismatch with -32001 (HeaderMismatch) over HTTP 400', async () => { + it('rejects a header/body protocol-version mismatch with -32020 (HeaderMismatch) over HTTP 400', async () => { const { factory } = testFactory(); const onerror = vi.fn(); const handler = createMcpHandler(factory, { onerror }); @@ -226,7 +226,7 @@ describe('createMcpHandler — modern path', () => { const response = await handler.fetch(postRequest(modernToolsCall('echo', { text: 'x' }), { 'mcp-protocol-version': '2025-11-25' })); expect(response.status).toBe(400); const body = (await response.json()) as JSONRPCErrorBody; - expect(body.error.code).toBe(-32_001); + expect(body.error.code).toBe(-32_020); // The rejection echoes the request id. expect(body.id).toBe(1); expect(onerror).toHaveBeenCalled(); @@ -326,7 +326,7 @@ describe("createMcpHandler — modern-only strict (legacy: 'reject')", () => { ); expect(response.status).toBe(400); const body = (await response.json()) as JSONRPCErrorBody; - expect(body.error.code).toBe(-32_004); + expect(body.error.code).toBe(-32_022); expect(body.error.data?.['supported']).toEqual([MODERN_REVISION]); expect(body.id).toBe(1); expect(state.contexts).toHaveLength(0); @@ -347,7 +347,7 @@ describe("createMcpHandler — modern-only strict (legacy: 'reject')", () => { ); expect(response.status).toBe(400); const body = (await response.json()) as JSONRPCErrorBody; - expect(body.error.code).toBe(-32_004); + expect(body.error.code).toBe(-32_022); expect(body.error.data?.['supported']).toEqual([MODERN_REVISION]); expect(body.error.data?.['requested']).toBe('2025-11-25'); expect(body.id).toBe('init-1'); @@ -679,7 +679,7 @@ describe('createMcpHandler — user-land routing with isLegacyRequest (replaces request: () => postRequest({ jsonrpc: '2.0', id: 5, method: 'server/discover', params: { _meta: ENVELOPE } }) }, { - name: 'envelope claiming an unsupported revision (modern path answers -32004)', + name: 'envelope claiming an unsupported revision (modern path answers -32022)', request: () => postRequest(modernToolsCall('echo', { text: 'x' }, { ...ENVELOPE, [PROTOCOL_VERSION_META_KEY]: '2030-01-01' })) }, @@ -696,7 +696,7 @@ describe('createMcpHandler — user-land routing with isLegacyRequest (replaces ) }, { - name: 'header/body mismatch (modern path answers -32001)', + name: 'header/body mismatch (modern path answers -32020)', request: () => postRequest(modernToolsCall('echo', { text: 'x' }), { 'mcp-protocol-version': '2025-11-25' }) } ]; diff --git a/packages/server/test/server/createMcpHandlerCapabilityGate.test.ts b/packages/server/test/server/createMcpHandlerCapabilityGate.test.ts index 9d40e8e66..38d346391 100644 --- a/packages/server/test/server/createMcpHandlerCapabilityGate.test.ts +++ b/packages/server/test/server/createMcpHandlerCapabilityGate.test.ts @@ -1,7 +1,7 @@ /** * The pre-dispatch client-capability gate at the HTTP entry: a request to a * method that requires a client capability the request's envelope did not - * declare is refused with the typed `-32003` error and HTTP 400, before any + * declare is refused with the typed `-32021` error and HTTP 400, before any * server instance is constructed or dispatched. * * No request method served on the 2026-07-28 registry has a static @@ -71,7 +71,7 @@ describe('the pre-dispatch client-capability gate', () => { expect(body.result.content[0]?.text).toBe('hi'); }); - it('refuses a request missing a required capability with -32003 and HTTP 400, echoing the request id', async () => { + it('refuses a request missing a required capability with -32021 and HTTP 400, echoing the request id', async () => { requirementTable['tools/call'] = { sampling: {} }; let factoryRan = false; const handler = createMcpHandler(() => { @@ -85,7 +85,7 @@ describe('the pre-dispatch client-capability gate', () => { id: unknown; error: { code: number; data?: { requiredCapabilities?: ClientCapabilities } }; }; - expect(body.error.code).toBe(-32_003); + expect(body.error.code).toBe(-32_021); expect(body.error.data?.requiredCapabilities).toEqual({ sampling: {} }); expect(body.id).toBe(7); // Pre-dispatch: the refusal happens before any per-request instance exists. diff --git a/packages/server/test/server/inputRequired.test.ts b/packages/server/test/server/inputRequired.test.ts index 9d0e1d26c..efce87284 100644 --- a/packages/server/test/server/inputRequired.test.ts +++ b/packages/server/test/server/inputRequired.test.ts @@ -7,7 +7,7 @@ * tools/call result schema are skipped for it; cache fields are never * stamped on it); * - the guards: at-least-one re-check for hand-built results, the per-embedded - * -request `-32003` capability check against the request's OWN envelope + * -request `-32021` capability check against the request's OWN envelope * capabilities, the server-bug guard (non-multi-round-trip methods, and any * method on a 2025-era request, never put a mis-typed result on the wire); * - a UrlElicitationRequiredError escaping a handler on the modern era fails @@ -222,7 +222,7 @@ describe('guards', () => { await close(); }); - it('checks every embedded request against the capabilities the request itself declared (-32003 on violation)', async () => { + it('checks every embedded request against the capabilities the request itself declared (-32021 on violation)', async () => { const server = new McpServer({ name: 's', version: '1.0.0' }, { capabilities: { tools: {} } }); server.registerTool('ask', { inputSchema: z.object({}) }, async () => inputRequired({ @@ -240,10 +240,10 @@ describe('guards', () => { ); const { request, close } = await wire(server, { era: 'modern' }); - // No elicitation capability declared on the request → -32003 naming + // No elicitation capability declared on the request → -32021 naming // the form sub-capability the embedded form-mode elicitation needs. const noCapability = await request(modernToolCall(1, 'ask', {}, { clientCapabilities: {} })); - expect(errorOf(noCapability).code).toBe(-32_003); + expect(errorOf(noCapability).code).toBe(-32_021); expect(errorOf(noCapability).data).toMatchObject({ requiredCapabilities: { elicitation: { form: {} } } }); // Form-mode capability declared → the same tool is served. @@ -254,13 +254,13 @@ describe('guards', () => { const urlWithoutUrlCapability = await request( modernToolCall(3, 'open-url', {}, { clientCapabilities: { elicitation: { form: {} } } }) ); - expect(errorOf(urlWithoutUrlCapability).code).toBe(-32_003); + expect(errorOf(urlWithoutUrlCapability).code).toBe(-32_021); expect(errorOf(urlWithoutUrlCapability).data).toMatchObject({ requiredCapabilities: { elicitation: { url: {} } } }); - // Form-mode embedded request toward a URL-only client → -32003: modes + // Form-mode embedded request toward a URL-only client → -32021: modes // are sub-capabilities and the server must not send an undeclared one. const formTowardUrlOnly = await request(modernToolCall(4, 'ask', {}, { clientCapabilities: { elicitation: { url: {} } } })); - expect(errorOf(formTowardUrlOnly).code).toBe(-32_003); + expect(errorOf(formTowardUrlOnly).code).toBe(-32_021); expect(errorOf(formTowardUrlOnly).data).toMatchObject({ requiredCapabilities: { elicitation: { form: {} } } }); // A bare `elicitation: {}` declaration is read as form support (the diff --git a/packages/server/test/server/invokeSeam.test.ts b/packages/server/test/server/invokeSeam.test.ts index 98cd86377..764bd9951 100644 --- a/packages/server/test/server/invokeSeam.test.ts +++ b/packages/server/test/server/invokeSeam.test.ts @@ -106,7 +106,7 @@ describe('invoke', () => { const response = await invoke(mcpServer, toolsCall('greet', { who: 'world' }), { classification: MODERN }); expect(response.status).toBe(400); const body = (await response.json()) as { error: { code: number; data: { supported: string[] } } }; - expect(body.error.code).toBe(-32_004); + expect(body.error.code).toBe(-32_022); expect(Array.isArray(body.error.data.supported)).toBe(true); }); diff --git a/packages/server/test/server/mcpParamValidation.test.ts b/packages/server/test/server/mcpParamValidation.test.ts index 8c49d92b4..0b8b02158 100644 --- a/packages/server/test/server/mcpParamValidation.test.ts +++ b/packages/server/test/server/mcpParamValidation.test.ts @@ -4,7 +4,7 @@ * * Pre-dispatch ladder rung: a `tools/call` whose `Mcp-Param-{Name}` headers * disagree with the body `arguments` (or are missing for a present body value, - * or carry an invalid Base64 sentinel) is rejected `400` / `-32001` with the + * or carry an invalid Base64 sentinel) is rejected `400` / `-32020` with the * same `HeaderMismatch` shape the inbound classifier emits for the * standard-header cross-checks. A `null`/absent body value passes regardless * of the header (the spec's "server MUST NOT expect" rows). The @@ -79,25 +79,25 @@ describe('SEP-2243 Mcp-Param-* server validation (createMcpHandler, modern era)' expect(response.status).toBe(200); }); - it('a disagreeing header is rejected 400/-32001 (HeaderMismatch) and reports the rejection', async () => { + it('a disagreeing header is rejected 400/-32020 (HeaderMismatch) and reports the rejection', async () => { const onerror = vi.fn(); const handler = createMcpHandler(makeFactory(), { onerror }); const response = await handler.fetch(call({ region: 'us-west1' }, { 'Mcp-Param-Region': 'eu' })); expect(response.status).toBe(400); const body = (await response.json()) as { id: unknown; error: { code: number; data?: { mismatch?: { header?: string } } } }; - expect(body.error.code).toBe(-32_001); + expect(body.error.code).toBe(-32_020); expect(body.error.data?.mismatch?.header).toBe('Mcp-Param-Region'); expect(body.id).toBe(7); expect(onerror).toHaveBeenCalled(); }); // sep-2243-server-reject-missing-required (globally-untested manifest check). - it('a missing header for a present body value is rejected 400/-32001', async () => { + it('a missing header for a present body value is rejected 400/-32020', async () => { const handler = createMcpHandler(makeFactory()); const response = await handler.fetch(call({ region: 'us-west1' })); expect(response.status).toBe(400); const body = (await response.json()) as { error: { code: number } }; - expect(body.error.code).toBe(-32_001); + expect(body.error.code).toBe(-32_020); }); // sep-2243-server-not-expect-null (globally-untested manifest check). @@ -109,11 +109,11 @@ describe('SEP-2243 Mcp-Param-* server validation (createMcpHandler, modern era)' expect(r2.status).toBe(200); }); - it('an invalid Base64 sentinel is rejected 400/-32001', async () => { + it('an invalid Base64 sentinel is rejected 400/-32020', async () => { const handler = createMcpHandler(makeFactory()); const response = await handler.fetch(call({ region: 'Hello' }, { 'Mcp-Param-Region': '=?base64?SGVsbG8?=' })); expect(response.status).toBe(400); - expect(((await response.json()) as { error: { code: number } }).error.code).toBe(-32_001); + expect(((await response.json()) as { error: { code: number } }).error.code).toBe(-32_020); }); }); diff --git a/packages/server/test/server/perRequestTransport.test.ts b/packages/server/test/server/perRequestTransport.test.ts index 38d2952a4..69a9088d7 100644 --- a/packages/server/test/server/perRequestTransport.test.ts +++ b/packages/server/test/server/perRequestTransport.test.ts @@ -127,7 +127,7 @@ describe('classification handoff into dispatch', () => { const response = await transport.handleMessage(toolsCall(1, null)); expect(response.status).toBe(400); const error = errorOf(await response.json()); - expect(error?.code).toBe(-32_004); + expect(error?.code).toBe(-32_022); expect(error?.data).toMatchObject({ requested: expect.any(String), supported: expect.any(Array) }); }); @@ -140,7 +140,7 @@ describe('classification handoff into dispatch', () => { const transport = await connectedTransport(server, { classification: MODERN }); const response = await transport.handleMessage(toolsCall()); expect(response.status).toBe(400); - expect(errorOf(await response.json())?.code).toBe(-32_004); + expect(errorOf(await response.json())?.code).toBe(-32_022); }); }); @@ -195,13 +195,13 @@ describe('HTTP status mapping', () => { it('keeps a handler-thrown unsupported-protocol-version error in-band on HTTP 200', async () => { const { server } = modernServer({ toolsCallHandler: async () => { - throw new ProtocolError(-32_004, 'Unsupported protocol version: 2099-01-01'); + throw new ProtocolError(-32_022, 'Unsupported protocol version: 2099-01-01'); } }); const transport = await connectedTransport(server); const response = await transport.handleMessage(toolsCall()); expect(response.status).toBe(200); - expect(errorOf(await response.json())?.code).toBe(-32_004); + expect(errorOf(await response.json())?.code).toBe(-32_022); }); it('keeps handler-produced invalid-params errors in-band on HTTP 200 (never status-mapped)', async () => { diff --git a/packages/server/test/server/serveStdio.test.ts b/packages/server/test/server/serveStdio.test.ts index e34291817..aced8d3b9 100644 --- a/packages/server/test/server/serveStdio.test.ts +++ b/packages/server/test/server/serveStdio.test.ts @@ -233,7 +233,7 @@ describe('modern opening', () => { await handle.close(); }); - it('once the modern era is pinned, a late claim-less initialize answers -32004 naming the supported revisions', async () => { + it('once the modern era is pinned, a late claim-less initialize answers -32022 naming the supported revisions', async () => { const { handle, request } = await startEntry(); const list = await request({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: envelope() } }); @@ -242,7 +242,7 @@ describe('modern opening', () => { const init = await request(initializeRequest(2)); expect(isJSONRPCErrorResponse(init)).toBe(true); if (isJSONRPCErrorResponse(init)) { - expect(init.error.code).toBe(-32_004); + expect(init.error.code).toBe(-32_022); const data = init.error.data as { supported?: string[]; requested?: string }; expect(data.supported).toContain(MODERN); expect(data.requested).toBe(LATEST_PROTOCOL_VERSION); @@ -457,7 +457,7 @@ describe('server/discover probe window', () => { const init = await request(initializeRequest(3)); expect(isJSONRPCErrorResponse(init)).toBe(true); if (isJSONRPCErrorResponse(init)) { - expect(init.error.code).toBe(-32_004); + expect(init.error.code).toBe(-32_022); } // The probe instance is the pinned instance: the factory ran exactly once. expect(eras).toEqual(['modern']); @@ -493,13 +493,13 @@ describe('server/discover probe window', () => { }); describe("legacy: 'reject'", () => { - it('answers a legacy opening with -32004 naming the supported modern revisions and never pins a legacy instance', async () => { + it('answers a legacy opening with -32022 naming the supported modern revisions and never pins a legacy instance', async () => { const { handle, request, eras } = await startEntry({ legacy: 'reject' }); const init = await request(initializeRequest(1)); expect(isJSONRPCErrorResponse(init)).toBe(true); if (isJSONRPCErrorResponse(init)) { - expect(init.error.code).toBe(-32_004); + expect(init.error.code).toBe(-32_022); const data = init.error.data as { supported?: string[]; requested?: string }; expect(data.supported).toContain(MODERN); expect(data.requested).toBe(LATEST_PROTOCOL_VERSION); @@ -553,7 +553,7 @@ describe('malformed and unsupported envelope claims (entry-answered, never pinne await handle.close(); }); - it('a valid claim naming an unsupported revision answers -32004 with the supported list', async () => { + it('a valid claim naming an unsupported revision answers -32022 with the supported list', async () => { const { handle, request, eras } = await startEntry(); const response = await request({ @@ -564,7 +564,7 @@ describe('malformed and unsupported envelope claims (entry-answered, never pinne }); expect(isJSONRPCErrorResponse(response)).toBe(true); if (isJSONRPCErrorResponse(response)) { - expect(response.error.code).toBe(-32_004); + expect(response.error.code).toBe(-32_022); const data = (response as JSONRPCErrorResponse).error.data as { supported?: string[]; requested?: string }; expect(data.supported).toContain(MODERN); expect(data.requested).toBe('2099-01-01'); diff --git a/packages/server/test/server/serveStdioListen.test.ts b/packages/server/test/server/serveStdioListen.test.ts index d800a21b2..b78ae5a75 100644 --- a/packages/server/test/server/serveStdioListen.test.ts +++ b/packages/server/test/server/serveStdioListen.test.ts @@ -182,7 +182,7 @@ describe('serveStdio — subscriptions/listen', () => { expect(err.id).toBe(9); // Same shape the opening classifier produces for an unsupported // revision (ProtocolErrorCode.UnsupportedProtocolVersion). - expect(err.error.code).toBe(-32_004); + expect(err.error.code).toBe(-32_022); expect(err.error.data).toMatchObject({ requested: '2099-01-01' }); expect(inbound.some(m => (m as JSONRPCNotification).method === 'notifications/subscriptions/acknowledged')).toBe(false); await handle.close(); diff --git a/packages/server/test/server/stdHeaderValidation.test.ts b/packages/server/test/server/stdHeaderValidation.test.ts index 58a8c89a4..3c7f9238a 100644 --- a/packages/server/test/server/stdHeaderValidation.test.ts +++ b/packages/server/test/server/stdHeaderValidation.test.ts @@ -8,7 +8,7 @@ * header, a missing `Mcp-Name` header on a `tools/call` / `prompts/get` / * `resources/read` request, an `Mcp-Name` value disagreeing with * `params.name` / `params.uri`, and an invalid `Mcp-Name` Base64 sentinel are - * all rejected `400` / `-32001` (`HeaderMismatch`) on the + * all rejected `400` / `-32020` (`HeaderMismatch`) on the * `standard-header-validation` rung — the same shape the classifier already * emits for the `MCP-Protocol-Version` and `Mcp-Method` mismatch cells on the * edge `era-classification` rung. Legacy-era traffic is byte-unchanged. @@ -59,7 +59,7 @@ async function expectHeaderMismatch(response: Response): Promise<{ code: number; expect(response.status).toBe(400); const body = (await response.json()) as { id: unknown; error: { code: number; message: string } }; expect(body.id).toBe(5); - expect(body.error.code).toBe(-32_001); + expect(body.error.code).toBe(-32_020); return body.error; } @@ -74,13 +74,13 @@ describe('SEP-2243 standard-header validation (createMcpHandler, modern era)', ( expect(body.result.content[0]?.text).toBe('hi'); }); - it('a missing Mcp-Method header is rejected 400/-32001', async () => { + it('a missing Mcp-Method header is rejected 400/-32020', async () => { const handler = createMcpHandler(makeFactory()); const error = await expectHeaderMismatch(await handler.fetch(modernRequest('tools/list', {}))); expect(error.message).toContain('Mcp-Method header is absent'); }); - it('a missing Mcp-Name header on tools/call is rejected 400/-32001', async () => { + it('a missing Mcp-Name header on tools/call is rejected 400/-32020', async () => { const handler = createMcpHandler(makeFactory()); const error = await expectHeaderMismatch( await handler.fetch(modernRequest('tools/call', { name: 'echo', arguments: {} }, { 'mcp-method': 'tools/call' })) @@ -88,7 +88,7 @@ describe('SEP-2243 standard-header validation (createMcpHandler, modern era)', ( expect(error.message).toContain('Mcp-Name header is absent'); }); - it('an Mcp-Name header disagreeing with params.name is rejected 400/-32001', async () => { + it('an Mcp-Name header disagreeing with params.name is rejected 400/-32020', async () => { const handler = createMcpHandler(makeFactory()); const error = await expectHeaderMismatch( await handler.fetch( @@ -128,7 +128,7 @@ describe('SEP-2243 standard-header validation (createMcpHandler, modern era)', ( expect(sentinel.status).toBe(200); }); - it('an invalid Mcp-Name Base64 sentinel is rejected 400/-32001', async () => { + it('an invalid Mcp-Name Base64 sentinel is rejected 400/-32020', async () => { const handler = createMcpHandler(makeFactory()); await expectHeaderMismatch( await handler.fetch( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f062d6895..6dab5e1d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1540,8 +1540,8 @@ importers: specifier: workspace:^ version: link:../../packages/client '@modelcontextprotocol/conformance': - specifier: file:./vendor/modelcontextprotocol-conformance-0.2.0-main.d70d7ad.tgz - version: file:test/conformance/vendor/modelcontextprotocol-conformance-0.2.0-main.d70d7ad.tgz(@cfworker/json-schema@4.1.1) + specifier: https://pkg.pr.new/@modelcontextprotocol/conformance@357 + version: https://pkg.pr.new/@modelcontextprotocol/conformance@357(@cfworker/json-schema@4.1.1) '@modelcontextprotocol/core': specifier: workspace:^ version: link:../../packages/core @@ -2552,9 +2552,9 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - '@modelcontextprotocol/conformance@file:test/conformance/vendor/modelcontextprotocol-conformance-0.2.0-main.d70d7ad.tgz': - resolution: {integrity: sha512-L61dGG8Fx+JzenEwzROovVB+tcIT8o6TzuKSVegYzgw3oIsf3aIHASJgZtKbnrhgOnSowxBpHm74d/HDKiaZnQ==, tarball: file:test/conformance/vendor/modelcontextprotocol-conformance-0.2.0-main.d70d7ad.tgz} - version: 0.2.0-main.d70d7ad + '@modelcontextprotocol/conformance@https://pkg.pr.new/@modelcontextprotocol/conformance@357': + resolution: {integrity: sha512-lBpIgubkIbMDksgtcebY6d/Z7HgQF81BIKIdudVhLOtgF8ymvhlC9O/ktAuLCDC+E1x7w/G96s89zRxH2Rf/FA==, tarball: https://pkg.pr.new/@modelcontextprotocol/conformance@357} + version: 0.2.0-alpha.5 hasBin: true '@modelcontextprotocol/sdk@1.29.0': @@ -6443,7 +6443,7 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@modelcontextprotocol/conformance@file:test/conformance/vendor/modelcontextprotocol-conformance-0.2.0-main.d70d7ad.tgz(@cfworker/json-schema@4.1.1)': + '@modelcontextprotocol/conformance@https://pkg.pr.new/@modelcontextprotocol/conformance@357(@cfworker/json-schema@4.1.1)': dependencies: '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) '@octokit/rest': 22.0.1 diff --git a/test/conformance/README.md b/test/conformance/README.md index 08521b21f..a30c2da87 100644 --- a/test/conformance/README.md +++ b/test/conformance/README.md @@ -4,18 +4,9 @@ This directory contains conformance test implementations for the TypeScript MCP > `@modelcontextprotocol/conformance` is pinned to an exact version (no `^` range) in `package.json`. New conformance releases are adopted by deliberately bumping the pin and updating `expected-failures.yaml` for any new scenarios/checks in the same change. -> **Local pin (temporary):** the pin currently points at a vendored tarball (`file:./vendor/modelcontextprotocol-conformance-0.2.0-main.d70d7ad.tgz`) built from `modelcontextprotocol/conformance@main` (`d70d7ad`, post-`0.2.0-alpha.4`). This pulls in -> [#347](https://github.com/modelcontextprotocol/conformance/pull/347) (client-scenario mocks now serve `server/discover`) ahead of the next published release so the discover-shim removal can be exercised. To revert to a published release, replace the `file:` spec in -> `package.json` with the exact version string (e.g. `"0.2.0-alpha.5"`), delete `vendor/`, run `pnpm install`, and reconcile `expected-failures.yaml` in the same change. -> -> Tarball sha256: `d09d694de2d3982e511d2b6a91af93b2e09a83f107bfc8f879c090d995e8ecfe`. Rebuild (from a checkout of `modelcontextprotocol/conformance` at `d70d7ad`, with this SDK's workspace `tsdown` on `PATH` because the conformance repo's own devDeps are unavailable on the -> internal mirror): -> -> ```sh -> npm version 0.2.0-main.d70d7ad --no-git-tag-version -> tsdown # deps externalised identically to the published alpha.4 dist -> npm pack --ignore-scripts -> ``` +> **Pre-release pin (temporary):** the pin currently points at the [pkg.pr.new](https://pkg.pr.new) build of [conformance#357](https://github.com/modelcontextprotocol/conformance/pull/357) (`https://pkg.pr.new/@modelcontextprotocol/conformance@357`, commit `65fcd39` — the +> `0.2.0-alpha.5` version bump on top of `main@d70d7ad`, carrying [#347](https://github.com/modelcontextprotocol/conformance/pull/347) and [#353](https://github.com/modelcontextprotocol/conformance/pull/353)). This is pinned to the alpha.5 pkg.pr.new build pending the npm +> publish; switch to `^0.2.0-alpha.5` once published, run `pnpm install`, and reconcile `expected-failures.yaml` in the same change. ## Client Conformance Tests diff --git a/test/conformance/expected-failures.2026-07-28.yaml b/test/conformance/expected-failures.2026-07-28.yaml index fe80b00a0..7e5f90148 100644 --- a/test/conformance/expected-failures.2026-07-28.yaml +++ b/test/conformance/expected-failures.2026-07-28.yaml @@ -15,6 +15,10 @@ # release pinned in package.json. Newer conformance releases are adopted by # deliberately bumping the pin and reconciling this file in the same change. # +# NOTE: the SDK's modern-path rejection codes are aligned with this referee's +# assignments — the spec#2907 / conformance#353 renumber (-32020 / -32021 / +# -32022) is adopted on both the emission and recognition paths. +# # Entries are grouped by what unblocks them. As each gap closes the # corresponding scenarios start passing and MUST be removed from this list # (the runner fails on stale entries), so the baseline burns down per @@ -40,18 +44,13 @@ client: # --- Auth scenarios cut short by the 2026 connection lifecycle --- # The fixture's auth flow drives the 2025 stateful lifecycle; the - # 2026-mode mock rejects the MCP POST (-32001, missing + # 2026-mode mock rejects the MCP POST (-32020, missing # MCP-Protocol-Version header) before the scope-escalation behaviour these # scenarios measure, so no authorization requests are observed. Unblocks # when the auth fixture flow speaks the 2026 per-request lifecycle. - auth/scope-step-up - auth/scope-retry-limit - # --- conformance#353 error-code renumber (spec#2907) --- - # Renumber pending Felix ruling (spec#2907 / conformance#353) — SDK still - # emits −32001/−32003/−32004. Same failure as the 2025 leg. - - request-metadata - # --- Same gaps as the 2025 baseline (fail identically when forced to 2026-07-28) --- # SEP-2106 (JSON Schema $ref handling): no fixture handler for the scenario yet. - json-schema-ref-no-deref @@ -74,12 +73,3 @@ server: # checks; it fails identically at 2025 in `--suite all` (not a 2026-path # regression). - json-schema-2020-12 - # conformance#353 error-code renumber: renumber pending Felix ruling - # (spec#2907 / conformance#353) — SDK still emits −32001/−32003/−32004; - # same failure as the 2025 leg. - - http-custom-header-server-validation - - http-header-validation - # One #353-renumber check fires in the server-stateless modern leg; the - # listChanged-on-listen WARNING was burned down separately by the listen - # arming change. - - server-stateless diff --git a/test/conformance/expected-failures.yaml b/test/conformance/expected-failures.yaml index 0a0ab4b68..0da31cd54 100644 --- a/test/conformance/expected-failures.yaml +++ b/test/conformance/expected-failures.yaml @@ -2,17 +2,14 @@ # CI exits 0 if only these fail, exits 1 on unexpected failures or stale entries. # # Baseline established against the @modelcontextprotocol/conformance pin in -# package.json (0.2.0-main.d70d7ad — a vendored build of conformance@main; see -# README.md). Newer conformance releases are adopted by deliberately bumping +# package.json (0.2.0-alpha.5 (via pkg.pr.new #357); see README.md). Newer +# conformance releases are adopted by deliberately bumping # the package.json pin and reconciling this file in the same change. # -# NOTE: the SDK's modern-path rejection codes are NOT yet aligned with this -# referee's assignments. The referee at this pin asserts the spec#2907 / -# conformance#353 renumber (-32020 HeaderMismatch / -32021 / -32022); the SDK -# still emits the pre-renumber codes (-32001 / -32003 / -32004) pending a -# project-level ruling on adopting the renumber. The affected cells are -# annotated below ("renumber pending Felix ruling") and re-derived when the -# ruling lands and/or a newer conformance release is pinned. A missing _meta +# NOTE: the SDK's modern-path rejection codes are aligned with this referee's +# assignments — the spec#2907 / conformance#353 renumber (-32020 HeaderMismatch +# / -32021 MissingRequiredClientCapability / -32022 UnsupportedProtocolVersion) +# is adopted on both the emission and recognition paths. A missing _meta # envelope (or missing protocolVersion key) still answers -32602 on both sides. # # Entries are grouped by SEP. As each SEP/milestone is implemented in the SDK the @@ -43,24 +40,8 @@ client: # client support for the token-exchange + JWT bearer flow. - auth/enterprise-managed-authorization - # --- conformance#353 error-code renumber (spec#2907) --- - # Renumber pending Felix ruling (spec#2907 / conformance#353) — SDK still - # emits −32001/−32003/−32004. The mock now rejects with −32020/−32022 where - # the SDK still recognises −32001/−32004, so connect falls through to a - # malformed `initialize` result. - - request-metadata - server: # --- Draft-spec scenarios (in `--suite draft`; the default `active` suite is green) --- - # --- conformance#353 error-code renumber (spec#2907) --- - # Renumber pending Felix ruling (spec#2907 / conformance#353) — SDK still - # emits −32001/−32003/−32004; the referee at this pin asserts - # −32020/−32021/−32022 for header/body mismatch, missing-required-client- - # capability, and unsupported-protocol-version respectively. - - http-custom-header-server-validation - # WARNING-only at this pin; same #353 cause as above. - - http-header-validation - # One #353-renumber check fires in the server-stateless modern leg; the - # listChanged-on-listen WARNING was burned down separately by the listen - # arming change. - - server-stateless + # (empty — the conformance#353 error-code renumber entries burned when the + # SDK adopted the spec#2907 -32020/-32021/-32022 assignments.) + [] diff --git a/test/conformance/package.json b/test/conformance/package.json index a2688e54e..580afd51a 100644 --- a/test/conformance/package.json +++ b/test/conformance/package.json @@ -40,7 +40,7 @@ "test:conformance:all": "pnpm run test:conformance:client:all && pnpm run test:conformance:server:all" }, "devDependencies": { - "@modelcontextprotocol/conformance": "file:./vendor/modelcontextprotocol-conformance-0.2.0-main.d70d7ad.tgz", + "@modelcontextprotocol/conformance": "https://pkg.pr.new/@modelcontextprotocol/conformance@357", "@modelcontextprotocol/client": "workspace:^", "@modelcontextprotocol/server": "workspace:^", "@modelcontextprotocol/core": "workspace:^", diff --git a/test/conformance/src/everythingClient.ts b/test/conformance/src/everythingClient.ts index 1cc2255d6..92e70f9f1 100644 --- a/test/conformance/src/everythingClient.ts +++ b/test/conformance/src/everythingClient.ts @@ -195,7 +195,7 @@ async function runToolsCallModernClient(serverUrl: string): Promise { // request-metadata scenario (SEP-2575): every request must carry the // MCP-Protocol-Version header and the per-request _meta envelope, and the // client must retry with a supported version when its first choice is -// rejected with -32004. The version-negotiation probe (server/discover plus +// rejected with -32022. The version-negotiation probe (server/discover plus // the corrective continuation) is exactly that mechanism. async function runRequestMetadataClient(serverUrl: string): Promise { const clientInfo = { name: 'test-client', version: '1.0.0' }; diff --git a/test/conformance/src/everythingServer.ts b/test/conformance/src/everythingServer.ts index bb349a3f8..256fb43ff 100644 --- a/test/conformance/src/everythingServer.ts +++ b/test/conformance/src/everythingServer.ts @@ -930,7 +930,7 @@ function createMcpServer() { // Capability-aware input requests: only ask for kinds the request's // declared client capabilities cover (the server seam enforces the same - // rule with a -32003 error; the tool simply never trips it). + // rule with a -32021 error; the tool simply never trips it). mcpServer.registerTool( 'test_input_required_result_capabilities', { diff --git a/test/conformance/vendor/.gitignore b/test/conformance/vendor/.gitignore deleted file mode 100644 index fceadb8a3..000000000 --- a/test/conformance/vendor/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Allow vendored conformance tarballs (root .gitignore has *.tgz). -!*.tgz diff --git a/test/conformance/vendor/modelcontextprotocol-conformance-0.2.0-main.d70d7ad.tgz b/test/conformance/vendor/modelcontextprotocol-conformance-0.2.0-main.d70d7ad.tgz deleted file mode 100644 index 2ef865142..000000000 Binary files a/test/conformance/vendor/modelcontextprotocol-conformance-0.2.0-main.d70d7ad.tgz and /dev/null differ diff --git a/test/e2e/requirements.ts b/test/e2e/requirements.ts index b6aeeaac7..60972ac16 100644 --- a/test/e2e/requirements.ts +++ b/test/e2e/requirements.ts @@ -2797,7 +2797,7 @@ export const REQUIREMENTS: Record = { 'typescript:mrtr:url-elicitation:no-32042-on-2026': { source: 'https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr', behavior: - 'URL-mode elicitation rides the multi-round-trip flow on the 2026-07-28 era: a tool handler that returns inputRequired.elicitUrl(...) embeds a URL-mode elicitation/create in an input_required result (capability-gated by -32003 on elicitation.url), the registered elicitation handler fulfils it, the retried call completes, and the urlElicitationRequired error code (-32042) never appears on the wire.', + 'URL-mode elicitation rides the multi-round-trip flow on the 2026-07-28 era: a tool handler that returns inputRequired.elicitUrl(...) embeds a URL-mode elicitation/create in an input_required result (capability-gated by -32021 on elicitation.url), the registered elicitation handler fulfils it, the retried call completes, and the urlElicitationRequired error code (-32042) never appears on the wire.', addedInSpecVersion: '2026-07-28', transports: ['entryModern'], supersedes: ['mcpserver:tool:url-elicitation-error', 'elicitation:url:required-error'], diff --git a/test/e2e/scenarios/sep2243.test.ts b/test/e2e/scenarios/sep2243.test.ts index 83353d9ce..4b946dc8d 100644 --- a/test/e2e/scenarios/sep2243.test.ts +++ b/test/e2e/scenarios/sep2243.test.ts @@ -54,7 +54,7 @@ verifies('sep-2243:param-header:roundtrip', async ({ transport }: TestArgs) => { expect(headerValue).toBe('us-west1'); // The call succeeded against the validating server (header agreed with - // the body argument, so no -32001 HeaderMismatch on the wire). + // the body argument, so no -32020 HeaderMismatch on the wire). expect(result.isError).toBeFalsy(); expect(result.content).toEqual([{ type: 'text', text: 'region=us-west1' }]); }); @@ -85,7 +85,7 @@ verifies('sep-2243:std-header:mismatch-rejected', async ({ transport }: TestArgs expect(response.status).toBe(400); const body = (await response.json()) as { error: { code: number; message: string } }; - // -32001 is the SEP-2243 HeaderMismatch code at this branch's spec pin. - expect(body.error.code).toBe(-32_001); + // -32020 is the SEP-2243 HeaderMismatch code (post-spec#2907 renumber). + expect(body.error.code).toBe(-32_020); expect(body.error.message).toMatch(/Mcp-Method/); }); diff --git a/test/integration/test/server/createMcpHandler.test.ts b/test/integration/test/server/createMcpHandler.test.ts index 83e3c112c..0cc2f2546 100644 --- a/test/integration/test/server/createMcpHandler.test.ts +++ b/test/integration/test/server/createMcpHandler.test.ts @@ -139,7 +139,7 @@ describe('createMcpHandler over HTTP (legacy postures end to end)', () => { }); expect(response.status).toBe(400); const body = (await response.json()) as { id: unknown; error: { code: number; data: { supported: string[] } } }; - expect(body.error.code).toBe(-32_004); + expect(body.error.code).toBe(-32_022); expect(body.error.data.supported).toEqual([MODERN]); // The rejection echoes the request id it answers (it could be read from the body). expect(body.id).toBe(1); diff --git a/test/integration/test/server/dualEraStdio.test.ts b/test/integration/test/server/dualEraStdio.test.ts index ff74afdfd..091d7aba2 100644 --- a/test/integration/test/server/dualEraStdio.test.ts +++ b/test/integration/test/server/dualEraStdio.test.ts @@ -170,7 +170,7 @@ describe('serveStdio over a real child-process pipe (one connection per spawned params: { protocolVersion: LATEST_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'late', version: '0' } } }); const lateError = (lateInitialize as { error: { code: number; data?: { supported?: string[] } } }).error; - expect(lateError.code).toBe(-32_004); + expect(lateError.code).toBe(-32_022); expect(lateError.data?.supported).toContain(MODERN); } finally { await client.close();