From 21b050f4c4db295469d0a32fb88d05e64a9e72cf Mon Sep 17 00:00:00 2001 From: Niels Kaspers Date: Tue, 10 Mar 2026 09:16:08 +0200 Subject: [PATCH] fix(client): preserve custom Accept headers in StreamableHTTPClientTransport The SDK unconditionally overwrites custom Accept headers provided via requestInit.headers with its own defaults. This prevents users from configuring MCP servers that require specific Accept header values. Only set default Accept headers when the user has not provided one. Fixes #1646 --- packages/client/src/client/streamableHttp.ts | 8 ++- .../client/test/client/streamableHttp.test.ts | 66 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/client/src/client/streamableHttp.ts b/packages/client/src/client/streamableHttp.ts index dab9b37ab..8db816936 100644 --- a/packages/client/src/client/streamableHttp.ts +++ b/packages/client/src/client/streamableHttp.ts @@ -214,7 +214,9 @@ export class StreamableHTTPClientTransport implements Transport { // Try to open an initial SSE stream with GET to listen for server messages // This is optional according to the spec - server may not support it const headers = await this._commonHeaders(); - headers.set('Accept', 'text/event-stream'); + if (!headers.has('accept')) { + headers.set('accept', 'text/event-stream'); + } // Include Last-Event-ID header for resumable streams if provided if (resumptionToken) { @@ -473,7 +475,9 @@ export class StreamableHTTPClientTransport implements Transport { const headers = await this._commonHeaders(); headers.set('content-type', 'application/json'); - headers.set('accept', 'application/json, text/event-stream'); + if (!headers.has('accept')) { + headers.set('accept', 'application/json, text/event-stream'); + } const init = { ...this._requestInit, diff --git a/packages/client/test/client/streamableHttp.test.ts b/packages/client/test/client/streamableHttp.test.ts index 0398964d3..b6288cdaa 100644 --- a/packages/client/test/client/streamableHttp.test.ts +++ b/packages/client/test/client/streamableHttp.test.ts @@ -601,6 +601,72 @@ describe('StreamableHTTPClientTransport', () => { expect((actualReqInit.headers as Headers).get('x-custom-header')).toBe('CustomValue'); }); + it('should preserve custom Accept header on POST requests', async () => { + transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), { + requestInit: { + headers: { + Accept: 'application/vnd.example.v1+json' + } + } + }); + + let actualReqInit: RequestInit = {}; + + (globalThis.fetch as Mock).mockImplementation(async (_url, reqInit) => { + actualReqInit = reqInit; + return new Response(JSON.stringify({ jsonrpc: '2.0', result: {} }), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + }); + + await transport.start(); + + await transport.send({ jsonrpc: '2.0', method: 'test', params: {} } as JSONRPCMessage); + expect((actualReqInit.headers as Headers).get('accept')).toBe('application/vnd.example.v1+json'); + }); + + it('should preserve custom Accept header on GET SSE requests', async () => { + transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), { + requestInit: { + headers: { + Accept: 'text/event-stream, application/json' + } + } + }); + + let actualReqInit: RequestInit = {}; + + (globalThis.fetch as Mock).mockImplementation(async (_url, reqInit) => { + actualReqInit = reqInit; + return new Response(null, { status: 200, headers: { 'content-type': 'text/event-stream' } }); + }); + + await transport.start(); + + await transport['_startOrAuthSse']({}); + expect((actualReqInit.headers as Headers).get('accept')).toBe('text/event-stream, application/json'); + }); + + it('should set default Accept header when none provided', async () => { + transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp')); + + let actualReqInit: RequestInit = {}; + + (globalThis.fetch as Mock).mockImplementation(async (_url, reqInit) => { + actualReqInit = reqInit; + return new Response(JSON.stringify({ jsonrpc: '2.0', result: {} }), { + status: 200, + headers: { 'content-type': 'application/json' } + }); + }); + + await transport.start(); + + await transport.send({ jsonrpc: '2.0', method: 'test', params: {} } as JSONRPCMessage); + expect((actualReqInit.headers as Headers).get('accept')).toBe('application/json, text/event-stream'); + }); + it('should have exponential backoff with configurable maxRetries', () => { // This test verifies the maxRetries and backoff calculation directly