From 4ef7e48b1506750a54e18d8218a166a368356fef Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 29 May 2026 16:38:11 +0800 Subject: [PATCH] fix(client): send MCP standard POST headers --- .changeset/fresh-headers-flow.md | 5 + packages/client/src/client/streamableHttp.ts | 20 ++++ .../client/test/client/streamableHttp.test.ts | 110 ++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 .changeset/fresh-headers-flow.md diff --git a/.changeset/fresh-headers-flow.md b/.changeset/fresh-headers-flow.md new file mode 100644 index 0000000000..c60d39613a --- /dev/null +++ b/.changeset/fresh-headers-flow.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/client": patch +--- + +Send MCP standard headers on Streamable HTTP POST requests. diff --git a/packages/client/src/client/streamableHttp.ts b/packages/client/src/client/streamableHttp.ts index 3b8ddafe5a..b99d31b29e 100644 --- a/packages/client/src/client/streamableHttp.ts +++ b/packages/client/src/client/streamableHttp.ts @@ -163,6 +163,25 @@ export type StreamableHTTPClientTransportOptions = { protocolVersion?: string; }; +function setMcpStandardPostHeaders(headers: Headers, message: JSONRPCMessage | JSONRPCMessage[]): void { + if (Array.isArray(message) || !('method' in message) || typeof message.method !== 'string') { + return; + } + + headers.set('Mcp-Method', message.method); + + const params = 'params' in message ? message.params : undefined; + if (!params || typeof params !== 'object' || Array.isArray(params)) { + return; + } + + const requestParams = params as { name?: unknown; uri?: unknown }; + const mcpName = typeof requestParams.name === 'string' ? requestParams.name : requestParams.uri; + if (typeof mcpName === 'string') { + headers.set('Mcp-Name', mcpName); + } +} + /** * Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. * It will connect to a server using HTTP `POST` for sending messages and HTTP `GET` with Server-Sent Events @@ -545,6 +564,7 @@ export class StreamableHTTPClientTransport implements Transport { const userAccept = headers.get('accept'); const types = [...(userAccept?.split(',').map(s => s.trim().toLowerCase()) ?? []), 'application/json', 'text/event-stream']; headers.set('accept', [...new Set(types)].join(', ')); + setMcpStandardPostHeaders(headers, message); const init = { ...this._requestInit, diff --git a/packages/client/test/client/streamableHttp.test.ts b/packages/client/test/client/streamableHttp.test.ts index 0edf8b75ac..0871edd8ae 100644 --- a/packages/client/test/client/streamableHttp.test.ts +++ b/packages/client/test/client/streamableHttp.test.ts @@ -62,6 +62,112 @@ describe('StreamableHTTPClientTransport', () => { ); }); + it('should include MCP standard headers on POST requests', async () => { + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'read_file', + arguments: { path: 'README.md' } + }, + id: 'test-id' + }; + + (globalThis.fetch as Mock).mockResolvedValueOnce({ + ok: true, + status: 202, + headers: new Headers() + }); + + await transport.send(message); + + const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers; + expect(headers.get('mcp-method')).toBe('tools/call'); + expect(headers.get('mcp-name')).toBe('read_file'); + }); + + it('should use params.uri for the MCP name header', async () => { + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'resources/read', + params: { + uri: 'file:///README.md' + }, + id: 'test-id' + }; + + (globalThis.fetch as Mock).mockResolvedValueOnce({ + ok: true, + status: 202, + headers: new Headers() + }); + + await transport.send(message); + + const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers; + expect(headers.get('mcp-method')).toBe('resources/read'); + expect(headers.get('mcp-name')).toBe('file:///README.md'); + }); + + it('should include the MCP method header for notifications', async () => { + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'notifications/initialized' + }; + + (globalThis.fetch as Mock).mockResolvedValueOnce({ + ok: true, + status: 202, + headers: new Headers() + }); + + await transport.send(message); + + const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers; + expect(headers.get('mcp-method')).toBe('notifications/initialized'); + expect(headers.get('mcp-name')).toBeNull(); + }); + + it('should not include MCP method headers for JSON-RPC responses', async () => { + const message: JSONRPCMessage = { + jsonrpc: '2.0', + id: 'test-id', + result: {} + }; + + (globalThis.fetch as Mock).mockResolvedValueOnce({ + ok: true, + status: 202, + headers: new Headers() + }); + + await transport.send(message); + + const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers; + expect(headers.get('mcp-method')).toBeNull(); + expect(headers.get('mcp-name')).toBeNull(); + }); + + it('should omit the MCP name header when params are absent', async () => { + const message: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'initialize', + id: 'test-id' + }; + + (globalThis.fetch as Mock).mockResolvedValueOnce({ + ok: true, + status: 202, + headers: new Headers() + }); + + await transport.send(message); + + const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers; + expect(headers.get('mcp-method')).toBe('initialize'); + expect(headers.get('mcp-name')).toBeNull(); + }); + it('should send batch messages', async () => { const messages: JSONRPCMessage[] = [ { jsonrpc: '2.0', method: 'test1', params: {}, id: 'id1' }, @@ -85,6 +191,10 @@ describe('StreamableHTTPClientTransport', () => { body: JSON.stringify(messages) }) ); + + const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers; + expect(headers.get('mcp-method')).toBeNull(); + expect(headers.get('mcp-name')).toBeNull(); }); it('should store session ID received during initialization', async () => {