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