fix(mcp): isolate OAuth request headers

This commit is contained in:
Aiden Cline 2026-06-24 16:27:01 -05:00
commit 5823deb7c8
5 changed files with 396 additions and 35 deletions

View file

@ -1,5 +1,5 @@
diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts
index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
index 6f567a193626587a2730b5a49293ca5dfd4181ea..5b7c841c000508e389ce617f559f7c2a5126ca9f 100644
--- a/dist/cjs/client/index.d.ts
+++ b/dist/cjs/client/index.d.ts
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
@ -7,25 +7,12 @@ index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d01
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
*/
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
[x: string]: unknown;
content: ({
diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts
index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
--- a/dist/esm/client/index.d.ts
+++ b/dist/esm/client/index.d.ts
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
*
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
*/
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
[x: string]: unknown;
content: ({
diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js
index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 100644
index 6ac1da14dc7f6211ae70f7711c124b76098816d8..88e58b90b673cb0f9c60920edec7d2ebb16f112b 100644
--- a/dist/cjs/client/index.js
+++ b/dist/cjs/client/index.js
@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol {
@ -112,11 +99,67 @@ index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c1
/**
* After initialization has completed, this will be populated with the server's reported capabilities.
*/
diff --git a/dist/cjs/client/sse.js b/dist/cjs/client/sse.js
index 8ca8dad652e76c299927f836cf622c0be877e1d4..cfbb08438a4e219fcd98d8d4ba8949079f3fcbe5 100644
--- a/dist/cjs/client/sse.js
+++ b/dist/cjs/client/sse.js
@@ -27,7 +27,8 @@ class SSEClientTransport {
this._requestInit = opts?.requestInit;
this._authProvider = opts?.authProvider;
this._fetch = opts?.fetch;
- this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, opts?.requestInit);
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
+ this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, oauthRequestInit);
}
async _authThenStart() {
if (!this._authProvider) {
@@ -63,10 +64,11 @@ class SSEClientTransport {
headers['mcp-protocol-version'] = this._protocolVersion;
}
const extraHeaders = (0, transport_js_1.normalizeHeaders)(this._requestInit?.headers);
- return new Headers({
- ...headers,
- ...extraHeaders
- });
+ const result = new Headers(extraHeaders);
+ for (const [name, value] of Object.entries(headers)) {
+ result.set(name, value);
+ }
+ return result;
}
_startOrAuth() {
const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch);
diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js
index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644
index a29a7d3a0f14d9cd800ef5b296485237350c666f..f253f88023c10d44e6cdb74bcf2ffa722619fd0d 100644
--- a/dist/cjs/client/streamableHttp.js
+++ b/dist/cjs/client/streamableHttp.js
@@ -290,7 +290,38 @@ class StreamableHTTPClientTransport {
@@ -33,7 +33,8 @@ class StreamableHTTPClientTransport {
this._requestInit = opts?.requestInit;
this._authProvider = opts?.authProvider;
this._fetch = opts?.fetch;
- this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, opts?.requestInit);
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
+ this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, oauthRequestInit);
this._sessionId = opts?.sessionId;
this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;
}
@@ -74,10 +75,11 @@ class StreamableHTTPClientTransport {
headers['mcp-protocol-version'] = this._protocolVersion;
}
const extraHeaders = (0, transport_js_1.normalizeHeaders)(this._requestInit?.headers);
- return new Headers({
- ...headers,
- ...extraHeaders
- });
+ const result = new Headers(extraHeaders);
+ for (const [name, value] of Object.entries(headers)) {
+ result.set(name, value);
+ }
+ return result;
}
async _startOrAuthSse(options) {
const { resumptionToken } = options;
@@ -290,7 +292,38 @@ class StreamableHTTPClientTransport {
this.onclose?.();
}
async send(message, options) {
@ -155,7 +198,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
const { resumptionToken, onresumptiontoken } = options || {};
if (resumptionToken) {
// If we have at last event ID, we need to reconnect the SSE stream
@@ -298,6 +329,7 @@ class StreamableHTTPClientTransport {
@@ -298,6 +331,7 @@ class StreamableHTTPClientTransport {
return;
}
const headers = await this._commonHeaders();
@ -163,7 +206,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
headers.set('content-type', 'application/json');
headers.set('accept', 'application/json, text/event-stream');
const init = {
@@ -310,11 +342,20 @@ class StreamableHTTPClientTransport {
@@ -310,11 +344,20 @@ class StreamableHTTPClientTransport {
const response = await (this._fetch ?? fetch)(this._url, init);
// Handle session ID received during initialization
const sessionId = response.headers.get('mcp-session-id');
@ -185,7 +228,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
if (response.status === 401 && this._authProvider) {
// Prevent infinite recursion when server returns 401 after successful auth
if (this._hasCompletedAuthFlow) {
@@ -335,7 +376,7 @@ class StreamableHTTPClientTransport {
@@ -335,7 +378,7 @@ class StreamableHTTPClientTransport {
// Mark that we completed auth flow
this._hasCompletedAuthFlow = true;
// Purposely _not_ awaited, so we don't call onerror twice
@ -194,7 +237,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
}
if (response.status === 403 && this._authProvider) {
const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response);
@@ -362,7 +403,7 @@ class StreamableHTTPClientTransport {
@@ -362,7 +405,7 @@ class StreamableHTTPClientTransport {
if (result !== 'AUTHORIZED') {
throw new auth_js_1.UnauthorizedError();
}
@ -204,7 +247,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
}
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js
index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb77538182b 100644
index 3617e787f0ba70447c99501aee7aa67584d89758..4ee4d158391558fdc1f977f5134b7cacfc45e8c3 100644
--- a/dist/cjs/shared/protocol.js
+++ b/dist/cjs/shared/protocol.js
@@ -744,7 +744,12 @@ class Protocol {
@ -221,8 +264,21 @@ index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb7
this._cleanupTimeout(messageId);
reject(error);
});
diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts
index 6f567a193626587a2730b5a49293ca5dfd4181ea..5b7c841c000508e389ce617f559f7c2a5126ca9f 100644
--- a/dist/esm/client/index.d.ts
+++ b/dist/esm/client/index.d.ts
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
*
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
*/
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
[x: string]: unknown;
content: ({
diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js
index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 100644
index 49b12c6cd918c457420fef7ad5528a9443d1a191..339153cb7e9299b7a9bdec0e56e41cedf425fb31 100644
--- a/dist/esm/client/index.js
+++ b/dist/esm/client/index.js
@@ -284,41 +284,16 @@ export class Client extends Protocol {
@ -309,8 +365,38 @@ index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8e
/**
* After initialization has completed, this will be populated with the server's reported capabilities.
*/
diff --git a/dist/esm/client/sse.js b/dist/esm/client/sse.js
index 58c474156ba4b33090ac092be6f3284e695a7ffd..ca088b48eeb04a9b65863d9a6dcd3bd11c4a8f71 100644
--- a/dist/esm/client/sse.js
+++ b/dist/esm/client/sse.js
@@ -23,7 +23,8 @@ export class SSEClientTransport {
this._requestInit = opts?.requestInit;
this._authProvider = opts?.authProvider;
this._fetch = opts?.fetch;
- this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
+ this._fetchWithInit = createFetchWithInit(opts?.fetch, oauthRequestInit);
}
async _authThenStart() {
if (!this._authProvider) {
@@ -59,10 +60,11 @@ export class SSEClientTransport {
headers['mcp-protocol-version'] = this._protocolVersion;
}
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
- return new Headers({
- ...headers,
- ...extraHeaders
- });
+ const result = new Headers(extraHeaders);
+ for (const [name, value] of Object.entries(headers)) {
+ result.set(name, value);
+ }
+ return result;
}
_startOrAuth() {
const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch);
diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js
index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644
index 624172aa24ae255a67c083f9c19053343e4a0581..d833dce32e6dc90a5b72dee03e70acd0676034da 100644
--- a/dist/esm/client/streamableHttp.js
+++ b/dist/esm/client/streamableHttp.js
@@ -1,5 +1,5 @@
@ -320,7 +406,33 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
import { EventSourceParserStream } from 'eventsource-parser/stream';
// Default reconnection options for StreamableHTTP connections
@@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport {
@@ -29,7 +29,8 @@ export class StreamableHTTPClientTransport {
this._requestInit = opts?.requestInit;
this._authProvider = opts?.authProvider;
this._fetch = opts?.fetch;
- this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
+ this._fetchWithInit = createFetchWithInit(opts?.fetch, oauthRequestInit);
this._sessionId = opts?.sessionId;
this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;
}
@@ -70,10 +71,11 @@ export class StreamableHTTPClientTransport {
headers['mcp-protocol-version'] = this._protocolVersion;
}
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
- return new Headers({
- ...headers,
- ...extraHeaders
- });
+ const result = new Headers(extraHeaders);
+ for (const [name, value] of Object.entries(headers)) {
+ result.set(name, value);
+ }
+ return result;
}
async _startOrAuthSse(options) {
const { resumptionToken } = options;
@@ -286,7 +288,38 @@ export class StreamableHTTPClientTransport {
this.onclose?.();
}
async send(message, options) {
@ -359,7 +471,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
const { resumptionToken, onresumptiontoken } = options || {};
if (resumptionToken) {
// If we have at last event ID, we need to reconnect the SSE stream
@@ -294,6 +325,7 @@ export class StreamableHTTPClientTransport {
@@ -294,6 +327,7 @@ export class StreamableHTTPClientTransport {
return;
}
const headers = await this._commonHeaders();
@ -367,7 +479,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
headers.set('content-type', 'application/json');
headers.set('accept', 'application/json, text/event-stream');
const init = {
@@ -306,11 +338,20 @@ export class StreamableHTTPClientTransport {
@@ -306,11 +340,20 @@ export class StreamableHTTPClientTransport {
const response = await (this._fetch ?? fetch)(this._url, init);
// Handle session ID received during initialization
const sessionId = response.headers.get('mcp-session-id');
@ -389,7 +501,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
if (response.status === 401 && this._authProvider) {
// Prevent infinite recursion when server returns 401 after successful auth
if (this._hasCompletedAuthFlow) {
@@ -331,7 +372,7 @@ export class StreamableHTTPClientTransport {
@@ -331,7 +374,7 @@ export class StreamableHTTPClientTransport {
// Mark that we completed auth flow
this._hasCompletedAuthFlow = true;
// Purposely _not_ awaited, so we don't call onerror twice
@ -398,7 +510,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
}
if (response.status === 403 && this._authProvider) {
const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response);
@@ -358,7 +399,7 @@ export class StreamableHTTPClientTransport {
@@ -358,7 +401,7 @@ export class StreamableHTTPClientTransport {
if (result !== 'AUTHORIZED') {
throw new UnauthorizedError();
}
@ -408,7 +520,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
}
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js
index bfa2b7120a0f50c569364ea5264e6f811076f44f..abd8dfd707c155f71dae7aeeeeaf7547368ac749 100644
index bfa2b7120a0f50c569364ea5264e6f811076f44f..dec477d16a0fd796854542c1144279a6e86567f2 100644
--- a/dist/esm/shared/protocol.js
+++ b/dist/esm/shared/protocol.js
@@ -740,7 +740,12 @@ export class Protocol {