From 572425e569e51fec008613eb598e63cb74376f0f Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Mon, 8 Dec 2025 14:41:36 +0000 Subject: [PATCH 1/4] feat: allow server to tell transport the protocol version --- src/server/index.ts | 6 ++++++ src/shared/transport.ts | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/server/index.ts b/src/server/index.ts index aa1a62d00..b445182b8 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -442,6 +442,12 @@ export class Server< const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : LATEST_PROTOCOL_VERSION; + // Inform the transport of the negotiated protocol version. + // This allows the transport to validate subsequent request headers. + if (this.transport?.setProtocolVersion) { + this.transport.setProtocolVersion(protocolVersion); + } + return { protocolVersion, capabilities: this.getCapabilities(), diff --git a/src/shared/transport.ts b/src/shared/transport.ts index f9b21bed3..8105384dc 100644 --- a/src/shared/transport.ts +++ b/src/shared/transport.ts @@ -122,7 +122,14 @@ export interface Transport { sessionId?: string; /** - * Sets the protocol version used for the connection (called when the initialize response is received). + * Sets the protocol version after negotiation during initialization. + * This is called by the Server/Client class after the initialize handshake completes. */ setProtocolVersion?: (version: string) => void; + + /** + * Gets the negotiated protocol version, if set. + * Available after initialization completes. + */ + protocolVersion?: string; } From be3db0ed87e8e10709be5388e6270eb75aff84a3 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Mon, 8 Dec 2025 14:51:50 +0000 Subject: [PATCH 2/4] feat: update tests and add protocol get and set to transport --- src/server/streamableHttp.ts | 35 ++++++++++- test/server/streamableHttp.test.ts | 96 ++++++++++++++++-------------- 2 files changed, 84 insertions(+), 47 deletions(-) diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index 35e7f64e7..6e0267503 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -184,6 +184,7 @@ export class StreamableHTTPServerTransport implements Transport { private _allowedOrigins?: string[]; private _enableDnsRebindingProtection: boolean; private _retryInterval?: number; + private _protocolVersion?: string; sessionId?: string; onclose?: () => void; @@ -213,6 +214,21 @@ export class StreamableHTTPServerTransport implements Transport { this._started = true; } + /** + * Sets the protocol version after negotiation during initialization. + * This is called by the Server class after the initialize handshake completes. + */ + setProtocolVersion(version: string): void { + this._protocolVersion = version; + } + + /** + * Gets the negotiated protocol version, if set. + */ + get protocolVersion(): string | undefined { + return this._protocolVersion; + } + /** * Validates request headers for DNS rebinding protection. * @returns Error message if validation fails, undefined if validation passes. @@ -794,19 +810,32 @@ export class StreamableHTTPServerTransport implements Transport { return true; } + /** + * Validates the MCP-Protocol-Version header on incoming requests. + * + * For initialization: Version negotiation handles unknown versions gracefully + * (server responds with its supported version). + * + * For subsequent requests with MCP-Protocol-Version header: + * - Accept if in supported list + * - 400 if unsupported + * + * For HTTP requests without the MCP-Protocol-Version header: + * - Accept and default to the version negotiated at initialization + */ private validateProtocolVersion(req: IncomingMessage, res: ServerResponse): boolean { - let protocolVersion = req.headers['mcp-protocol-version'] ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION; + let protocolVersion = req.headers['mcp-protocol-version']; if (Array.isArray(protocolVersion)) { protocolVersion = protocolVersion[protocolVersion.length - 1]; } - if (!SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) { + if (protocolVersion !== undefined && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) { res.writeHead(400).end( JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, - message: `Bad Request: Unsupported protocol version (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')})` + message: `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')})` }, id: null }) diff --git a/test/server/streamableHttp.test.ts b/test/server/streamableHttp.test.ts index 8d94b272e..118dfdc32 100644 --- a/test/server/streamableHttp.test.ts +++ b/test/server/streamableHttp.test.ts @@ -55,7 +55,20 @@ const TEST_MESSAGES = { method: 'initialize', params: { clientInfo: { name: 'test-client', version: '1.0' }, - protocolVersion: '2025-03-26', + protocolVersion: '2025-11-25', + capabilities: {} + }, + + id: 'init-1' + } as JSONRPCMessage, + + // Initialize message with an older protocol version for backward compatibility tests + initializeOldVersion: { + jsonrpc: '2.0', + method: 'initialize', + params: { + clientInfo: { name: 'test-client', version: '1.0' }, + protocolVersion: '2025-06-18', capabilities: {} }, @@ -93,13 +106,13 @@ async function sendPostRequest( const headers: Record = { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', + // Include protocol version header (best practice per spec) + 'mcp-protocol-version': '2025-11-25', ...extraHeaders }; if (sessionId) { headers['mcp-session-id'] = sessionId; - // After initialization, include the protocol version header - headers['mcp-protocol-version'] = '2025-03-26'; } return fetch(baseUrl, { @@ -460,7 +473,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -501,7 +514,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -533,7 +546,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -545,7 +558,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -564,7 +577,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'application/json', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -758,7 +771,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -796,7 +809,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'DELETE', headers: { 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -815,7 +828,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'DELETE', headers: { 'mcp-session-id': 'invalid-session-id', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -869,15 +882,12 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(response.status).toBe(400); const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version \(supported versions: .+\)/); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version: .+ \(supported versions: .+\)/); }); it('should accept when protocol version differs from negotiated version', async () => { sessionId = await initializeServer(); - // Spy on console.warn to verify warning is logged - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - // Send request with different but supported protocol version const response = await fetch(baseUrl, { method: 'POST', @@ -892,11 +902,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { // Request should still succeed expect(response.status).toBe(200); - - warnSpy.mockRestore(); }); - it('should handle protocol version validation for GET requests', async () => { + it('should reject unsupported protocol version on GET requests', async () => { sessionId = await initializeServer(); // GET request with unsupported protocol version @@ -905,16 +913,16 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': 'invalid-version' + 'mcp-protocol-version': '1999-01-01' // Unsupported version } }); expect(response.status).toBe(400); const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version \(supported versions: .+\)/); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); }); - it('should handle protocol version validation for DELETE requests', async () => { + it('should reject unsupported protocol version on DELETE requests', async () => { sessionId = await initializeServer(); // DELETE request with unsupported protocol version @@ -922,13 +930,13 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'DELETE', headers: { 'mcp-session-id': sessionId, - 'mcp-protocol-version': 'invalid-version' + 'mcp-protocol-version': '1999-01-01' // Unsupported version } }); expect(response.status).toBe(400); const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version \(supported versions: .+\)/); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); }); }); }); @@ -1325,7 +1333,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -1370,7 +1378,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); expect(sseResponse.status).toBe(200); @@ -1404,7 +1412,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26', + 'mcp-protocol-version': '2025-11-25', 'last-event-id': firstEventId } }); @@ -1428,7 +1436,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); expect(sseResponse.status).toBe(200); @@ -1461,7 +1469,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26', + 'mcp-protocol-version': '2025-11-25', 'last-event-id': lastEventId } }); @@ -1565,7 +1573,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'GET', headers: { Accept: 'text/event-stream', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); expect(stream1.status).toBe(200); @@ -1575,7 +1583,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'GET', headers: { Accept: 'text/event-stream', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); expect(stream2.status).toBe(409); // Conflict - only one stream allowed @@ -1692,12 +1700,12 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { baseUrl = result.baseUrl; mcpServer = result.mcpServer; - // Initialize to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + // Initialize with OLD protocol version to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initializeOldVersion); sessionId = initResponse.headers.get('mcp-session-id') as string; expect(sessionId).toBeDefined(); - // Send a tool call request with OLD protocol version + // Send a tool call request with the same OLD protocol version const toolCallRequest: JSONRPCMessage = { jsonrpc: '2.0', id: 100, @@ -1932,12 +1940,12 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { return { content: [{ type: 'text', text: 'Done' }] }; }); - // Initialize to get session ID - const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); + // Initialize with OLD protocol version to get session ID + const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initializeOldVersion); sessionId = initResponse.headers.get('mcp-session-id') as string; expect(sessionId).toBeDefined(); - // Call the tool with OLD protocol version + // Call the tool with the same OLD protocol version const toolCallRequest: JSONRPCMessage = { jsonrpc: '2.0', id: 200, @@ -2009,7 +2017,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { 'Content-Type': 'application/json', Accept: 'text/event-stream, application/json', 'mcp-session-id': sessionId, - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' }, body: JSON.stringify(toolCallRequest) }); @@ -2307,7 +2315,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'DELETE', headers: { 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -2367,7 +2375,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'DELETE', headers: { 'mcp-session-id': 'invalid-session-id', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -2415,7 +2423,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'DELETE', headers: { 'mcp-session-id': sessionId1 || '', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -2428,7 +2436,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'DELETE', headers: { 'mcp-session-id': sessionId2 || '', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -2527,7 +2535,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'DELETE', headers: { 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -2588,7 +2596,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'DELETE', headers: { 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); @@ -2632,7 +2640,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { method: 'DELETE', headers: { 'mcp-session-id': tempSessionId || '', - 'mcp-protocol-version': '2025-03-26' + 'mcp-protocol-version': '2025-11-25' } }); From 24d0034240593595313e6693df25f5510e9abe89 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Mon, 8 Dec 2025 15:26:00 +0000 Subject: [PATCH 3/4] revert some style --- test/server/streamableHttp.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/server/streamableHttp.test.ts b/test/server/streamableHttp.test.ts index 118dfdc32..9fc2d3017 100644 --- a/test/server/streamableHttp.test.ts +++ b/test/server/streamableHttp.test.ts @@ -58,7 +58,6 @@ const TEST_MESSAGES = { protocolVersion: '2025-11-25', capabilities: {} }, - id: 'init-1' } as JSONRPCMessage, @@ -71,7 +70,6 @@ const TEST_MESSAGES = { protocolVersion: '2025-06-18', capabilities: {} }, - id: 'init-1' } as JSONRPCMessage, @@ -106,13 +104,12 @@ async function sendPostRequest( const headers: Record = { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', - // Include protocol version header (best practice per spec) - 'mcp-protocol-version': '2025-11-25', ...extraHeaders }; if (sessionId) { headers['mcp-session-id'] = sessionId; + headers['mcp-protocol-version'] = '2025-11-25'; } return fetch(baseUrl, { From ed64dd1b7d6dd40103c4ffd12d775c3fcff4372a Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Mon, 8 Dec 2025 15:36:28 +0000 Subject: [PATCH 4/4] revert saving protocol --- src/server/index.ts | 6 ------ src/server/streamableHttp.ts | 16 ---------------- src/shared/transport.ts | 9 +-------- 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index b445182b8..aa1a62d00 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -442,12 +442,6 @@ export class Server< const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : LATEST_PROTOCOL_VERSION; - // Inform the transport of the negotiated protocol version. - // This allows the transport to validate subsequent request headers. - if (this.transport?.setProtocolVersion) { - this.transport.setProtocolVersion(protocolVersion); - } - return { protocolVersion, capabilities: this.getCapabilities(), diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index 6e0267503..b9ae5eeb7 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -184,7 +184,6 @@ export class StreamableHTTPServerTransport implements Transport { private _allowedOrigins?: string[]; private _enableDnsRebindingProtection: boolean; private _retryInterval?: number; - private _protocolVersion?: string; sessionId?: string; onclose?: () => void; @@ -214,21 +213,6 @@ export class StreamableHTTPServerTransport implements Transport { this._started = true; } - /** - * Sets the protocol version after negotiation during initialization. - * This is called by the Server class after the initialize handshake completes. - */ - setProtocolVersion(version: string): void { - this._protocolVersion = version; - } - - /** - * Gets the negotiated protocol version, if set. - */ - get protocolVersion(): string | undefined { - return this._protocolVersion; - } - /** * Validates request headers for DNS rebinding protection. * @returns Error message if validation fails, undefined if validation passes. diff --git a/src/shared/transport.ts b/src/shared/transport.ts index 8105384dc..f9b21bed3 100644 --- a/src/shared/transport.ts +++ b/src/shared/transport.ts @@ -122,14 +122,7 @@ export interface Transport { sessionId?: string; /** - * Sets the protocol version after negotiation during initialization. - * This is called by the Server/Client class after the initialize handshake completes. + * Sets the protocol version used for the connection (called when the initialize response is received). */ setProtocolVersion?: (version: string) => void; - - /** - * Gets the negotiated protocol version, if set. - * Available after initialization completes. - */ - protocolVersion?: string; }