diff --git a/.changeset/require-mcp-protocol-version-header.md b/.changeset/require-mcp-protocol-version-header.md new file mode 100644 index 0000000000..71a6c2ef0e --- /dev/null +++ b/.changeset/require-mcp-protocol-version-header.md @@ -0,0 +1,31 @@ +--- +'@modelcontextprotocol/core-internal': patch +'@modelcontextprotocol/server': patch +--- + +Reject a modern (2026-07-28) POST that omits the required `MCP-Protocol-Version` header. + +`createMcpHandler` accepted a request whose body carried a valid per-request `_meta` +envelope but whose `MCP-Protocol-Version` header was absent: the request was classified +modern, dispatched, and answered `200` — tool handlers ran. Only the _mismatch_ case +(header present, disagreeing with the body) was rejected, so of the three standard +headers SEP-2243 requires on every modern POST, presence was enforced for `Mcp-Method` +and `Mcp-Name` but not for `MCP-Protocol-Version`. + +Such a request is now refused with `400 Bad Request` and JSON-RPC `-32020` +(`HeaderMismatch`), matching the shape the sibling missing-header cells already emit and +echoing the request id — per the Streamable HTTP spec, which requires the header on every +POST and lists a missing required standard header as a `HeaderMismatch` failure. The +spec's allowance to treat a header-less request as `2025-03-26` is available only to a +server that also serves pre-2025-06-18 clients, and permits routing it to _legacy_ +handling — never serving it as 2026-07-28; under `legacy: 'reject'` the requirement is +unconditional. + +Era classification is deliberately unchanged and stays body-primary: a proxy that strips +the header still must not change the era, so such a request is still _classified_ modern +and is refused one rung later, at `standard-header-validation` — the same rung that +already answers a missing `Mcp-Method`. Legacy-era traffic is untouched, notifications +are unaffected, and stdio serving (which has no HTTP headers) is not involved. + +Clients built with this SDK always send the header, so no first-party client is affected; +hand-rolled clients that omitted it must add it. diff --git a/docs/migration/support-2026-07-28.md b/docs/migration/support-2026-07-28.md index 13e869c0f2..21c2268fdb 100644 --- a/docs/migration/support-2026-07-28.md +++ b/docs/migration/support-2026-07-28.md @@ -636,7 +636,13 @@ present body value, malformed, or disagree with the body — `400 Bad Request` w JSON-RPC `-32020` (`HeaderMismatch`). The Streamable HTTP transport also emits the `Mcp-Name` standard header on every modern-enveloped request, and `createMcpHandler` validates the SEP-2243 standard headers (`MCP-Protocol-Version`, `Mcp-Method`, -`Mcp-Name`) against the body on the modern path with the same rejection. +`Mcp-Name`) against the body on the modern path with the same rejection — both their +**presence** (all three are required on every modern POST; `Mcp-Name` only for the +methods that mirror `params.name` / `params.uri`) and their agreement with the body. A +modern-enveloped POST that omits `MCP-Protocol-Version` is refused rather than served, +even though the body claim alone still determines the era — so a hand-rolled client that +relied on the body envelope without sending the header must add it. Clients built with +this SDK send all three already. **Modern-era exception** to the `SdkHttpError` mapping: on a modern-enveloped request, an HTTP `400` whose body is a well-formed JSON-RPC error response addressed to the diff --git a/packages/core-internal/src/shared/inboundClassification.ts b/packages/core-internal/src/shared/inboundClassification.ts index 22883cf488..fcad0c9a58 100644 --- a/packages/core-internal/src/shared/inboundClassification.ts +++ b/packages/core-internal/src/shared/inboundClassification.ts @@ -17,9 +17,13 @@ * named revision belongs to (a malformed envelope behind a present claim is * a validation error, never a silent fall back to legacy handling). * - A request without a claim is legacy-era traffic. - * - The `MCP-Protocol-Version` header is a cross-check only: it never - * upgrades or downgrades a body-derived classification, and a disagreement - * between header and body is an explicit ladder outcome. + * - The `MCP-Protocol-Version` header is a cross-check only *for + * classification*: it never upgrades or downgrades a body-derived + * classification, and a disagreement between header and body is an explicit + * ladder outcome. Its absence likewise never changes the era — but the spec + * requires the header on every modern POST, so a modern-classified request + * that omits it is refused one rung later, by + * {@linkcode validateStandardRequestHeaders}, not here. * - Notifications carry no envelope claim of their own under the current * spec, so for notification POSTs without a body claim the modern header is * determinative; the `Mcp-Method` header is validated against the body when @@ -320,10 +324,14 @@ export const INBOUND_VALIDATION_LADDER: readonly InboundValidationRungDescriptor codes: [HEADER_MISMATCH_ERROR_CODE], conformance: ['http-header-validation'], rationale: - 'SEP-2243 standard `Mcp-Method` / `Mcp-Name` headers — presence, sentinel decoding, and `Mcp-Name` ↔ body cross-check ' + - '— are validated by the HTTP entry on a modern-classified request after the supported-revision gate and before ' + - 'dispatch. The classifier’s own header-mismatch cells (protocol-version, `Mcp-Method` mismatch) stay on the edge ' + - '`era-classification` rung; this rung carries the entry-layer presence/`Mcp-Name` half. Evaluated before the ' + + 'SEP-2243 standard `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` headers — presence, sentinel decoding, and ' + + '`Mcp-Name` ↔ body cross-check — are validated by the HTTP entry on a modern-classified request after the ' + + 'supported-revision gate and before dispatch. The spec requires all three on every modern POST and names them in that ' + + 'order, so a request missing several is answered by the earliest. The classifier’s own header-mismatch cells ' + + '(protocol-version, `Mcp-Method` mismatch) stay on the edge ' + + '`era-classification` rung; this rung carries the entry-layer presence/`Mcp-Name` half — including the missing ' + + '`MCP-Protocol-Version` cell, which cannot live on the edge rung without breaking body-primary classification. ' + + 'Evaluated before the ' + 'capability gate, the factory call, and the `Mcp-Param-*` rung so a request that fails several rungs is answered by ' + 'the standard-header rung first. The documented order (after method-registry 5 and request-params 6) is NOT the ' + 'observed precedence: serveModern evaluates this rung immediately after the supported-revision gate, so a request ' + @@ -494,6 +502,10 @@ function stripHttpOws(value: string): string { * `era-classification` rung for the `MCP-Protocol-Version` and * `Mcp-Method` *mismatch* cells) when: * + * - the required `MCP-Protocol-Version` header is absent (SEP-2243 requires it + * on every modern POST, and lists it first among the required standard + * headers — so a request missing it *and* `Mcp-Method` is answered by this + * cell); * - the required `Mcp-Method` header is absent; * - the required `Mcp-Name` header is absent on a `tools/call`, * `prompts/get`, or `resources/read` request whose body carries the @@ -511,7 +523,10 @@ function stripHttpOws(value: string): string { * call to the classifier (no headers passed) keeps routing a modern request * unchanged: the classifier remains a pure body-primary router, and this * function is the presence/`Mcp-Name` half of the standard-header rung the - * entry layers on top. + * entry layers on top. That separation is what lets the missing + * `MCP-Protocol-Version` cell live here without disturbing the body-primary + * rule — classification still resolves from the body (a proxy stripping the + * header must not change the era), and only this rung refuses to serve it. */ export function validateStandardRequestHeaders(request: InboundHttpRequest, route: InboundModernRoute): InboundLadderRejection | undefined { if (route.messageKind !== 'request') { @@ -519,6 +534,24 @@ export function validateStandardRequestHeaders(request: InboundHttpRequest, rout } const method = route.message.method; + // SEP-2243 names `MCP-Protocol-Version` first among the required standard + // headers, so a request missing both it and `Mcp-Method` is answered by + // the header the spec names first. The presence check lives here rather + // than in `classifyInboundRequest` on purpose: classification stays + // body-primary (a proxy stripping the header must not change the era), and + // only this rung refuses to serve the request. + if (request.protocolVersionHeader === undefined) { + const claimed = route.classification.revision; + return crossCheckMismatch( + 'version-header-missing', + '(missing)', + claimed === undefined + ? 'the body carries a modern per-request envelope but the required MCP-Protocol-Version header is absent' + : `the body envelope names protocol version ${claimed} but the required MCP-Protocol-Version header is absent`, + 'standard-header-validation' + ); + } + if (request.mcpMethodHeader === undefined) { return crossCheckMismatch( 'method-header-missing', diff --git a/packages/core-internal/test/shared/inboundLadderCellSheet.test.ts b/packages/core-internal/test/shared/inboundLadderCellSheet.test.ts index ace5d9d2c3..ed9dc914d8 100644 --- a/packages/core-internal/test/shared/inboundLadderCellSheet.test.ts +++ b/packages/core-internal/test/shared/inboundLadderCellSheet.test.ts @@ -76,7 +76,10 @@ const SHEET: readonly SheetRow[] = [ conformance: ['server-stateless'], input: post(enveloped('tools/call', { name: 'echo', arguments: {} })), route: 'modern', - rationale: 'Body-primary classification: a proxy stripping the protocol-version header must not change the era.' + rationale: + 'Body-primary classification: a proxy stripping the protocol-version header must not change the era. This cell pins ' + + 'the CLASSIFICATION only — such a request is still refused before dispatch by the standard-header rung, which requires ' + + 'the header the spec mandates on every modern POST (see validateStandardRequestHeaders / version-header-missing).' }, { cell: 'legacy-claimless-request', diff --git a/packages/core-internal/test/shared/standardHeaderValidation.test.ts b/packages/core-internal/test/shared/standardHeaderValidation.test.ts index 99de31457b..459516f8f1 100644 --- a/packages/core-internal/test/shared/standardHeaderValidation.test.ts +++ b/packages/core-internal/test/shared/standardHeaderValidation.test.ts @@ -4,7 +4,8 @@ * * Evaluated by the HTTP entry on a modern-classified request immediately * after `classifyInboundRequest` returns a modern route: rejects `400` / - * `-32020` (`HeaderMismatch`) when the required `Mcp-Method` header is + * `-32020` (`HeaderMismatch`) when the required `MCP-Protocol-Version` header + * is absent, 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 @@ -60,6 +61,50 @@ function expectRejection(result: InboundLadderRejection | undefined, cell: strin expect(result?.settled).toBe(true); } +describe('SEP-2243 standard-header validation (MCP-Protocol-Version presence)', () => { + test('a modern request without an MCP-Protocol-Version header is rejected (version-header-missing)', () => { + const request: InboundHttpRequest = { + httpMethod: 'POST', + mcpMethodHeader: 'tools/list', + body: { jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: ENVELOPE } } + }; + const outcome = classifyInboundRequest(request); + // Body-primary classification is deliberately untouched: a proxy that + // strips the header must not change the era, so the request still + // routes modern and the rejection belongs to this rung — not to the + // classifier. + expect(outcome.kind).toBe('modern'); + expectRejection(validateStandardRequestHeaders(request, outcome as InboundModernRoute), 'version-header-missing'); + }); + + test('the missing-version cell outranks the missing-method cell (spec header order)', () => { + const request: InboundHttpRequest = { + httpMethod: 'POST', + body: { jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: ENVELOPE } } + }; + const outcome = classifyInboundRequest(request); + expect(outcome.kind).toBe('modern'); + expectRejection(validateStandardRequestHeaders(request, outcome as InboundModernRoute), 'version-header-missing'); + }); + + test('the header/body version mismatch cell stays inside classifyInboundRequest (this rung never sees it)', () => { + // The protocol-version check is split across two rungs: *absence* is + // this rung's cell, *disagreement* stays on the classifier's edge + // `era-classification` rung. Pinned so the split stays observable — + // the same guard the Mcp-Method pair carries below. + const outcome = classifyInboundRequest({ + httpMethod: 'POST', + protocolVersionHeader: '2025-11-25', + mcpMethodHeader: 'tools/list', + body: { jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: ENVELOPE } } + }); + expect(outcome.kind).toBe('reject'); + expect((outcome as InboundLadderRejection).cell).toBe('header-body-version-mismatch'); + expect((outcome as InboundLadderRejection).rung).toBe('era-classification'); + expect((outcome as InboundLadderRejection).code).toBe(-32_020); + }); +}); + describe('SEP-2243 standard-header validation (Mcp-Method presence)', () => { test('a modern request without an Mcp-Method header is rejected (method-header-missing)', () => { const { request, route } = modernPost('tools/list', {}); @@ -86,7 +131,14 @@ describe('SEP-2243 standard-header validation (Mcp-Method presence)', () => { expect((outcome as InboundLadderRejection).cell).toBe('method-header-mismatch'); }); - test('notifications are never enforced', () => { + test('notifications are never enforced — including the MCP-Protocol-Version presence cell', () => { + // Deliberate, not an oversight. The Streamable HTTP "Sending Messages" + // note states that "header requirements for notification POSTs are not + // defined by this revision" (the revision defines no client-to-server + // notifications over Streamable HTTP at all), so the "Every POST + // request MUST include an MCP-Protocol-Version header" rule does not + // reach a posted notification and this rung stays request-only. + // The request below carries NO standard headers whatsoever. const route: InboundModernRoute = { kind: 'modern', messageKind: 'notification', diff --git a/packages/middleware/node/test/toNodeHandler.test.ts b/packages/middleware/node/test/toNodeHandler.test.ts index 3861f3afd2..3f76c53a3d 100644 --- a/packages/middleware/node/test/toNodeHandler.test.ts +++ b/packages/middleware/node/test/toNodeHandler.test.ts @@ -40,8 +40,9 @@ function modernToolsCall(name: string, args: Record): unknown { function bodyDerivedStandardHeaders(body: unknown): Record { if (body === null || typeof body !== 'object' || Array.isArray(body)) return {}; const b = body as { method?: unknown; params?: { name?: unknown; uri?: unknown; _meta?: Record } }; - if (typeof b.params?._meta?.[PROTOCOL_VERSION_META_KEY] !== 'string') return {}; - const out: Record = {}; + const claimedVersion = b.params?._meta?.[PROTOCOL_VERSION_META_KEY]; + if (b.params === undefined || typeof claimedVersion !== 'string') return {}; + const out: Record = { 'mcp-protocol-version': claimedVersion }; if (typeof b.method === 'string') out['mcp-method'] = b.method; const name = b.method === 'resources/read' ? b.params.uri : b.params.name; if (typeof name === 'string') out['mcp-name'] = name; diff --git a/packages/server/src/server/createMcpHandler.ts b/packages/server/src/server/createMcpHandler.ts index a484869944..db495b409c 100644 --- a/packages/server/src/server/createMcpHandler.ts +++ b/packages/server/src/server/createMcpHandler.ts @@ -675,6 +675,7 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa const stdHeaderRejection = validateStandardRequestHeaders( { httpMethod: request.method, + protocolVersionHeader: request.headers.get('mcp-protocol-version') ?? undefined, mcpMethodHeader: request.headers.get('mcp-method') ?? undefined, mcpNameHeader: request.headers.get('mcp-name') ?? undefined }, diff --git a/packages/server/test/server/createMcpHandler.test.ts b/packages/server/test/server/createMcpHandler.test.ts index ded506e57c..a51062b977 100644 --- a/packages/server/test/server/createMcpHandler.test.ts +++ b/packages/server/test/server/createMcpHandler.test.ts @@ -48,8 +48,9 @@ function modernToolsCall(name: string, args: Record, envelope: function bodyDerivedStandardHeaders(body: unknown): Record { if (body === null || typeof body !== 'object' || Array.isArray(body)) return {}; const b = body as { method?: unknown; params?: { name?: unknown; uri?: unknown; _meta?: Record } }; - if (typeof b.params?._meta?.[PROTOCOL_VERSION_META_KEY] !== 'string') return {}; - const out: Record = {}; + const claimedVersion = b.params?._meta?.[PROTOCOL_VERSION_META_KEY]; + if (b.params === undefined || typeof claimedVersion !== 'string') return {}; + const out: Record = { 'mcp-protocol-version': claimedVersion }; if (typeof b.method === 'string') out['mcp-method'] = b.method; const name = b.method === 'resources/read' ? b.params.uri : b.params.name; if (typeof name === 'string') out['mcp-name'] = name; diff --git a/packages/server/test/server/createMcpHandlerCapabilityGate.test.ts b/packages/server/test/server/createMcpHandlerCapabilityGate.test.ts index e6b6bad4e6..6dcfd91b32 100644 --- a/packages/server/test/server/createMcpHandlerCapabilityGate.test.ts +++ b/packages/server/test/server/createMcpHandlerCapabilityGate.test.ts @@ -36,6 +36,7 @@ function postEcho(clientCapabilities: ClientCapabilities): Request { headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', + 'mcp-protocol-version': MODERN_REVISION, 'mcp-method': 'tools/call', 'mcp-name': 'echo' }, diff --git a/packages/server/test/server/createMcpHandlerListen.test.ts b/packages/server/test/server/createMcpHandlerListen.test.ts index 7a0d2c8675..fe17aa7536 100644 --- a/packages/server/test/server/createMcpHandlerListen.test.ts +++ b/packages/server/test/server/createMcpHandlerListen.test.ts @@ -19,8 +19,9 @@ import { createMcpHandler } from '../../src/server/createMcpHandler'; import { McpServer } from '../../src/server/mcp'; import type { ServerEventBus } from '../../src/server/serverEventBus'; +const MODERN_REVISION = '2026-07-28'; const ENVELOPE = { - [PROTOCOL_VERSION_META_KEY]: '2026-07-28', + [PROTOCOL_VERSION_META_KEY]: MODERN_REVISION, [CLIENT_INFO_META_KEY]: { name: 'listen-test-client', version: '1.0.0' }, [CLIENT_CAPABILITIES_META_KEY]: {} }; @@ -31,6 +32,7 @@ function listenRequest(id: string | number, filter: Record): Re headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', + 'mcp-protocol-version': MODERN_REVISION, 'mcp-method': 'subscriptions/listen' }, body: JSON.stringify({ @@ -212,6 +214,7 @@ describe('createMcpHandler — subscriptions/listen', () => { headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', + 'mcp-protocol-version': MODERN_REVISION, 'mcp-method': 'subscriptions/listen' }, body: JSON.stringify({ jsonrpc: '2.0', id: 9, method: 'subscriptions/listen', params: { _meta: ENVELOPE } }) diff --git a/packages/server/test/server/stdHeaderValidation.test.ts b/packages/server/test/server/stdHeaderValidation.test.ts index 21b491f90a..90a3303caa 100644 --- a/packages/server/test/server/stdHeaderValidation.test.ts +++ b/packages/server/test/server/stdHeaderValidation.test.ts @@ -4,7 +4,8 @@ * * The presence and `Mcp-Name` cross-check half of the standard-header rung, * evaluated by the entry on a modern-classified request immediately after the - * body-primary classifier returns a modern route. A missing `Mcp-Method` + * body-primary classifier returns a modern route. A missing + * `MCP-Protocol-Version` header, a missing `Mcp-Method` * 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 @@ -74,6 +75,87 @@ describe('SEP-2243 standard-header validation (createMcpHandler, modern era)', ( expect(body.result.content[0]?.text).toBe('hi'); }); + it('a missing MCP-Protocol-Version header is rejected 400/-32020', async () => { + const handler = createMcpHandler(makeFactory()); + const error = await expectHeaderMismatch( + await handler.fetch( + new Request('http://localhost/mcp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-method': 'tools/list' + }, + body: JSON.stringify({ jsonrpc: '2.0', id: 5, method: 'tools/list', params: { _meta: ENVELOPE } }) + }) + ) + ); + expect(error.message).toContain('MCP-Protocol-Version header is absent'); + }); + + it('a missing MCP-Protocol-Version header is rejected under the strict (legacy: reject) posture', async () => { + // The spec's "MAY treat a header-less request as 2025-03-26" allowance + // is available only to a server that also serves pre-2025-06-18 + // clients; a modern-only endpoint MUST reject. + const handler = createMcpHandler(makeFactory(), { legacy: 'reject' }); + await expectHeaderMismatch( + await handler.fetch( + new Request('http://localhost/mcp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-method': 'tools/list' + }, + body: JSON.stringify({ jsonrpc: '2.0', id: 5, method: 'tools/list', params: { _meta: ENVELOPE } }) + }) + ) + ); + }); + + it('a tools/call missing the MCP-Protocol-Version header never reaches the handler', async () => { + let ran = false; + const handler = createMcpHandler(() => { + const s = new McpServer({ name: 'std-header-server', version: '1.0.0' }); + s.registerTool('echo', { inputSchema: z.object({ text: z.string().optional() }) }, async ({ text }) => { + ran = true; + return { content: [{ type: 'text', text: text ?? 'ok' }] }; + }); + return s; + }); + await expectHeaderMismatch( + await handler.fetch( + new Request('http://localhost/mcp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-method': 'tools/call', + 'mcp-name': 'echo' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 5, + method: 'tools/call', + params: { name: 'echo', arguments: { text: 'ran' }, _meta: ENVELOPE } + }) + }) + ) + ); + expect(ran).toBe(false); + }); + + it('a present-but-disagreeing MCP-Protocol-Version header is still rejected 400/-32020', async () => { + // The absence cell added above must not displace the pre-existing + // disagreement cell, which the classifier still answers on its edge + // `era-classification` rung before serveModern runs this rung at all. + const handler = createMcpHandler(makeFactory()); + const error = await expectHeaderMismatch( + await handler.fetch(modernRequest('tools/list', {}, { 'mcp-protocol-version': '2025-11-25', 'mcp-method': 'tools/list' })) + ); + expect(error.message).toContain('MCP-Protocol-Version header names 2025-11-25'); + }); + 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', {}))); @@ -166,4 +248,16 @@ describe('SEP-2243 standard-header validation is era-gated', () => { // The default 'stateless' legacy posture answers initialize. expect(response.status).toBe(200); }); + + it.each(['GET', 'DELETE'])('a body-less %s is method-routed, never standard-header validated', async httpMethod => { + // The modern era is POST-only, so a body-less session operation never + // reaches a modern route and the presence rung cannot fire on it — + // whatever it lacks in standard headers. It is answered by the + // http-method rung instead: 405 / -32000, never 400 / -32020. + const handler = createMcpHandler(makeFactory()); + const response = await handler.fetch(new Request('http://localhost/mcp', { method: httpMethod })); + expect(response.status).toBe(405); + const body = (await response.json()) as { error: { code: number } }; + expect(body.error.code).toBe(-32_000); + }); }); diff --git a/test/e2e/helpers/index.ts b/test/e2e/helpers/index.ts index 0f3b4daa1c..e72cd7463b 100644 --- a/test/e2e/helpers/index.ts +++ b/test/e2e/helpers/index.ts @@ -356,6 +356,21 @@ export function modernEnvelopeMeta(clientInfo?: Implementation): Record { + return { + 'mcp-protocol-version': MODERN_REVISION, + 'mcp-method': method, + ...(name !== undefined && { 'mcp-name': name }) + }; +} + /** * Fail fast if an entryModern connection did not actually negotiate the * 2026-07-28 revision. Every cell on the arm asserts modern-path behavior, so diff --git a/test/e2e/scenarios/hosting-entry-session.test.ts b/test/e2e/scenarios/hosting-entry-session.test.ts index 0d25ab626b..256955b693 100644 --- a/test/e2e/scenarios/hosting-entry-session.test.ts +++ b/test/e2e/scenarios/hosting-entry-session.test.ts @@ -31,7 +31,7 @@ import { createMcpHandler, isLegacyRequest, McpServer, WebStandardStreamableHTTP import { expect, vi } from 'vitest'; import { z } from 'zod/v4'; -import { modernEnvelopeMeta } from '../helpers/index'; +import { modernEnvelopeMeta, modernStandardHeaders } from '../helpers/index'; import { verifies } from '../helpers/verifies'; const LEGACY = '2025-11-25'; @@ -172,8 +172,7 @@ verifies('typescript:hosting:entry:byo-sessionful-legacy', async () => { headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', - 'mcp-method': 'tools/call', - 'mcp-name': 'greet' + ...modernStandardHeaders('tools/call', 'greet') }, body: JSON.stringify({ jsonrpc: '2.0', diff --git a/test/e2e/scenarios/subscriptions.test.ts b/test/e2e/scenarios/subscriptions.test.ts index 45753afc57..ce644fa430 100644 --- a/test/e2e/scenarios/subscriptions.test.ts +++ b/test/e2e/scenarios/subscriptions.test.ts @@ -11,7 +11,7 @@ import { createMcpHandler, McpServer, SUBSCRIPTION_ID_META_KEY } from '@modelcon import { expect } from 'vitest'; import { z } from 'zod/v4'; -import { modernEnvelopeMeta, wire } from '../helpers/index'; +import { modernEnvelopeMeta, modernStandardHeaders, wire } from '../helpers/index'; import { verifies } from '../helpers/verifies'; import type { TestArgs } from '../types'; @@ -72,7 +72,7 @@ verifies('subscriptions:listen:ack-first-stamped', async () => { headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', - 'mcp-method': 'subscriptions/listen' + ...modernStandardHeaders('subscriptions/listen') }, body: JSON.stringify({ jsonrpc: '2.0', @@ -270,7 +270,7 @@ verifies('subscriptions:listen:capacity-guard', async () => { headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', - 'mcp-method': 'subscriptions/listen' + ...modernStandardHeaders('subscriptions/listen') }, body: JSON.stringify({ jsonrpc: '2.0', diff --git a/test/integration/test/server/createMcpHandler.test.ts b/test/integration/test/server/createMcpHandler.test.ts index 9c2a6a7a0d..d58d3a8e11 100644 --- a/test/integration/test/server/createMcpHandler.test.ts +++ b/test/integration/test/server/createMcpHandler.test.ts @@ -173,6 +173,7 @@ describe('createMcpHandler over HTTP — subscriptions/listen honored filter', ( headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', + 'mcp-protocol-version': MODERN, 'mcp-method': 'subscriptions/listen' }, body: JSON.stringify({