Skip to content

Commit 226c482

Browse files
committed
fix(client): let auth headers override request headers
1 parent ab552c3 commit 226c482

5 files changed

Lines changed: 78 additions & 5 deletions

File tree

.changeset/fresh-buses-auth.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/client': patch
3+
---
4+
5+
Let auth provider headers override requestInit headers in client transports.

packages/client/src/client/sse.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ export class SSEClientTransport implements Transport {
120120
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
121121

122122
return new Headers({
123-
...headers,
124-
...extraHeaders
123+
...extraHeaders,
124+
...headers
125125
});
126126
}
127127

packages/client/src/client/streamableHttp.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,8 @@ export class StreamableHTTPClientTransport implements Transport {
226226
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
227227

228228
return new Headers({
229-
...headers,
230-
...extraHeaders
229+
...extraHeaders,
230+
...headers
231231
});
232232
}
233233

packages/client/test/client/sse.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,44 @@ describe('SSEClientTransport', () => {
344344
}
345345
});
346346

347+
it('lets auth provider headers override custom Authorization headers', async () => {
348+
const authProvider: AuthProvider = {
349+
token: async () => 'fresh-token'
350+
};
351+
352+
transport = new SSEClientTransport(resourceBaseUrl, {
353+
authProvider,
354+
requestInit: {
355+
headers: {
356+
Authorization: 'Bearer stale-token',
357+
'X-Custom-Header': 'custom-value'
358+
}
359+
}
360+
});
361+
362+
await transport.start();
363+
364+
const originalFetch = globalThis.fetch;
365+
try {
366+
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true });
367+
368+
const message: JSONRPCMessage = {
369+
jsonrpc: '2.0',
370+
id: '1',
371+
method: 'test',
372+
params: {}
373+
};
374+
375+
await transport.send(message);
376+
377+
const calledHeaders = (globalThis.fetch as Mock).mock.calls[0]![1].headers;
378+
expect(calledHeaders.get('Authorization')).toBe('Bearer fresh-token');
379+
expect(calledHeaders.get('X-Custom-Header')).toBe('custom-value');
380+
} finally {
381+
globalThis.fetch = originalFetch;
382+
}
383+
});
384+
347385
it('passes custom headers to fetch requests (Headers class)', async () => {
348386
const customHeaders = new Headers({
349387
Authorization: 'Bearer test-token',

packages/client/test/client/streamableHttp.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core'
22
import { OAuthError, OAuthErrorCode, SdkErrorCode, SdkHttpError } from '@modelcontextprotocol/core';
33
import type { Mock, Mocked } from 'vitest';
44

5-
import type { OAuthClientProvider } from '../../src/client/auth.js';
5+
import type { AuthProvider, OAuthClientProvider } from '../../src/client/auth.js';
66
import { UnauthorizedError } from '../../src/client/auth.js';
77
import type { ReconnectionScheduler, StartSSEOptions, StreamableHTTPReconnectionOptions } from '../../src/client/streamableHttp.js';
88
import { StreamableHTTPClientTransport } from '../../src/client/streamableHttp.js';
@@ -573,6 +573,36 @@ describe('StreamableHTTPClientTransport', () => {
573573
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
574574
});
575575

576+
it('should let auth provider headers override custom Authorization headers', async () => {
577+
const authProvider: AuthProvider = {
578+
token: async () => 'fresh-token'
579+
};
580+
581+
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
582+
authProvider,
583+
requestInit: {
584+
headers: {
585+
Authorization: 'Bearer stale-token',
586+
'X-Custom-Header': 'CustomValue'
587+
}
588+
}
589+
});
590+
591+
let actualReqInit: RequestInit = {};
592+
593+
(globalThis.fetch as Mock).mockImplementation(async (_url, reqInit) => {
594+
actualReqInit = reqInit;
595+
return new Response(null, { status: 200, headers: { 'content-type': 'text/event-stream' } });
596+
});
597+
598+
await transport.start();
599+
600+
await transport['_startOrAuthSse']({});
601+
602+
expect((actualReqInit.headers as Headers).get('authorization')).toBe('Bearer fresh-token');
603+
expect((actualReqInit.headers as Headers).get('x-custom-header')).toBe('CustomValue');
604+
});
605+
576606
it('should always send specified custom headers (Headers class)', async () => {
577607
const requestInit = {
578608
headers: new Headers({

0 commit comments

Comments
 (0)