From bd6677fde30ea53f4047bf349250fd7397045d49 Mon Sep 17 00:00:00 2001 From: Dhruvil Date: Sat, 16 May 2026 13:00:22 -0400 Subject: [PATCH] fix(server): reject initialize with mismatched MCP-Protocol-Version header When the initial JSON-RPC `initialize` request carries an MCP-Protocol-Version header that disagrees with params.protocolVersion in the body, the Streamable HTTP server silently accepts the request and lets the body win, leaving the client with a session under a version it did not ask for. Add a header-vs-body equality check at the start of the initialize branch in WebStandardStreamableHTTPServerTransport. Header absent stays a soft case (existing behavior). Header present must match body or the request gets a 400. Tests cover both mismatch directions, the matching case, and header-absent (existing behavior preserved). Fixes #2108 --- .../fix-init-protocol-version-mismatch.md | 5 ++ packages/server/src/server/streamableHttp.ts | 15 +++++ .../server/test/server/streamableHttp.test.ts | 62 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 .changeset/fix-init-protocol-version-mismatch.md diff --git a/.changeset/fix-init-protocol-version-mismatch.md b/.changeset/fix-init-protocol-version-mismatch.md new file mode 100644 index 0000000000..d90335f1ef --- /dev/null +++ b/.changeset/fix-init-protocol-version-mismatch.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +fix(server): reject initialize when MCP-Protocol-Version header disagrees with body protocolVersion diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index fd3563a077..0d9fa255cb 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -675,6 +675,21 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { this.onerror?.(new Error('Invalid Request: Only one initialization request is allowed')); return this.createJsonErrorResponse(400, -32_600, 'Invalid Request: Only one initialization request is allowed'); } + // When the MCP-Protocol-Version header is present on initialize, it must agree + // with the body's params.protocolVersion. The spec does not require this check, + // but a silent header/body mismatch lets middleware misconfiguration through + // (the body wins) and routes responses to a version the client never asked for. + // See issue #2108. + const headerProtocolVersion = req.headers.get('mcp-protocol-version'); + if (headerProtocolVersion !== null) { + const initRequest = messages.find(element => isInitializeRequest(element)); + const bodyProtocolVersion = initRequest?.params.protocolVersion; + if (bodyProtocolVersion !== undefined && headerProtocolVersion !== bodyProtocolVersion) { + const error = `Bad Request: MCP-Protocol-Version header (${headerProtocolVersion}) does not match initialize body protocolVersion (${bodyProtocolVersion})`; + this.onerror?.(new Error(error)); + return this.createJsonErrorResponse(400, -32_600, error); + } + } this.sessionId = this.sessionIdGenerator?.(); this._initialized = true; diff --git a/packages/server/test/server/streamableHttp.test.ts b/packages/server/test/server/streamableHttp.test.ts index 7a23dd56bb..5355fb084d 100644 --- a/packages/server/test/server/streamableHttp.test.ts +++ b/packages/server/test/server/streamableHttp.test.ts @@ -752,6 +752,68 @@ describe('Zod v4', () => { const errorData = await response.json(); expectErrorResponse(errorData, -32_000, /Unsupported protocol version/); }); + + it('should reject initialize when MCP-Protocol-Version header disagrees with body (header older)', async () => { + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-protocol-version': '2025-03-26' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + const response = await transport.handleRequest(request); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32_600, /MCP-Protocol-Version header.*does not match initialize body protocolVersion/); + }); + + it('should reject initialize when MCP-Protocol-Version header disagrees with body (header newer)', async () => { + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-protocol-version': '2025-11-25' + }, + body: JSON.stringify(TEST_MESSAGES.initializeOldVersion) + }); + + const response = await transport.handleRequest(request); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32_600, /MCP-Protocol-Version header.*does not match initialize body protocolVersion/); + }); + + it('should accept initialize when MCP-Protocol-Version header matches body', async () => { + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-protocol-version': '2025-11-25' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + expect(response.headers.get('mcp-session-id')).toBeDefined(); + }); + + it('should accept initialize when MCP-Protocol-Version header is absent (existing behavior)', async () => { + const request = createRequest('POST', TEST_MESSAGES.initialize); + + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + expect(response.headers.get('mcp-session-id')).toBeDefined(); + }); }); describe('HTTPServerTransport - start() method', () => {