From 1e98a97a76f521233656fa406156656a189cc2b7 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sun, 31 May 2026 11:05:31 +0800 Subject: [PATCH] fix(client): let auth headers override request init --- .../fix-client-auth-header-precedence.md | 5 +++++ packages/client/src/client/sse.ts | 4 ++-- packages/client/src/client/streamableHttp.ts | 4 ++-- packages/client/test/client/sse.test.ts | 22 +++++++++++++++++++ .../client/test/client/tokenProvider.test.ts | 22 +++++++++++++++++++ 5 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-client-auth-header-precedence.md diff --git a/.changeset/fix-client-auth-header-precedence.md b/.changeset/fix-client-auth-header-precedence.md new file mode 100644 index 000000000..543bc1b26 --- /dev/null +++ b/.changeset/fix-client-auth-header-precedence.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/client": patch +--- + +Let transport auth headers override configured request headers. diff --git a/packages/client/src/client/sse.ts b/packages/client/src/client/sse.ts index bf554aba2..51d96e777 100644 --- a/packages/client/src/client/sse.ts +++ b/packages/client/src/client/sse.ts @@ -120,8 +120,8 @@ export class SSEClientTransport implements Transport { const extraHeaders = normalizeHeaders(this._requestInit?.headers); return new Headers({ - ...headers, - ...extraHeaders + ...extraHeaders, + ...headers }); } diff --git a/packages/client/src/client/streamableHttp.ts b/packages/client/src/client/streamableHttp.ts index 3b8ddafe5..aca612615 100644 --- a/packages/client/src/client/streamableHttp.ts +++ b/packages/client/src/client/streamableHttp.ts @@ -226,8 +226,8 @@ export class StreamableHTTPClientTransport implements Transport { const extraHeaders = normalizeHeaders(this._requestInit?.headers); return new Headers({ - ...headers, - ...extraHeaders + ...extraHeaders, + ...headers }); } diff --git a/packages/client/test/client/sse.test.ts b/packages/client/test/client/sse.test.ts index 6948d9a4e..81438f823 100644 --- a/packages/client/test/client/sse.test.ts +++ b/packages/client/test/client/sse.test.ts @@ -658,6 +658,28 @@ describe('SSEClientTransport', () => { expect(lastServerRequest.headers['x-custom-header']).toBe('custom-value'); }); + it('lets OAuth tokens override configured Authorization headers', async () => { + mockAuthProvider.tokens.mockResolvedValue({ + access_token: 'fresh-token', + token_type: 'Bearer' + }); + + transport = new SSEClientTransport(resourceBaseUrl, { + authProvider: mockAuthProvider, + requestInit: { + headers: { + Authorization: 'Bearer stale-token', + 'X-Custom-Header': 'custom-value' + } + } + }); + + await transport.start(); + + expect(lastServerRequest.headers.authorization).toBe('Bearer fresh-token'); + expect(lastServerRequest.headers['x-custom-header']).toBe('custom-value'); + }); + it('refreshes expired token during SSE connection', async () => { // Mock tokens() to return expired token until saveTokens is called let currentTokens: OAuthTokens = { diff --git a/packages/client/test/client/tokenProvider.test.ts b/packages/client/test/client/tokenProvider.test.ts index e1108267e..f1cf7e95a 100644 --- a/packages/client/test/client/tokenProvider.test.ts +++ b/packages/client/test/client/tokenProvider.test.ts @@ -34,6 +34,28 @@ describe('StreamableHTTPClientTransport with AuthProvider', () => { expect(init.headers.get('Authorization')).toBe('Bearer my-bearer-token'); }); + it('should let AuthProvider.token() override configured Authorization headers', async () => { + const authProvider: AuthProvider = { token: vi.fn(async () => 'fresh-token') }; + transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), { + authProvider, + requestInit: { + headers: { + Authorization: 'Bearer stale-token', + 'X-Custom-Header': 'custom-value' + } + } + }); + vi.spyOn(globalThis, 'fetch'); + + (globalThis.fetch as Mock).mockResolvedValueOnce({ ok: true, status: 202, headers: new Headers() }); + + await transport.send(message); + + const [, init] = (globalThis.fetch as Mock).mock.calls[0]!; + expect(init.headers.get('Authorization')).toBe('Bearer fresh-token'); + expect(init.headers.get('X-Custom-Header')).toBe('custom-value'); + }); + it('should not set Authorization header when token() returns undefined', async () => { const authProvider: AuthProvider = { token: vi.fn(async () => undefined) }; transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), { authProvider });