Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/client/src/client/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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');
}

Comment thread
felixweinberger marked this conversation as resolved.
const init = {
...this._requestInit,
Expand Down
66 changes: 66 additions & 0 deletions packages/client/test/client/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading