Skip to content

Commit 1e98a97

Browse files
committed
fix(client): let auth headers override request init
1 parent 16d13ab commit 1e98a97

5 files changed

Lines changed: 53 additions & 4 deletions

File tree

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 transport auth headers override configured request headers.

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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,28 @@ describe('SSEClientTransport', () => {
658658
expect(lastServerRequest.headers['x-custom-header']).toBe('custom-value');
659659
});
660660

661+
it('lets OAuth tokens override configured Authorization headers', async () => {
662+
mockAuthProvider.tokens.mockResolvedValue({
663+
access_token: 'fresh-token',
664+
token_type: 'Bearer'
665+
});
666+
667+
transport = new SSEClientTransport(resourceBaseUrl, {
668+
authProvider: mockAuthProvider,
669+
requestInit: {
670+
headers: {
671+
Authorization: 'Bearer stale-token',
672+
'X-Custom-Header': 'custom-value'
673+
}
674+
}
675+
});
676+
677+
await transport.start();
678+
679+
expect(lastServerRequest.headers.authorization).toBe('Bearer fresh-token');
680+
expect(lastServerRequest.headers['x-custom-header']).toBe('custom-value');
681+
});
682+
661683
it('refreshes expired token during SSE connection', async () => {
662684
// Mock tokens() to return expired token until saveTokens is called
663685
let currentTokens: OAuthTokens = {

packages/client/test/client/tokenProvider.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ describe('StreamableHTTPClientTransport with AuthProvider', () => {
3434
expect(init.headers.get('Authorization')).toBe('Bearer my-bearer-token');
3535
});
3636

37+
it('should let AuthProvider.token() override configured Authorization headers', async () => {
38+
const authProvider: AuthProvider = { token: vi.fn(async () => 'fresh-token') };
39+
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
40+
authProvider,
41+
requestInit: {
42+
headers: {
43+
Authorization: 'Bearer stale-token',
44+
'X-Custom-Header': 'custom-value'
45+
}
46+
}
47+
});
48+
vi.spyOn(globalThis, 'fetch');
49+
50+
(globalThis.fetch as Mock).mockResolvedValueOnce({ ok: true, status: 202, headers: new Headers() });
51+
52+
await transport.send(message);
53+
54+
const [, init] = (globalThis.fetch as Mock).mock.calls[0]!;
55+
expect(init.headers.get('Authorization')).toBe('Bearer fresh-token');
56+
expect(init.headers.get('X-Custom-Header')).toBe('custom-value');
57+
});
58+
3759
it('should not set Authorization header when token() returns undefined', async () => {
3860
const authProvider: AuthProvider = { token: vi.fn(async () => undefined) };
3961
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), { authProvider });

0 commit comments

Comments
 (0)