@@ -4,6 +4,8 @@ import type { FetchLike, JSONRPCMessage, Transport } from '@modelcontextprotocol import { createFetchWithInit, encodeMcpParamValue,
Show 12 hidden lines
+ fetchLeavingRedirects,+ fetchWithinOrigin, isInitializedNotification, isInitializeRequest, isJSONRPCErrorResponse,@@ -16,7 +18,8 @@ import { PROTOCOL_VERSION_META_KEY, SdkError, SdkErrorCode,- SdkHttpError+ SdkHttpError,+ unfollowedRedirect
} from '@modelcontextprotocol/core-internal'; import { EventSourceParserStream } from 'eventsource-parser/stream'; @@ -189,6 +192,11 @@ export type StreamableHTTPClientTransportOptions = { * token, `mcp-session-id`, and `mcp-protocol-version`. A caller-supplied `Authorization` * value is therefore only sent while the provider has no token, which lets a static API * key fall back to OAuth once the provider obtains one.
Show 1 hidden lines
+ *
+ * A `redirect` of `'error'` or `'manual'` is passed to fetch as it is for the POST, GET and+ * DELETE requests to the server URL. For the OAuth requests, and for any other value,+ * `redirect` is consulted only when+ * {@linkcode StreamableHTTPClientTransportOptions.redirectPolicy | redirectPolicy} is `'follow'`. */ requestInit?: RequestInit; @@ -197,6 +205,21 @@ export type StreamableHTTPClientTransportOptions = { */ fetch?: FetchLike; + /**+ * How a redirect of one of the transport's requests is handled, including the OAuth+ * requests it makes for {@linkcode StreamableHTTPClientTransportOptions.authProvider | authProvider}.+ * The option does not reach requests that a provider makes with a fetch of its own, such as+ * those of the `assertion` callback of a `CrossAppAccessProvider`.+ *
Show 4 hidden lines
+ * - `'same-origin'` (default): a redirect is followed only when it stays within the origin+ * of the request and keeps the method, and any other redirect fails the request.+ * - `'follow'`: redirects are left to the fetch implementation, as in earlier versions. It+ * follows them to any origin unless `requestInit.redirect` says otherwise.
+ *+ * @default 'same-origin'+ */+ redirectPolicy?: 'same-origin' | 'follow';+ /** * Options to configure the reconnection behavior. */@@ -328,6 +351,7 @@ export class StreamableHTTPClientTransport implements Transport { private _skipIssuerMetadataValidation?: boolean; private _fetch?: FetchLike; private _fetchWithInit: FetchLike;+ private _redirectPolicy?: 'same-origin' | 'follow'; private _sessionId?: string; private _reconnectionOptions: StreamableHTTPReconnectionOptions; private _protocolVersion?: string;@@ -371,7 +395,9 @@ export class StreamableHTTPClientTransport implements Transport { } else { this._authProvider = opts?.authProvider; }+ this._redirectPolicy = opts?.redirectPolicy; this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);+ if (this._redirectPolicy === 'follow') this._fetchWithInit = fetchLeavingRedirects(this._fetchWithInit); this._sessionId = opts?.sessionId; this._protocolVersion = opts?.protocolVersion; this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;@@ -448,6 +474,17 @@ export class StreamableHTTPClientTransport implements Transport { }); } + /** `baseFetch` with redirects handled as `redirectPolicy` says. */+ private _redirects(baseFetch: FetchLike): FetchLike {+ return this._redirectPolicy === 'follow' ? baseFetch : fetchWithinOrigin(baseFetch);+ }++ /** Error text for a redirect `response` that was not followed, or `undefined` for any other response. */+ private _unfollowedRedirect(url: string | URL, response: Response): string | undefined {+ const text = this._redirectPolicy === 'follow' ? undefined : unfollowedRedirect(url, response);+ return text && `${text} (redirectPolicy: 'same-origin')`;+ }+ private async _commonHeaders(): Promise<Headers> { // Start from the caller-supplied `requestInit.headers` and `set()` the // transport-managed headers on top. `Headers.set` compares names@@ -562,7 +599,7 @@ export class StreamableHTTPClientTransport implements Transport {
StreamableHTTPClientTransport adds a redirectPolicy option defaulting to same-origin, limiting redirects to the request origin and same method so cross-origin redirects cannot carry inherited headers or bodies.