From 5f5180c4d32f1c53cd93f036f5ce3632cc064e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Barthelet?= Date: Sun, 25 May 2025 17:20:02 -0700 Subject: [PATCH 01/16] Register completions capabilities --- src/server/mcp.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 5b864b8b..38c869c7 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -236,6 +236,10 @@ export class McpServer { CompleteRequestSchema.shape.method.value, ); + this.server.registerCapabilities({ + completions: {}, + }); + this.server.setRequestHandler( CompleteRequestSchema, async (request): Promise => { From 269c8caf8ec7a248693bc54f22e013ac6259fc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Barthelet?= Date: Sun, 25 May 2025 18:00:28 -0700 Subject: [PATCH 02/16] Remove server-specific capabilities definition from Client constructor in mcp tests --- src/server/mcp.test.ts | 271 +++++++++++------------------------------ 1 file changed, 72 insertions(+), 199 deletions(-) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 49f852d6..bef8ae02 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -913,18 +913,10 @@ describe("tool()", () => { name: "test server", version: "1.0", }); - - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - tools: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); mcpServer.tool( "test", @@ -1056,17 +1048,10 @@ describe("tool()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - tools: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); // Register a tool with outputSchema mcpServer.registerTool( @@ -1169,17 +1154,10 @@ describe("tool()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - tools: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); // Register a tool with outputSchema that returns only content without structuredContent mcpServer.registerTool( @@ -1233,17 +1211,10 @@ describe("tool()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - tools: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); // Register a tool with outputSchema that returns invalid data mcpServer.registerTool( @@ -1308,17 +1279,10 @@ describe("tool()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - tools: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); let receivedSessionId: string | undefined; mcpServer.tool("test-tool", async (extra) => { @@ -1364,17 +1328,10 @@ describe("tool()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - tools: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); let receivedRequestId: string | number | undefined; mcpServer.tool("request-id-test", async (extra) => { @@ -1423,17 +1380,10 @@ describe("tool()", () => { { capabilities: { logging: {} } }, ); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - tools: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); let receivedLogMessage: string | undefined; const loggingMessage = "hello here is log message 1"; @@ -1480,17 +1430,10 @@ describe("tool()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - tools: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); mcpServer.tool( "test", @@ -1546,17 +1489,10 @@ describe("tool()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - tools: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); mcpServer.tool("error-test", async () => { throw new Error("Tool execution failed"); @@ -1598,17 +1534,10 @@ describe("tool()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - tools: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); mcpServer.tool("test-tool", async () => ({ content: [ @@ -2401,17 +2330,10 @@ describe("resource()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - resources: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); mcpServer.resource( "test", @@ -2469,17 +2391,10 @@ describe("resource()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - resources: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); mcpServer.resource( "test", @@ -2540,17 +2455,10 @@ describe("resource()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - resources: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); let receivedRequestId: string | number | undefined; mcpServer.resource("request-id-test", "test://resource", async (_uri, extra) => { @@ -3052,17 +2960,10 @@ describe("prompt()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - prompts: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); mcpServer.prompt( "test", @@ -3258,17 +3159,10 @@ describe("prompt()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - prompts: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); mcpServer.prompt("test-prompt", async () => ({ messages: [ @@ -3312,17 +3206,10 @@ describe("prompt()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - prompts: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); mcpServer.prompt( "test-prompt", @@ -3380,17 +3267,10 @@ describe("prompt()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - prompts: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); mcpServer.prompt( "test-prompt", @@ -3450,17 +3330,10 @@ describe("prompt()", () => { version: "1.0", }); - const client = new Client( - { - name: "test client", - version: "1.0", - }, - { - capabilities: { - prompts: {}, - }, - }, - ); + const client = new Client({ + name: "test client", + version: "1.0", + }); let receivedRequestId: string | number | undefined; mcpServer.prompt("request-id-test", async (extra) => { From 7e0521550b3bf21a5fe043d7ee62aacc0f8a8c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Barthelet?= Date: Sun, 25 May 2025 19:07:26 -0700 Subject: [PATCH 03/16] Add tests for completions capability registration on template resource with completion or prompt completable argument registration --- src/server/mcp.test.ts | 85 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index bef8ae02..0ba1998d 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -2321,6 +2321,48 @@ describe("resource()", () => { ).rejects.toThrow(/Resource test:\/\/nonexistent not found/); }); + /*** + * Test: Registering a resource template with a complete callback should update server capabilities to advertise support for completion + */ + test("should advertise support for completion when a resource template with a complete callback is defined", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + const client = new Client({ + name: "test client", + version: "1.0", + }); + + mcpServer.resource( + "test", + new ResourceTemplate("test://resource/{category}", { + list: undefined, + complete: { + category: () => ["books", "movies", "music"], + }, + }), + async () => ({ + contents: [ + { + uri: "test://resource/test", + text: "Test content", + }, + ], + }), + ); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + expect(client.getServerCapabilities()).toMatchObject({ completions: {} }) + }) + /*** * Test: Resource Template Parameter Completion */ @@ -3197,6 +3239,49 @@ describe("prompt()", () => { ).rejects.toThrow(/Prompt nonexistent-prompt not found/); }); + + /*** + * Test: Registering a prompt with a completable argument should update server capabilities to advertise support for completion + */ + test("should advertise support for completion when a prompt with a completable argument is defined", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + const client = new Client({ + name: "test client", + version: "1.0", + }); + + mcpServer.prompt( + "test-prompt", + { + name: completable(z.string(), () => ["Alice", "Bob", "Charlie"]), + }, + async ({ name }) => ({ + messages: [ + { + role: "assistant", + content: { + type: "text", + text: `Hello ${name}`, + }, + }, + ], + }), + ); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + expect(client.getServerCapabilities()).toMatchObject({ completions: {} }) + }) + /*** * Test: Prompt Argument Completion */ From 49777d565265424726af6b653adc99b592dbfd9b Mon Sep 17 00:00:00 2001 From: PapacyDai Date: Thu, 29 May 2025 15:15:52 +0800 Subject: [PATCH 04/16] fix: Ensure _meta object is not lost when onprogress option is passed When the onprogress option is passed, the _meta object in the request params was being overwritten, causing any existing _meta content to be lost. --- src/shared/protocol.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 4694929d..98f8e1c9 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -541,7 +541,10 @@ export abstract class Protocol< this._progressHandlers.set(messageId, options.onprogress); jsonrpcRequest.params = { ...request.params, - _meta: { progressToken: messageId }, + _meta: { + ...(request.params._meta || {}), + progressToken: messageId + }, }; } From cdcb00975cbcfbec6b3fdaa2a5cedbff048332a3 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Thu, 29 May 2025 14:59:47 +0100 Subject: [PATCH 05/16] fix build and add tests --- src/shared/protocol.test.ts | 126 ++++++++++++++++++++++++++++++++++++ src/shared/protocol.ts | 2 +- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/shared/protocol.test.ts b/src/shared/protocol.test.ts index e0141da1..05bc8f3b 100644 --- a/src/shared/protocol.test.ts +++ b/src/shared/protocol.test.ts @@ -27,9 +27,11 @@ class MockTransport implements Transport { describe("protocol tests", () => { let protocol: Protocol; let transport: MockTransport; + let sendSpy: jest.SpyInstance; beforeEach(() => { transport = new MockTransport(); + sendSpy = jest.spyOn(transport, 'send'); protocol = new (class extends Protocol { protected assertCapabilityForMethod(): void {} protected assertNotificationCapability(): void {} @@ -63,6 +65,130 @@ describe("protocol tests", () => { expect(oncloseMock).toHaveBeenCalled(); }); + describe("_meta preservation with onprogress", () => { + test("should preserve existing _meta when adding progressToken", async () => { + await protocol.connect(transport); + const request = { + method: "example", + params: { + data: "test", + _meta: { + customField: "customValue", + anotherField: 123 + } + } + }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string(), + }); + const onProgressMock = jest.fn(); + + protocol.request(request, mockSchema, { + onprogress: onProgressMock, + }); + + expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ + method: "example", + params: { + data: "test", + _meta: { + customField: "customValue", + anotherField: 123, + progressToken: expect.any(Number) + } + }, + jsonrpc: "2.0", + id: expect.any(Number) + }), expect.any(Object)); + }); + + test("should create _meta with progressToken when no _meta exists", async () => { + await protocol.connect(transport); + const request = { + method: "example", + params: { + data: "test" + } + }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string(), + }); + const onProgressMock = jest.fn(); + + protocol.request(request, mockSchema, { + onprogress: onProgressMock, + }); + + expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ + method: "example", + params: { + data: "test", + _meta: { + progressToken: expect.any(Number) + } + }, + jsonrpc: "2.0", + id: expect.any(Number) + }), expect.any(Object)); + }); + + test("should not modify _meta when onprogress is not provided", async () => { + await protocol.connect(transport); + const request = { + method: "example", + params: { + data: "test", + _meta: { + customField: "customValue" + } + } + }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string(), + }); + + protocol.request(request, mockSchema); + + expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ + method: "example", + params: { + data: "test", + _meta: { + customField: "customValue" + } + }, + jsonrpc: "2.0", + id: expect.any(Number) + }), expect.any(Object)); + }); + + test("should handle params being undefined with onprogress", async () => { + await protocol.connect(transport); + const request = { + method: "example" + }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string(), + }); + const onProgressMock = jest.fn(); + + protocol.request(request, mockSchema, { + onprogress: onProgressMock, + }); + + expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ + method: "example", + params: { + _meta: { + progressToken: expect.any(Number) + } + }, + jsonrpc: "2.0", + id: expect.any(Number) + }), expect.any(Object)); + }); + }); + describe("progress notification timeout behavior", () => { beforeEach(() => { jest.useFakeTimers(); diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 98f8e1c9..a04f26eb 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -542,7 +542,7 @@ export abstract class Protocol< jsonrpcRequest.params = { ...request.params, _meta: { - ...(request.params._meta || {}), + ...(request.params?._meta || {}), progressToken: messageId }, }; From d46d4882e6168ec242d7355f7e50f511f6453172 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Thu, 29 May 2025 15:39:24 +0100 Subject: [PATCH 06/16] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 467800fc..fb1dd3cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.12.0", + "version": "1.12.1", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 6cf034595dbfb58c2061a370a49da4078e429d1e Mon Sep 17 00:00:00 2001 From: Carl Peaslee Date: Fri, 30 May 2025 11:32:10 -0700 Subject: [PATCH 07/16] updates listed resources from resource templates to favor their own metadata --- src/server/mcp.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 5b864b8b..65ed1b8f 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -369,8 +369,9 @@ export class McpServer { const result = await template.resourceTemplate.listCallback(extra); for (const resource of result.resources) { templateResources.push({ - ...resource, ...template.metadata, + // the defined resource metadata should override the template metadata if present + ...resource, }); } } From 1878143f1a8ffc3ce5f7acd4dc61cef67b589b4b Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Thu, 5 Jun 2025 11:52:59 -0400 Subject: [PATCH 08/16] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb1dd3cf..764ce2cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.12.1", + "version": "1.12.2", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 84971c83ea19f6481f72e4e489f101c8a2055458 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 10 Jun 2025 14:07:10 +0100 Subject: [PATCH 09/16] Set Mcp-Protocol-Version in client requests after init, and warn when it doesn't match negotiated version (it SHOULD match) --- src/client/index.ts | 2 + src/client/sse.ts | 14 +- src/client/streamableHttp.ts | 6 +- .../stateManagementStreamableHttp.test.ts | 21 +++ src/server/index.ts | 11 +- src/server/sse.ts | 1 + src/server/streamableHttp.test.ts | 145 +++++++++++++++++- src/server/streamableHttp.ts | 55 ++++++- src/shared/transport.ts | 7 + 9 files changed, 243 insertions(+), 19 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index 98618a17..0e7d6cf4 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -165,6 +165,8 @@ export class Client< this._serverCapabilities = result.capabilities; this._serverVersion = result.serverInfo; + // HTTP transports must set the protocol version in each header after initialization. + transport.protocolVersion = result.protocolVersion; this._instructions = result.instructions; diff --git a/src/client/sse.ts b/src/client/sse.ts index 7939e8cb..a80e06eb 100644 --- a/src/client/sse.ts +++ b/src/client/sse.ts @@ -62,6 +62,7 @@ export class SSEClientTransport implements Transport { private _eventSourceInit?: EventSourceInit; private _requestInit?: RequestInit; private _authProvider?: OAuthClientProvider; + protocolVersion?: string; onclose?: () => void; onerror?: (error: Error) => void; @@ -99,13 +100,18 @@ export class SSEClientTransport implements Transport { } private async _commonHeaders(): Promise { - const headers: HeadersInit = { ...this._requestInit?.headers }; + const headers = { + ...this._requestInit?.headers, + } as HeadersInit & Record; if (this._authProvider) { const tokens = await this._authProvider.tokens(); if (tokens) { - (headers as Record)["Authorization"] = `Bearer ${tokens.access_token}`; + headers["Authorization"] = `Bearer ${tokens.access_token}`; } } + if (this.protocolVersion) { + headers["mcp-protocol-version"] = this.protocolVersion; + } return headers; } @@ -214,7 +220,9 @@ export class SSEClientTransport implements Transport { try { const commonHeaders = await this._commonHeaders(); - const headers = new Headers({ ...commonHeaders, ...this._requestInit?.headers }); + // Note: this._requestInit?.headers already set in _commonHeaders + // const headers = new Headers({ ...commonHeaders, ...this._requestInit?.headers }); + const headers = new Headers(commonHeaders); headers.set("content-type", "application/json"); const init = { ...this._requestInit, diff --git a/src/client/streamableHttp.ts b/src/client/streamableHttp.ts index 1bcfbb2d..1bc2724e 100644 --- a/src/client/streamableHttp.ts +++ b/src/client/streamableHttp.ts @@ -124,6 +124,7 @@ export class StreamableHTTPClientTransport implements Transport { private _authProvider?: OAuthClientProvider; private _sessionId?: string; private _reconnectionOptions: StreamableHTTPReconnectionOptions; + protocolVersion?: string; onclose?: () => void; onerror?: (error: Error) => void; @@ -162,7 +163,7 @@ export class StreamableHTTPClientTransport implements Transport { } private async _commonHeaders(): Promise { - const headers: HeadersInit = {}; + const headers: HeadersInit & Record = {}; if (this._authProvider) { const tokens = await this._authProvider.tokens(); if (tokens) { @@ -173,6 +174,9 @@ export class StreamableHTTPClientTransport implements Transport { if (this._sessionId) { headers["mcp-session-id"] = this._sessionId; } + if (this.protocolVersion != null) { + headers["mcp-protocol-version"] = this.protocolVersion; + } return new Headers( { ...headers, ...this._requestInit?.headers } diff --git a/src/integration-tests/stateManagementStreamableHttp.test.ts b/src/integration-tests/stateManagementStreamableHttp.test.ts index b7ff17e6..503be80a 100644 --- a/src/integration-tests/stateManagementStreamableHttp.test.ts +++ b/src/integration-tests/stateManagementStreamableHttp.test.ts @@ -211,6 +211,27 @@ describe('Streamable HTTP Transport Session Management', () => { // Clean up await transport.close(); }); + + it('should set protocol version after connecting', async () => { + // Create and connect a client + const client = new Client({ + name: 'test-client', + version: '1.0.0' + }); + + const transport = new StreamableHTTPClientTransport(baseUrl); + + // Verify protocol version is not set before connecting + expect(transport.protocolVersion).toBeUndefined(); + + await client.connect(transport); + + // Verify protocol version is set after connecting + expect(transport.protocolVersion).toBe('2025-03-26'); + + // Clean up + await transport.close(); + }); }); describe('Stateful Mode', () => { diff --git a/src/server/index.ts b/src/server/index.ts index 3901099e..d05c5489 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -251,10 +251,15 @@ export class Server< this._clientCapabilities = request.params.capabilities; this._clientVersion = request.params.clientInfo; - return { - protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) + const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion - : LATEST_PROTOCOL_VERSION, + : LATEST_PROTOCOL_VERSION; + if (this.transport) { + this.transport.protocolVersion = protocolVersion; + } + + return { + protocolVersion, capabilities: this.getCapabilities(), serverInfo: this._serverInfo, ...(this._instructions && { instructions: this._instructions }), diff --git a/src/server/sse.ts b/src/server/sse.ts index 03f6fefc..b2f85ce0 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -18,6 +18,7 @@ export class SSEServerTransport implements Transport { private _sseResponse?: ServerResponse; private _sessionId: string; + protocolVersion?: string; onclose?: () => void; onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void; diff --git a/src/server/streamableHttp.test.ts b/src/server/streamableHttp.test.ts index b961f6c4..51cc4c47 100644 --- a/src/server/streamableHttp.test.ts +++ b/src/server/streamableHttp.test.ts @@ -185,6 +185,8 @@ async function sendPostRequest(baseUrl: URL, message: JSONRPCMessage | JSONRPCMe if (sessionId) { headers["mcp-session-id"] = sessionId; + // After initialization, include the protocol version header + headers["mcp-protocol-version"] = "2025-03-26"; } return fetch(baseUrl, { @@ -277,7 +279,7 @@ describe("StreamableHTTPServerTransport", () => { expectErrorResponse(errorData, -32600, /Only one initialization request is allowed/); }); - it("should pandle post requests via sse response correctly", async () => { + it("should handle post requests via sse response correctly", async () => { sessionId = await initializeServer(); const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, sessionId); @@ -376,6 +378,7 @@ describe("StreamableHTTPServerTransport", () => { headers: { Accept: "text/event-stream", "mcp-session-id": sessionId, + "mcp-protocol-version": "2025-03-26", }, }); @@ -417,6 +420,7 @@ describe("StreamableHTTPServerTransport", () => { headers: { Accept: "text/event-stream", "mcp-session-id": sessionId, + "mcp-protocol-version": "2025-03-26", }, }); @@ -448,6 +452,7 @@ describe("StreamableHTTPServerTransport", () => { headers: { Accept: "text/event-stream", "mcp-session-id": sessionId, + "mcp-protocol-version": "2025-03-26", }, }); @@ -459,6 +464,7 @@ describe("StreamableHTTPServerTransport", () => { headers: { Accept: "text/event-stream", "mcp-session-id": sessionId, + "mcp-protocol-version": "2025-03-26", }, }); @@ -477,6 +483,7 @@ describe("StreamableHTTPServerTransport", () => { headers: { Accept: "application/json", "mcp-session-id": sessionId, + "mcp-protocol-version": "2025-03-26", }, }); @@ -670,6 +677,7 @@ describe("StreamableHTTPServerTransport", () => { headers: { Accept: "text/event-stream", "mcp-session-id": sessionId, + "mcp-protocol-version": "2025-03-26", }, }); @@ -705,7 +713,10 @@ describe("StreamableHTTPServerTransport", () => { // Now DELETE the session const deleteResponse = await fetch(tempUrl, { method: "DELETE", - headers: { "mcp-session-id": tempSessionId || "" }, + headers: { + "mcp-session-id": tempSessionId || "", + "mcp-protocol-version": "2025-03-26", + }, }); expect(deleteResponse.status).toBe(200); @@ -721,13 +732,129 @@ describe("StreamableHTTPServerTransport", () => { // Try to delete with invalid session ID const response = await fetch(baseUrl, { method: "DELETE", - headers: { "mcp-session-id": "invalid-session-id" }, + headers: { + "mcp-session-id": "invalid-session-id", + "mcp-protocol-version": "2025-03-26", + }, }); expect(response.status).toBe(404); const errorData = await response.json(); expectErrorResponse(errorData, -32001, /Session not found/); }); + + describe("protocol version header validation", () => { + it("should accept requests with matching protocol version", async () => { + sessionId = await initializeServer(); + + // Send request with matching protocol version + const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, sessionId); + + expect(response.status).toBe(200); + }); + + it("should accept requests without protocol version header", async () => { + sessionId = await initializeServer(); + + // Send request without protocol version header + const response = await fetch(baseUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + "mcp-session-id": sessionId, + // No mcp-protocol-version header + }, + body: JSON.stringify(TEST_MESSAGES.toolsList), + }); + + expect(response.status).toBe(200); + }); + + it("should reject requests with unsupported protocol version", async () => { + sessionId = await initializeServer(); + + // Send request with unsupported protocol version + const response = await fetch(baseUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + "mcp-session-id": sessionId, + "mcp-protocol-version": "1999-01-01", // Unsupported version + }, + body: JSON.stringify(TEST_MESSAGES.toolsList), + }); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); + }); + + it("should accept but warn when protocol version differs from negotiated version", async () => { + sessionId = await initializeServer(); + + // Spy on console.warn to verify warning is logged + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + // Send request with different but supported protocol version + const response = await fetch(baseUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + "mcp-session-id": sessionId, + "mcp-protocol-version": "2024-11-05", // Different but supported version + }, + body: JSON.stringify(TEST_MESSAGES.toolsList), + }); + + // Request should still succeed + expect(response.status).toBe(200); + + // But warning should have been logged + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("Request has header with protocol version 2024-11-05, but version previously negotiated is 2025-03-26") + ); + + warnSpy.mockRestore(); + }); + + it("should handle protocol version validation for GET requests", async () => { + sessionId = await initializeServer(); + + // GET request with unsupported protocol version + const response = await fetch(baseUrl, { + method: "GET", + headers: { + Accept: "text/event-stream", + "mcp-session-id": sessionId, + "mcp-protocol-version": "invalid-version", + }, + }); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); + }); + + it("should handle protocol version validation for DELETE requests", async () => { + sessionId = await initializeServer(); + + // DELETE request with unsupported protocol version + const response = await fetch(baseUrl, { + method: "DELETE", + headers: { + "mcp-session-id": sessionId, + "mcp-protocol-version": "invalid-version", + }, + }); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); + }); + }); }); describe("StreamableHTTPServerTransport with AuthInfo", () => { @@ -1120,6 +1247,7 @@ describe("StreamableHTTPServerTransport with resumability", () => { headers: { Accept: "text/event-stream", "mcp-session-id": sessionId, + "mcp-protocol-version": "2025-03-26", }, }); @@ -1196,6 +1324,7 @@ describe("StreamableHTTPServerTransport with resumability", () => { headers: { Accept: "text/event-stream", "mcp-session-id": sessionId, + "mcp-protocol-version": "2025-03-26", "last-event-id": firstEventId }, }); @@ -1282,14 +1411,20 @@ describe("StreamableHTTPServerTransport in stateless mode", () => { // Open first SSE stream const stream1 = await fetch(baseUrl, { method: "GET", - headers: { Accept: "text/event-stream" }, + headers: { + Accept: "text/event-stream", + "mcp-protocol-version": "2025-03-26" + }, }); expect(stream1.status).toBe(200); // Open second SSE stream - should still be rejected, stateless mode still only allows one const stream2 = await fetch(baseUrl, { method: "GET", - headers: { Accept: "text/event-stream" }, + headers: { + Accept: "text/event-stream", + "mcp-protocol-version": "2025-03-26" + }, }); expect(stream2.status).toBe(409); // Conflict - only one stream allowed }); diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index dc99c306..b387f2ff 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -1,6 +1,6 @@ import { IncomingMessage, ServerResponse } from "node:http"; import { Transport } from "../shared/transport.js"; -import { isInitializeRequest, isJSONRPCError, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema, RequestId } from "../types.js"; +import { isInitializeRequest, isJSONRPCError, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema, RequestId, SUPPORTED_PROTOCOL_VERSIONS } from "../types.js"; import getRawBody from "raw-body"; import contentType from "content-type"; import { randomUUID } from "node:crypto"; @@ -110,7 +110,8 @@ export class StreamableHTTPServerTransport implements Transport { private _eventStore?: EventStore; private _onsessioninitialized?: (sessionId: string) => void; - sessionId?: string | undefined; + sessionId?: string; + protocolVersion?: string; onclose?: () => void; onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void; @@ -172,6 +173,9 @@ export class StreamableHTTPServerTransport implements Transport { if (!this.validateSession(req, res)) { return; } + if (!this.validateProtocolVersion(req, res)) { + return; + } // Handle resumability: check for Last-Event-ID header if (this._eventStore) { const lastEventId = req.headers['last-event-id'] as string | undefined; @@ -378,11 +382,17 @@ export class StreamableHTTPServerTransport implements Transport { } } - // If an Mcp-Session-Id is returned by the server during initialization, - // clients using the Streamable HTTP transport MUST include it - // in the Mcp-Session-Id header on all of their subsequent HTTP requests. - if (!isInitializationRequest && !this.validateSession(req, res)) { - return; + if (!isInitializationRequest) { + // If an Mcp-Session-Id is returned by the server during initialization, + // clients using the Streamable HTTP transport MUST include it + // in the Mcp-Session-Id header on all of their subsequent HTTP requests. + if (!this.validateSession(req, res)) { + return; + } + // Mcp-Protocol-Version header is required for all requests after initialization. + if (!this.validateProtocolVersion(req, res)) { + return; + } } @@ -457,6 +467,9 @@ export class StreamableHTTPServerTransport implements Transport { if (!this.validateSession(req, res)) { return; } + if (!this.validateProtocolVersion(req, res)) { + return; + } await this.close(); res.writeHead(200).end(); } @@ -524,6 +537,34 @@ export class StreamableHTTPServerTransport implements Transport { return true; } + private validateProtocolVersion(req: IncomingMessage, res: ServerResponse): boolean { + let protocolVersion = req.headers["mcp-protocol-version"]; + if (Array.isArray(protocolVersion)) { + protocolVersion = protocolVersion[protocolVersion.length - 1]; + } + + if (protocolVersion == null || protocolVersion === undefined) { + // If the protocol version is not set, we assume the client supports the implicit protocol version + return true; + } + + protocolVersion = String(protocolVersion).trim(); + if (this.protocolVersion !== undefined && this.protocolVersion !== protocolVersion) { + console.warn(`Request has header with protocol version ${protocolVersion}, but version previously negotiated is ${this.protocolVersion}.`); + } + if (!SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) { + res.writeHead(400).end(JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32000, + message: 'Bad Request: Unsupported protocol version' + }, + id: null + })); + return false; + } + return true; + } async close(): Promise { // Close all SSE connections diff --git a/src/shared/transport.ts b/src/shared/transport.ts index fe0a60e6..976be79d 100644 --- a/src/shared/transport.ts +++ b/src/shared/transport.ts @@ -75,4 +75,11 @@ export interface Transport { * The session ID generated for this connection. */ sessionId?: string; + + /** + * The protocol version used for the connection. + * + * Only set after the initialize response was received. + */ + protocolVersion?: string; } From bb0592bd50935ca487e3f08568b9e4d7313a7c64 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 12 Jun 2025 18:42:51 +0100 Subject: [PATCH 10/16] add supported versions to error message --- src/server/streamableHttp.test.ts | 6 +++--- src/server/streamableHttp.ts | 15 +++++---------- src/types.ts | 1 + 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/server/streamableHttp.test.ts b/src/server/streamableHttp.test.ts index 51cc4c47..f00721e2 100644 --- a/src/server/streamableHttp.test.ts +++ b/src/server/streamableHttp.test.ts @@ -788,7 +788,7 @@ describe("StreamableHTTPServerTransport", () => { expect(response.status).toBe(400); const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version \(supported versions: .+\)/); }); it("should accept but warn when protocol version differs from negotiated version", async () => { @@ -835,7 +835,7 @@ describe("StreamableHTTPServerTransport", () => { expect(response.status).toBe(400); const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version \(supported versions: .+\)/); }); it("should handle protocol version validation for DELETE requests", async () => { @@ -852,7 +852,7 @@ describe("StreamableHTTPServerTransport", () => { expect(response.status).toBe(400); const errorData = await response.json(); - expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/); + expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version \(supported versions: .+\)/); }); }); }); diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index b387f2ff..c6010fd3 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -1,6 +1,6 @@ import { IncomingMessage, ServerResponse } from "node:http"; import { Transport } from "../shared/transport.js"; -import { isInitializeRequest, isJSONRPCError, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema, RequestId, SUPPORTED_PROTOCOL_VERSIONS } from "../types.js"; +import { isInitializeRequest, isJSONRPCError, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema, RequestId, SUPPORTED_PROTOCOL_VERSIONS, DEFAULT_NEGOTIATED_PROTOCOL_VERSION } from "../types.js"; import getRawBody from "raw-body"; import contentType from "content-type"; import { randomUUID } from "node:crypto"; @@ -538,17 +538,12 @@ export class StreamableHTTPServerTransport implements Transport { } private validateProtocolVersion(req: IncomingMessage, res: ServerResponse): boolean { - let protocolVersion = req.headers["mcp-protocol-version"]; + let protocolVersion = req.headers["mcp-protocol-version"] ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION; if (Array.isArray(protocolVersion)) { protocolVersion = protocolVersion[protocolVersion.length - 1]; } - - if (protocolVersion == null || protocolVersion === undefined) { - // If the protocol version is not set, we assume the client supports the implicit protocol version - return true; - } - - protocolVersion = String(protocolVersion).trim(); + + if (this.protocolVersion !== undefined && this.protocolVersion !== protocolVersion) { console.warn(`Request has header with protocol version ${protocolVersion}, but version previously negotiated is ${this.protocolVersion}.`); } @@ -557,7 +552,7 @@ export class StreamableHTTPServerTransport implements Transport { jsonrpc: "2.0", error: { code: -32000, - message: 'Bad Request: Unsupported protocol version' + message: `Bad Request: Unsupported protocol version (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")})` }, id: null })); diff --git a/src/types.ts b/src/types.ts index ae25848e..4bf87ef6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import { z, ZodTypeAny } from "zod"; export const LATEST_PROTOCOL_VERSION = "2025-03-26"; +export const DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26"; export const SUPPORTED_PROTOCOL_VERSIONS = [ LATEST_PROTOCOL_VERSION, "2024-11-05", From a28e25a9c0c4e4bd3ee1ead292c30b1238d9d057 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 12 Jun 2025 18:43:06 +0100 Subject: [PATCH 11/16] use const in test --- src/integration-tests/stateManagementStreamableHttp.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integration-tests/stateManagementStreamableHttp.test.ts b/src/integration-tests/stateManagementStreamableHttp.test.ts index 503be80a..d12a4f99 100644 --- a/src/integration-tests/stateManagementStreamableHttp.test.ts +++ b/src/integration-tests/stateManagementStreamableHttp.test.ts @@ -5,7 +5,7 @@ import { Client } from '../client/index.js'; import { StreamableHTTPClientTransport } from '../client/streamableHttp.js'; import { McpServer } from '../server/mcp.js'; import { StreamableHTTPServerTransport } from '../server/streamableHttp.js'; -import { CallToolResultSchema, ListToolsResultSchema, ListResourcesResultSchema, ListPromptsResultSchema } from '../types.js'; +import { CallToolResultSchema, ListToolsResultSchema, ListResourcesResultSchema, ListPromptsResultSchema, DEFAULT_NEGOTIATED_PROTOCOL_VERSION } from '../types.js'; import { z } from 'zod'; describe('Streamable HTTP Transport Session Management', () => { @@ -227,7 +227,7 @@ describe('Streamable HTTP Transport Session Management', () => { await client.connect(transport); // Verify protocol version is set after connecting - expect(transport.protocolVersion).toBe('2025-03-26'); + expect(transport.protocolVersion).toBe(DEFAULT_NEGOTIATED_PROTOCOL_VERSION); // Clean up await transport.close(); From e573a0f342f92b24250d860bc0f3d2a6aa55c53e Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 12 Jun 2025 18:43:49 +0100 Subject: [PATCH 12/16] nits: remove dead comment & align test style on local idioms --- src/client/sse.ts | 2 -- src/client/streamableHttp.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client/sse.ts b/src/client/sse.ts index a80e06eb..3111cd9d 100644 --- a/src/client/sse.ts +++ b/src/client/sse.ts @@ -220,8 +220,6 @@ export class SSEClientTransport implements Transport { try { const commonHeaders = await this._commonHeaders(); - // Note: this._requestInit?.headers already set in _commonHeaders - // const headers = new Headers({ ...commonHeaders, ...this._requestInit?.headers }); const headers = new Headers(commonHeaders); headers.set("content-type", "application/json"); const init = { diff --git a/src/client/streamableHttp.ts b/src/client/streamableHttp.ts index 1bc2724e..2a1895f5 100644 --- a/src/client/streamableHttp.ts +++ b/src/client/streamableHttp.ts @@ -174,7 +174,7 @@ export class StreamableHTTPClientTransport implements Transport { if (this._sessionId) { headers["mcp-session-id"] = this._sessionId; } - if (this.protocolVersion != null) { + if (this.protocolVersion) { headers["mcp-protocol-version"] = this.protocolVersion; } From bc5312991ebf47d5133b22d04e9a56cb3b9a3f15 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Thu, 12 Jun 2025 18:47:16 +0100 Subject: [PATCH 13/16] adding tests to prevent regressions --- src/server/mcp.test.ts | 140 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 49f852d6..b7bfdffb 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -3499,4 +3499,144 @@ describe("prompt()", () => { expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); expect(result.messages[0].content.text).toContain("Received request ID:"); }); + + /*** + * Test: Resource Template Metadata Priority + */ + test("should prioritize individual resource metadata over template metadata", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + const client = new Client({ + name: "test client", + version: "1.0", + }); + + mcpServer.resource( + "test", + new ResourceTemplate("test://resource/{id}", { + list: async () => ({ + resources: [ + { + name: "Resource 1", + uri: "test://resource/1", + description: "Individual resource description", + mimeType: "text/plain", + }, + { + name: "Resource 2", + uri: "test://resource/2", + // This resource has no description or mimeType + }, + ], + }), + }), + { + description: "Template description", + mimeType: "application/json", + }, + async (uri) => ({ + contents: [ + { + uri: uri.href, + text: "Test content", + }, + ], + }), + ); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + const result = await client.request( + { + method: "resources/list", + }, + ListResourcesResultSchema, + ); + + expect(result.resources).toHaveLength(2); + + // Resource 1 should have its own metadata + expect(result.resources[0].name).toBe("Resource 1"); + expect(result.resources[0].description).toBe("Individual resource description"); + expect(result.resources[0].mimeType).toBe("text/plain"); + + // Resource 2 should inherit template metadata + expect(result.resources[1].name).toBe("Resource 2"); + expect(result.resources[1].description).toBe("Template description"); + expect(result.resources[1].mimeType).toBe("application/json"); + }); + + /*** + * Test: Resource Template Metadata Overrides All Fields + */ + test("should allow resource to override all template metadata fields", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + const client = new Client({ + name: "test client", + version: "1.0", + }); + + mcpServer.resource( + "test", + new ResourceTemplate("test://resource/{id}", { + list: async () => ({ + resources: [ + { + name: "Overridden Name", + uri: "test://resource/1", + description: "Overridden description", + mimeType: "text/markdown", + // Add any other metadata fields if they exist + }, + ], + }), + }), + { + name: "Template Name", + description: "Template description", + mimeType: "application/json", + }, + async (uri) => ({ + contents: [ + { + uri: uri.href, + text: "Test content", + }, + ], + }), + ); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + const result = await client.request( + { + method: "resources/list", + }, + ListResourcesResultSchema, + ); + + expect(result.resources).toHaveLength(1); + + // All fields should be from the individual resource, not the template + expect(result.resources[0].name).toBe("Overridden Name"); + expect(result.resources[0].description).toBe("Overridden description"); + expect(result.resources[0].mimeType).toBe("text/markdown"); + }); }); From ea6d97d5abe21fb147d49ede26eafbeee4e89ca1 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Thu, 12 Jun 2025 19:21:45 +0100 Subject: [PATCH 14/16] 1.12.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 764ce2cb..6b184f31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.12.2", + "version": "1.12.3", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 4aaa26d900d452b61165cc440776b8e960b88a99 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 13 Jun 2025 13:35:38 +0100 Subject: [PATCH 15/16] remove protocol version mismatch warning --- src/server/streamableHttp.test.ts | 7 +------ src/server/streamableHttp.ts | 4 ---- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/server/streamableHttp.test.ts b/src/server/streamableHttp.test.ts index f00721e2..d66083fe 100644 --- a/src/server/streamableHttp.test.ts +++ b/src/server/streamableHttp.test.ts @@ -791,7 +791,7 @@ describe("StreamableHTTPServerTransport", () => { expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version \(supported versions: .+\)/); }); - it("should accept but warn when protocol version differs from negotiated version", async () => { + it("should accept when protocol version differs from negotiated version", async () => { sessionId = await initializeServer(); // Spy on console.warn to verify warning is logged @@ -812,11 +812,6 @@ describe("StreamableHTTPServerTransport", () => { // Request should still succeed expect(response.status).toBe(200); - // But warning should have been logged - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining("Request has header with protocol version 2024-11-05, but version previously negotiated is 2025-03-26") - ); - warnSpy.mockRestore(); }); diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index c6010fd3..62503120 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -543,10 +543,6 @@ export class StreamableHTTPServerTransport implements Transport { protocolVersion = protocolVersion[protocolVersion.length - 1]; } - - if (this.protocolVersion !== undefined && this.protocolVersion !== protocolVersion) { - console.warn(`Request has header with protocol version ${protocolVersion}, but version previously negotiated is ${this.protocolVersion}.`); - } if (!SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) { res.writeHead(400).end(JSON.stringify({ jsonrpc: "2.0", From 33257f14d4d4e8b09b20f58126ab1fb8ef9e05ab Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 13 Jun 2025 13:36:04 +0100 Subject: [PATCH 16/16] switch to optional Transport.setProtocolVersion --- src/client/index.ts | 4 +++- src/client/sse.ts | 10 +++++++--- src/client/streamableHttp.ts | 13 ++++++++++--- src/server/index.ts | 3 --- src/server/sse.ts | 2 -- src/server/streamableHttp.ts | 1 - src/shared/transport.ts | 6 ++---- 7 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index 0e7d6cf4..a8fbdcee 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -166,7 +166,9 @@ export class Client< this._serverCapabilities = result.capabilities; this._serverVersion = result.serverInfo; // HTTP transports must set the protocol version in each header after initialization. - transport.protocolVersion = result.protocolVersion; + if (transport.setProtocolVersion) { + transport.setProtocolVersion(result.protocolVersion); + } this._instructions = result.instructions; diff --git a/src/client/sse.ts b/src/client/sse.ts index 3111cd9d..5aa99abb 100644 --- a/src/client/sse.ts +++ b/src/client/sse.ts @@ -62,7 +62,7 @@ export class SSEClientTransport implements Transport { private _eventSourceInit?: EventSourceInit; private _requestInit?: RequestInit; private _authProvider?: OAuthClientProvider; - protocolVersion?: string; + private _protocolVersion?: string; onclose?: () => void; onerror?: (error: Error) => void; @@ -109,8 +109,8 @@ export class SSEClientTransport implements Transport { headers["Authorization"] = `Bearer ${tokens.access_token}`; } } - if (this.protocolVersion) { - headers["mcp-protocol-version"] = this.protocolVersion; + if (this._protocolVersion) { + headers["mcp-protocol-version"] = this._protocolVersion; } return headers; @@ -255,4 +255,8 @@ export class SSEClientTransport implements Transport { throw error; } } + + setProtocolVersion(version: string): void { + this._protocolVersion = version; + } } diff --git a/src/client/streamableHttp.ts b/src/client/streamableHttp.ts index 2a1895f5..4117bb1b 100644 --- a/src/client/streamableHttp.ts +++ b/src/client/streamableHttp.ts @@ -124,7 +124,7 @@ export class StreamableHTTPClientTransport implements Transport { private _authProvider?: OAuthClientProvider; private _sessionId?: string; private _reconnectionOptions: StreamableHTTPReconnectionOptions; - protocolVersion?: string; + private _protocolVersion?: string; onclose?: () => void; onerror?: (error: Error) => void; @@ -174,8 +174,8 @@ export class StreamableHTTPClientTransport implements Transport { if (this._sessionId) { headers["mcp-session-id"] = this._sessionId; } - if (this.protocolVersion) { - headers["mcp-protocol-version"] = this.protocolVersion; + if (this._protocolVersion) { + headers["mcp-protocol-version"] = this._protocolVersion; } return new Headers( @@ -520,4 +520,11 @@ export class StreamableHTTPClientTransport implements Transport { throw error; } } + + setProtocolVersion(version: string): void { + this._protocolVersion = version; + } + get protocolVersion(): string | undefined { + return this._protocolVersion; + } } diff --git a/src/server/index.ts b/src/server/index.ts index d05c5489..caf72f9c 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -254,9 +254,6 @@ export class Server< const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : LATEST_PROTOCOL_VERSION; - if (this.transport) { - this.transport.protocolVersion = protocolVersion; - } return { protocolVersion, diff --git a/src/server/sse.ts b/src/server/sse.ts index b2f85ce0..e9a4d53a 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -17,8 +17,6 @@ const MAXIMUM_MESSAGE_SIZE = "4mb"; export class SSEServerTransport implements Transport { private _sseResponse?: ServerResponse; private _sessionId: string; - - protocolVersion?: string; onclose?: () => void; onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void; diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index 62503120..34b2ab68 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -111,7 +111,6 @@ export class StreamableHTTPServerTransport implements Transport { private _onsessioninitialized?: (sessionId: string) => void; sessionId?: string; - protocolVersion?: string; onclose?: () => void; onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void; diff --git a/src/shared/transport.ts b/src/shared/transport.ts index 976be79d..b75e072e 100644 --- a/src/shared/transport.ts +++ b/src/shared/transport.ts @@ -77,9 +77,7 @@ export interface Transport { sessionId?: string; /** - * The protocol version used for the connection. - * - * Only set after the initialize response was received. + * Sets the protocol version used for the connection (called when the initialize response is received). */ - protocolVersion?: string; + setProtocolVersion?: (version: string) => void; }