const require_chunk = require('./chunk-Bnu9O96Y.cjs'); const require_src = require('./src-DpAS0jfg.cjs'); let _modelcontextprotocol_client__shims = require("@modelcontextprotocol/client/_shims"); let pkce_challenge = require("pkce-challenge"); pkce_challenge = require_chunk.__toESM(pkce_challenge); let eventsource = require("eventsource"); let eventsource_parser_stream = require("eventsource-parser/stream"); let _modelcontextprotocol_core_internal = require("@modelcontextprotocol/core/internal"); //#region src/client/authErrors.ts /** * Base class for the OAuth-client-flow error family. Concrete subclasses are * added to this module alongside the SEP-2468/837/2207/2350/2352 behavior * changes that throw them, so callers can catch the whole family with a single * `instanceof OAuthClientFlowError` guard once those land. * * @remarks Nothing in the SDK throws this base class directly. In the release * that introduces it no subclass exists yet — the guard is a forward-compat * hook and will not match anything until the first behavior change ships. */ var OAuthClientFlowError = class extends Error { static { Object.defineProperty(this, "mcpBrand", { value: "mcp.OAuthClientFlowError" }); } static [Symbol.hasInstance](value) { return require_src.brandedHasInstance(this, value); } /** * Brand-based type guard: equivalent to `value instanceof this`, as an * explicit static predicate (the axios/AWS-SDK `isInstance` style). Reads * the caller's own brand via `this`, so every branded subclass gets a * correctly-scoped guard by inheritance. Must be invoked on the class — * in callback position write `v => SdkError.isInstance(v)`, not * `.filter(SdkError.isInstance)` (detached calls throw rather than * silently matching nothing). */ static isInstance(value) { if (typeof this !== "function") throw new TypeError("isInstance must be called on the class (e.g. `SdkError.isInstance(value)`); for callbacks use `v => SdkError.isInstance(v)`"); return require_src.brandedHasInstance(this, value); } constructor(message) { super(message); this.name = new.target.name; require_src.stampErrorBrands(this, new.target); } }; /** * Thrown when an authorization-server issuer identifier fails validation. * * Two checks raise this error, distinguished by {@linkcode IssuerMismatchError.kind | kind}: * - `'metadata'` — the `issuer` in fetched authorization-server metadata does * not match the issuer identifier the well-known URL was constructed from * (RFC 8414 §3.3 / OpenID Connect Discovery §4.3). * - `'authorization_response'` — the `iss` parameter on the authorization * callback failed RFC 9207 §2.4 validation against the recorded issuer. * * Intentionally does **not** extend `OAuthError`: the `auth()` * orchestrator's `OAuthError` retry block must not swallow this — a mix-up * indication is fatal for the flow, not a retryable credential problem. * * On the `'authorization_response'` path the {@linkcode IssuerMismatchError.received | received} * value is attacker-controllable in a mix-up attack; callers **MUST NOT** display * it (or any `error`/`error_description`/`error_uri` from the same callback) to * end users. The values are JSON-encoded in the message to neutralize log-injection. */ var IssuerMismatchError = class extends OAuthClientFlowError { static { Object.defineProperty(this, "mcpBrand", { value: "mcp.IssuerMismatchError" }); } /** Which check failed — metadata echo (RFC 8414 §3.3) or authorization-response `iss` (RFC 9207). */ kind; /** The issuer the client expected (from validated metadata / discovery input). */ expected; /** The issuer value that was received. Attacker-controllable on the `'authorization_response'` path. */ received; constructor(kind, expected, received) { super(`Issuer mismatch in ${kind === "metadata" ? "authorization server metadata (RFC 8414 §3.3)" : "authorization response (RFC 9207)"}: expected ${JSON.stringify(expected)}, received ${JSON.stringify(received)}`); this.kind = kind; this.expected = expected; this.received = received; } }; /** * Thrown by `registerClient()` when the authorization server rejects a * Dynamic Client Registration request. Carries the HTTP status, the raw * response body, and the metadata that was submitted, so callers can inspect * the AS's `error` / `error_description` and retry with adjusted metadata * (for example a different `application_type`) per SEP-837. * * The `body` is the raw RFC 7591 error JSON; compare `JSON.parse(body).error` * against `OAuthErrorCode` (e.g. `OAuthErrorCode.InvalidRedirectUri`, * `OAuthErrorCode.InvalidClientMetadata`). * * Intentionally does **not** extend `OAuthError`: registration rejection is * not a recoverable-by-credential-invalidation condition, and staying outside * that hierarchy keeps it from being caught by `auth()`'s `OAuthError` retry * path. */ var RegistrationRejectedError = class extends OAuthClientFlowError { static { Object.defineProperty(this, "mcpBrand", { value: "mcp.RegistrationRejectedError" }); } /** HTTP status code returned by the registration endpoint. */ status; /** Raw response body text (typically an RFC 7591 error JSON document). */ body; /** The exact client metadata that was POSTed (after SDK defaults were applied). */ submittedMetadata; constructor(args) { super(`Dynamic Client Registration rejected (HTTP ${args.status}): ${args.body}`); this.status = args.status; this.body = args.body; this.submittedMetadata = args.submittedMetadata; } }; /** * Thrown by the token-exchange and refresh paths when the resolved token * endpoint is not `https:` and is not a loopback host. The MCP authorization * specification requires authorization server endpoints to use HTTPS; loopback * hosts are exempt here for local development. This is a configuration error — * re-authorizing cannot fix it — so it intentionally does **not** extend * `OAuthError` and `auth()`'s refresh branch rethrows it instead of falling * through to a fresh `/authorize` redirect. */ var InsecureTokenEndpointError = class extends OAuthClientFlowError { static { Object.defineProperty(this, "mcpBrand", { value: "mcp.InsecureTokenEndpointError" }); } /** The token endpoint URL that was rejected. */ tokenEndpoint; constructor(tokenEndpoint) { super(`Refusing to send credentials to non-https token endpoint '${tokenEndpoint}'. OAuth token requests MUST use TLS (localhost / *.localhost / 127.0.0.1 / ::1 are exempt).`); this.tokenEndpoint = tokenEndpoint; } }; /** * Thrown by the HTTP client transport when the server responds with * `403 Forbidden` and `WWW-Authenticate: Bearer error="insufficient_scope"`, * and either (a) the transport's `onInsufficientScope` option is `'throw'`, or * (b) `onInsufficientScope` is the default `'reauthorize'` but the transport * has no {@linkcode index.OAuthClientProvider | OAuthClientProvider} to drive * step-up (e.g. a minimal `AuthProvider`, `requestInit`-only headers, or no * `authProvider`). * * Carries the challenge parameters so the host can decide whether to initiate * step-up authorization itself (e.g., behind a UX gate) or surface the error. * * Does **not** extend `OAuthError`: that class represents OAuth protocol errors * from the authorization server; this is a resource-server challenge surfaced * at the transport layer. * * All fields originate from the resource server's `WWW-Authenticate` header; * treat them as untrusted input when displaying or logging (this includes * `requiredScope`, which appears in the error message). */ /** * Thrown by `auth()` and `fetchToken()` when the authorization server in use differs * from the one a credential is bound to and the flow cannot simply start over: * * - on the authorization-code callback leg, when it differs from the one recorded * in `discoveryState()` at redirect time — the `authorization_code` and PKCE * `code_verifier` are bound to the AS that minted the code (RFC 7636); * - for client information whose `issuer` stamp names a different authorization * server on a provider that cannot re-register (no `saveClientInformation()`, * e.g. the bundled static-credential providers); `fetchToken()` throws for any provider. * * Stored tokens and re-registrable client information are protected structurally * by the `issuer` stamp instead (they read back as absent). */ var AuthorizationServerMismatchError = class extends OAuthClientFlowError { static { Object.defineProperty(this, "mcpBrand", { value: "mcp.AuthorizationServerMismatchError" }); } constructor(recordedIssuer, currentIssuer) { super(`Authorization server mismatch: credentials are bound to ${JSON.stringify(recordedIssuer)} but this call resolved ${JSON.stringify(currentIssuer)}; refusing to present them to a different authorization server`); this.recordedIssuer = recordedIssuer; this.currentIssuer = currentIssuer; } }; var InsufficientScopeError = class extends OAuthClientFlowError { static { Object.defineProperty(this, "mcpBrand", { value: "mcp.InsufficientScopeError" }); } /** The `scope` value from the `WWW-Authenticate` challenge — the scopes the resource server says are required. */ requiredScope; /** The `resource_metadata` URL from the `WWW-Authenticate` challenge, if present. */ resourceMetadataUrl; /** The `error_description` from the `WWW-Authenticate` challenge, if present. */ errorDescription; constructor(init) { super(`Insufficient scope${init.requiredScope ? `: required "${init.requiredScope}"` : ""}`); this.requiredScope = init.requiredScope; this.resourceMetadataUrl = init.resourceMetadataUrl; this.errorDescription = init.errorDescription; } }; //#endregion //#region src/client/auth.ts /** * SEP-2352 stamp check: returns `stored` only when its `issuer` stamp matches the * resolved authorization server. A stamp that names a *different* issuer reads back * as `undefined`, so a credential issued by one authorization server is never reused * at another — the flow falls through to re-registration / re-authorization exactly * as if nothing were stored. An unstamped value (legacy provider or pre-SEP-2352 * storage) is returned as-is with a `console.warn`; {@linkcode auth} writes the * stamp back on first use so the window closes after one call. * * {@linkcode auth} stamps every value it writes via `saveTokens` / `saveClientInformation`, * so a provider that round-trips the stored object verbatim is protected with no extra * code. Providers that hold credentials for multiple authorization servers key their * storage on `ctx.issuer` instead. * * @param opts.canPersistStamp - When `false`, suppresses the unstamped-credential * warning: the caller cannot back-stamp (no `saveClientInformation`), so the * "binding on first use" claim would be false and would fire on every call. */ function discardIfIssuerMismatch(stored, issuer, opts) { if (!stored) return void 0; if (typeof stored.issuer !== "string") { if (opts?.canPersistStamp !== false) console.warn("[mcp-sdk] SEP-2352: stored OAuth credential has no 'issuer' stamp (pre-upgrade storage or provider not round-tripping the value). SEP-2352 isolation is inactive for this read; ensure your provider round-trips the issuer field."); return stored.issuer === void 0 ? stored : { ...stored, issuer: void 0 }; } return issuersMatch(stored.issuer, issuer) ? stored : void 0; } /** * SEP-2352 issuer-identity comparison. Tolerates a single trailing `/` difference, * mirroring the RFC 8414 §3.3 "one narrow tolerance" applied at metadata-echo * validation in {@linkcode discoverAuthorizationServerMetadata}: when the SDK * derives an issuer from `String(new URL(...))` (always slash-suffixed) and the AS * publishes a slash-free `metadata.issuer`, the two name the same authorization * server. */ function issuersMatch(a, b) { return a === b || a.endsWith("/") && a.slice(0, -1) === b || b.endsWith("/") && b.slice(0, -1) === a; } /** * Type guard distinguishing `OAuthClientProvider` from a minimal `AuthProvider`. * Transports use this at construction time to classify the `authProvider` option. * * Checks for `tokens()` + `clientInformation()` — two required `OAuthClientProvider` * methods that a minimal `AuthProvider` `{ token: ... }` would never have. */ function isOAuthClientProvider(provider) { if (provider == null) return false; const p = provider; return typeof p.tokens === "function" && typeof p.clientInformation === "function"; } /** * Standard `onUnauthorized` behavior for OAuth providers: extracts * `WWW-Authenticate` parameters from the 401 response and runs {@linkcode auth}. * Used by {@linkcode adaptOAuthProvider} to bridge `OAuthClientProvider` to `AuthProvider`. */ async function handleOAuthUnauthorized(provider, ctx, extraAuthOptions) { const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(ctx.response); if (await auth(provider, { serverUrl: ctx.serverUrl, resourceMetadataUrl, scope, fetchFn: ctx.fetchFn, ...extraAuthOptions }) !== "AUTHORIZED") throw new UnauthorizedError(); } /** * Adapts an `OAuthClientProvider` to the minimal `AuthProvider` interface that * transports consume. Called once at transport construction — the transport stores * the adapted provider for `_commonHeaders()` and 401 handling, while keeping the * original `OAuthClientProvider` for OAuth-specific paths (`finishAuth()`, 403 `insufficient_scope` step-up). * * SEP-2352 note: `token()` here is the per-request `Authorization: Bearer …` read for * the *resource server* (the MCP transport URL), not an authorization server. No OAuth * discovery has run at this layer, so there is no `issuer` to pass as `ctx` and no * {@linkcode discardIfIssuerMismatch} check to apply — the access token is sent only to * the resource server, never to an AS, so the SEP-2352 cross-AS isolation invariant is * not in scope. Providers that key storage on `ctx.issuer` MUST treat `ctx === undefined` * as "return the most-recently-saved token set" (the only consumer is the resource server * the token was minted for); providers that round-trip a single blob need no change. * * SEP-1932 (DPoP) note: DPoP request-signing is deliberately *not* done here. When the provider * implements {@linkcode OAuthClientProvider.dpop | dpop()}, the transports wrap their * resource-server `fetch` with {@linkcode withDpopFromProvider}, which upgrades the `Bearer` header * this adapter produces to `DPoP` + proof for DPoP-bound tokens and handles nonce challenges — at * the one layer that sees the real method, URL and response of every request. */ function adaptOAuthProvider(provider, extraAuthOptions) { return { token: async () => { return (await provider.tokens())?.access_token; }, onUnauthorized: async (ctx) => handleOAuthUnauthorized(provider, ctx, extraAuthOptions) }; } var UnauthorizedError = class extends Error { static { Object.defineProperty(this, "mcpBrand", { value: "mcp.UnauthorizedError" }); } static [Symbol.hasInstance](value) { return require_src.brandedHasInstance(this, value); } /** * Brand-based type guard: equivalent to `value instanceof this`, as an * explicit static predicate (the axios/AWS-SDK `isInstance` style). Reads * the caller's own brand via `this`, so every branded subclass gets a * correctly-scoped guard by inheritance. Must be invoked on the class — * in callback position write `v => SdkError.isInstance(v)`, not * `.filter(SdkError.isInstance)` (detached calls throw rather than * silently matching nothing). */ static isInstance(value) { if (typeof this !== "function") throw new TypeError("isInstance must be called on the class (e.g. `SdkError.isInstance(value)`); for callbacks use `v => SdkError.isInstance(v)`"); return require_src.brandedHasInstance(this, value); } constructor(message) { super(message ?? "Unauthorized"); this.name = "UnauthorizedError"; require_src.stampErrorBrands(this, new.target); } }; /** * Validates the `iss` parameter from an authorization response against the * issuer recorded from the authorization server's validated metadata, per * RFC 9207 §2.4 and the MCP specification's four-row decision table. * * | `issParameterSupported` | `iss` | Action | * | ----------------------- | ------- | ------------------------------------------------ | * | `true` | present | compare; throw {@linkcode IssuerMismatchError} on mismatch | * | `true` | absent | throw {@linkcode IssuerMismatchError} | * | `false` | present | compare; throw {@linkcode IssuerMismatchError} on mismatch | * | `false` | absent | proceed (no-op) | * * Comparison is **simple string equality** (RFC 3986 §6.2.1). Scheme/host case * folding, default-port elision, trailing-slash, and percent-encoding * normalization are explicitly **not** applied — any difference is a mismatch. * * When `expectedIssuer` is `undefined` (no validated metadata document exists), * the check has no authentic baseline and degenerates to a no-op. * * @throws {IssuerMismatchError} with `kind: 'authorization_response'` */ /** * Reads RFC 9207's `authorization_response_iss_parameter_supported` from * authorization-server metadata. Only a literal `true` counts as advertised; * absent, `false`, or a non-boolean wire value (coerced to `undefined` by the * schema) means not advertised. */ function isIssParameterSupported(metadata) { return metadata?.authorization_response_iss_parameter_supported === true; } function validateAuthorizationResponseIssuer({ iss, expectedIssuer, issParameterSupported }) { if (expectedIssuer === void 0) return; if (iss === void 0) { if (issParameterSupported) throw new IssuerMismatchError("authorization_response", expectedIssuer, void 0); return; } if (iss !== expectedIssuer) throw new IssuerMismatchError("authorization_response", expectedIssuer, iss); } /** * Computes the union of one or more OAuth `scope` strings. * * Each argument is a space-delimited scope string per RFC 6749 §3.3, or * `undefined`. The result is a single space-delimited string containing each * distinct scope token exactly once, in first-seen order, or `undefined` if * every input is empty/undefined. * * No hierarchical deduplication is performed: a union may contain semantically * redundant entries (e.g., a broad scope alongside a narrower one it implies). * Authorization servers normalize such redundancy during token issuance; the * spec's step-up flow does not require clients to. * * Used by the transport's `403 insufficient_scope` step-up path to accumulate * previously-requested scopes with newly-challenged scopes so re-authorization * does not lose previously-granted permissions. */ function computeScopeUnion(...scopes) { const seen = /* @__PURE__ */ new Set(); for (const scope of scopes) { if (!scope) continue; for (const token of scope.split(/\s+/)) if (token) seen.add(token); } return seen.size > 0 ? [...seen].join(" ") : void 0; } /** * Whether `union` contains at least one scope token not present in `current`. * Both arguments are space-delimited scope strings per RFC 6749 §3.3. * * Used to gate the step-up refresh bypass: when the union of previously-requested * and newly-challenged scopes is a strict superset of the current token's * granted scope, refreshing cannot widen the grant (RFC 6749 §6), so the * transport must force a fresh authorization request instead. When the current * token already covers the union, refresh remains valid. * * An undefined or empty `current` is treated as the empty set, so any non-empty * `union` is a strict superset. Note that per RFC 6749 §3.3 an authorization * server MAY omit the token's `scope` field when it equals the requested scope; * this helper is conservative and treats an absent token `scope` as empty, so * step-up always forces a fresh authorization request in that case rather than * risking a refresh that silently drops the widened scope. */ function isStrictScopeSuperset(union, current) { if (!union) return false; const currentSet = new Set((current ?? "").split(/\s+/).filter(Boolean)); for (const token of union.split(/\s+/)) if (token && !currentSet.has(token)) return true; return false; } /** * Shared `finishAuth` resolver for the `(code, iss?)` and `(URLSearchParams)` overloads. * * For the `URLSearchParams` form, only `iss` and `code` are read up front. When a `code` is * present the returned values flow into {@linkcode auth}, which runs * {@linkcode validateAuthorizationResponseIssuer} against freshly-discovered metadata before * the code is redeemed — so on mismatch the thrown {@linkcode IssuerMismatchError} carries no * `error`/`error_description`/`error_uri` text from the callback (those are attacker-controlled * in a mix-up). When no `code` is present (an error-shaped callback), `iss` is validated here * against the provider's recorded discovery state — or, when the provider does not implement * `discoveryState`, against freshly-discovered metadata mirroring what {@linkcode auth} does on * the code-present path — **before** the callback's error parameters are read; only after that * passes are they surfaced as an {@linkcode OAuthError}. When no issuer baseline can be * obtained either way, a generic {@linkcode UnauthorizedError} is thrown without surfacing the * callback's `error`/`error_description`/`error_uri`. * * @internal Exported for the transport `finishAuth` overloads; not part of the public barrel. */ async function resolveAuthorizationCallbackParams(codeOrParams, iss, provider, serverUrl, opts) { if (typeof codeOrParams === "string") return { authorizationCode: codeOrParams, iss }; const issParam = codeOrParams.get("iss") ?? void 0; const code = codeOrParams.get("code"); if (code) return { authorizationCode: code, iss: issParam }; let metadata = (await provider.discoveryState?.())?.authorizationServerMetadata; if (!metadata) try { metadata = (await discoverOAuthServerInfo(serverUrl, opts)).authorizationServerMetadata; } catch { metadata = void 0; } if (!metadata) throw new UnauthorizedError("Authorization callback failed and the issuer could not be verified"); validateAuthorizationResponseIssuer({ iss: issParam, expectedIssuer: metadata.issuer, issParameterSupported: isIssParameterSupported(metadata) }); const error = codeOrParams.get("error"); if (error) throw new require_src.OAuthError(error, codeOrParams.get("error_description") ?? error, codeOrParams.get("error_uri") ?? void 0); throw new UnauthorizedError("Authorization callback contained neither `code` nor `error`"); } function isClientAuthMethod(method) { return [ "client_secret_basic", "client_secret_post", "none" ].includes(method); } const AUTHORIZATION_CODE_RESPONSE_TYPE = "code"; const AUTHORIZATION_CODE_CHALLENGE_METHOD = "S256"; /** * Determines the best client authentication method to use based on server support and client configuration. * * Priority order (highest to lowest): * 1. `client_secret_basic` (if client secret is available) * 2. `client_secret_post` (if client secret is available) * 3. `none` (for public clients) * * @param clientInformation - OAuth client information containing credentials * @param supportedMethods - Authentication methods supported by the authorization server * @returns The selected authentication method */ function selectClientAuthMethod(clientInformation, supportedMethods) { const hasClientSecret = clientInformation.client_secret !== void 0; if ("token_endpoint_auth_method" in clientInformation && clientInformation.token_endpoint_auth_method && isClientAuthMethod(clientInformation.token_endpoint_auth_method) && (supportedMethods.length === 0 || supportedMethods.includes(clientInformation.token_endpoint_auth_method))) return clientInformation.token_endpoint_auth_method; if (supportedMethods.length === 0) return hasClientSecret ? "client_secret_basic" : "none"; if (hasClientSecret && supportedMethods.includes("client_secret_basic")) return "client_secret_basic"; if (hasClientSecret && supportedMethods.includes("client_secret_post")) return "client_secret_post"; if (supportedMethods.includes("none")) return "none"; return hasClientSecret ? "client_secret_post" : "none"; } /** * Applies client authentication to the request based on the specified method. * * Implements OAuth 2.1 client authentication methods: * - `client_secret_basic`: HTTP Basic authentication (RFC 6749 Section 2.3.1) * - `client_secret_post`: Credentials in request body (RFC 6749 Section 2.3.1) * - `none`: Public client authentication (RFC 6749 Section 2.1) * * @param method - The authentication method to use * @param clientInformation - OAuth client information containing credentials * @param headers - HTTP headers object to modify * @param params - URL search parameters to modify * @throws {Error} When required credentials are missing */ function applyClientAuthentication(method, clientInformation, headers, params) { const { client_id, client_secret } = clientInformation; switch (method) { case "client_secret_basic": applyBasicAuth(client_id, client_secret, headers); return; case "client_secret_post": applyPostAuth(client_id, client_secret, params); return; case "none": applyPublicAuth(client_id, params); return; default: throw new Error(`Unsupported client authentication method: ${method}`); } } /** * Applies HTTP Basic authentication (RFC 6749 Section 2.3.1) */ function applyBasicAuth(clientId, clientSecret, headers) { if (!clientSecret) throw new Error("client_secret_basic authentication requires a client_secret"); const credentials = btoa(`${clientId}:${clientSecret}`); headers.set("Authorization", `Basic ${credentials}`); } /** * Applies POST body authentication (RFC 6749 Section 2.3.1) */ function applyPostAuth(clientId, clientSecret, params) { params.set("client_id", clientId); if (clientSecret) params.set("client_secret", clientSecret); } /** * Applies public client authentication (RFC 6749 Section 2.1) */ function applyPublicAuth(clientId, params) { params.set("client_id", clientId); } /** * Loopback hosts exempt from the in-transit `https:` requirement (RFC 8252 §7.3). * Includes bare `localhost` and any name ending in `.localhost` (RFC 6761 §6.3). */ function isLoopbackHost(hostname) { return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"; } /** * Refuse to send credentials to a non-TLS token endpoint. The MCP authorization * specification requires authorization server endpoints to use HTTPS. Loopback * hosts are exempt here for local development. * * @throws {@linkcode InsecureTokenEndpointError} if the endpoint is insecure. */ function assertSecureTokenEndpoint(tokenEndpoint) { const url = new URL(String(tokenEndpoint)); if (url.protocol !== "https:" && !isLoopbackHost(url.hostname)) throw new InsecureTokenEndpointError(url.href); return url; } /** * Derives an OIDC `application_type` from a client's registered redirect URIs * when the consumer has not set one explicitly (SEP-837). Loopback hosts and * non-`http(s)` custom URI schemes indicate a native application (RFC 8252); * everything else is treated as a web application. The result is a heuristic * default — callers that know better should set `clientMetadata.application_type` * themselves, which {@linkcode resolveClientMetadata} never overwrites. * * A mixed redirect set (for example a public `https:` URI alongside a loopback * URI) is inherently ambiguous under OIDC DCR §2 — neither value satisfies the * AS for both URIs — so consumers with mixed sets should set `application_type` * explicitly rather than relying on this heuristic. */ function deriveApplicationType(redirectUris) { for (const raw of redirectUris ?? []) { let url; try { url = new URL(raw); } catch { continue; } if (url.protocol !== "http:" && url.protocol !== "https:") return "native"; if (isLoopbackHost(url.hostname)) return "native"; } return "web"; } /** * Reads {@linkcode OAuthClientProvider.clientMetadata | clientMetadata} from the * provider and fills the SEP-837 / SEP-2207 defaults the SDK relies on, so * {@linkcode registerClient} sees a consistent, fully-populated document. * * - `grant_types` defaults to `['authorization_code', 'refresh_token']` for * interactive providers (those with a {@linkcode OAuthClientProvider.redirectUrl | redirectUrl}) * so authorization servers that gate refresh-token issuance on the registered * grant types issue one (SEP-2207). Non-interactive providers (no * `redirectUrl`) get no `grant_types` default. This default applies to the * Dynamic Client Registration body only — it does **not** drive * {@linkcode determineScope}'s `offline_access` augmentation. * - `application_type` defaults from `redirect_uris`: loopback redirect hosts * and custom URI schemes → `'native'`, otherwise `'web'` (SEP-837 / RFC 8252). * * A field the consumer set explicitly is **never** overwritten. {@linkcode auth} * calls this once at the top of the flow; direct callers of * {@linkcode registerClient} that want the same defaults should pass the result * of this function as `clientMetadata`. */ function resolveClientMetadata(provider) { const clientMetadata = provider.clientMetadata; return { ...clientMetadata, grant_types: clientMetadata.grant_types ?? (provider.redirectUrl === void 0 ? void 0 : ["authorization_code", "refresh_token"]), application_type: clientMetadata.application_type ?? deriveApplicationType(clientMetadata.redirect_uris) }; } /** * Parses an OAuth error response from a string or Response object. * * If the input is a standard OAuth2.0 error response, it will be parsed according to the spec * and an {@linkcode OAuthError} will be returned with the appropriate error code. * If parsing fails, it falls back to a generic {@linkcode OAuthErrorCode.ServerError | ServerError} that includes * the response status (if available) and original content. * * @param input - A Response object or string containing the error response * @returns A Promise that resolves to an {@linkcode OAuthError} instance */ async function parseErrorResponse(input) { const statusCode = input instanceof Response ? input.status : void 0; const body = input instanceof Response ? await input.text() : input; try { const result = _modelcontextprotocol_core_internal.OAuthErrorResponseSchema.parse(JSON.parse(body)); return require_src.OAuthError.fromResponse(result); } catch (error) { const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ""}Invalid OAuth error response: ${error}. Raw body: ${body}`; return new require_src.OAuthError(require_src.OAuthErrorCode.ServerError, errorMessage); } } /** * Recovering from a recoverable OAuth error discards stored credentials and silently starts a * fresh authorization. On a headless client whose `redirectToAuthorization()` is a no-op that * recovery is indistinguishable from nothing happening at all, so name the cause. The most * common case is `invalid_grant` — an expired, revoked, or rotation-reuse-detected refresh * token. See issue #2034. */ function warnCredentialInvalidation(provider, error, invalidated) { const action = provider.invalidateCredentials === void 0 ? `retrying authorization without discarding the stored ${invalidated} (provider implements no invalidateCredentials())` : `invalidating the stored ${invalidated} and retrying authorization`; console.warn(`[mcp-sdk] OAuth ${JSON.stringify(error.code)} — ${action}. Cause: ${JSON.stringify(error.message)}`); } /** * Orchestrates the full auth flow with a server. * * This can be used as a single entry point for all authorization functionality, * instead of linking together the other lower-level functions in this module. */ async function auth(provider, options) { try { return await authInternal(provider, options); } catch (error) { if (error instanceof require_src.OAuthError) { if (error.code === require_src.OAuthErrorCode.InvalidClient || error.code === require_src.OAuthErrorCode.UnauthorizedClient) { warnCredentialInvalidation(provider, error, "client credentials and tokens"); await provider.invalidateCredentials?.("client"); await provider.invalidateCredentials?.("tokens"); return await authInternal(provider, options); } else if (error.code === require_src.OAuthErrorCode.InvalidGrant || error.code === require_src.OAuthErrorCode.InvalidDpopProof) { warnCredentialInvalidation(provider, error, "tokens"); await provider.invalidateCredentials?.("tokens"); return await authInternal(provider, options); } } throw error; } } /** * Selects scopes per the MCP spec and augment for refresh token support. */ function determineScope(options) { const { requestedScope, resourceMetadata, authServerMetadata, clientMetadata } = options; let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(" ") || clientMetadata.scope; if (effectiveScope && authServerMetadata?.scopes_supported?.includes("offline_access") && !effectiveScope.split(" ").includes("offline_access") && clientMetadata.grant_types?.includes("refresh_token")) effectiveScope = `${effectiveScope} offline_access`; return effectiveScope; } async function authInternal(provider, { serverUrl, authorizationCode, iss, scope, resourceMetadataUrl, fetchFn, skipIssuerMetadataValidation, forceReauthorization }) { const clientMetadata = resolveClientMetadata(provider); const cachedState = await provider.discoveryState?.(); let resourceMetadata; let authorizationServerUrl; let metadata; let freshDiscoveryState; let effectiveResourceMetadataUrl = resourceMetadataUrl; if (!effectiveResourceMetadataUrl && cachedState?.resourceMetadataUrl) effectiveResourceMetadataUrl = new URL(cachedState.resourceMetadataUrl); if (cachedState?.authorizationServerUrl) { authorizationServerUrl = cachedState.authorizationServerUrl; resourceMetadata = cachedState.resourceMetadata; metadata = cachedState.authorizationServerMetadata ?? await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn, skipIssuerValidation: skipIssuerMetadataValidation }); if (!resourceMetadata) try { resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn); } catch (error) { if (error instanceof TypeError) throw error; } if (metadata !== cachedState.authorizationServerMetadata || resourceMetadata !== cachedState.resourceMetadata) await provider.saveDiscoveryState?.({ authorizationServerUrl: String(authorizationServerUrl), resourceMetadataUrl: effectiveResourceMetadataUrl?.toString(), resourceMetadata, authorizationServerMetadata: metadata }); } else { const serverInfo = await discoverOAuthServerInfo(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl, fetchFn, skipIssuerMetadataValidation }); authorizationServerUrl = serverInfo.authorizationServerUrl; metadata = serverInfo.authorizationServerMetadata; resourceMetadata = serverInfo.resourceMetadata; freshDiscoveryState = { authorizationServerUrl: String(authorizationServerUrl), resourceMetadataUrl: effectiveResourceMetadataUrl?.toString(), resourceMetadata, authorizationServerMetadata: metadata }; } const issuer = metadata?.issuer ?? String(authorizationServerUrl); const infoCtx = { issuer }; await provider.saveAuthorizationServerUrl?.(issuer); if (authorizationCode !== void 0) { const recordedIssuer = cachedState?.authorizationServerMetadata?.issuer ?? cachedState?.authorizationServerUrl; if (recordedIssuer === void 0) { if (provider.saveDiscoveryState !== void 0) throw new AuthorizationServerMismatchError("discoveryState was not available on the callback leg; ensure your provider persists discoveryState alongside codeVerifier", issuer); console.warn("[mcp-sdk] OAuthClientProvider does not implement saveDiscoveryState()/discoveryState(); the SEP-2352 callback-leg authorization-server binding cannot be checked. Implement discoveryState (persist alongside codeVerifier) — see docs/migration/upgrade-to-v2.md §SEP-2352."); } else if (!issuersMatch(recordedIssuer, issuer)) throw new AuthorizationServerMismatchError(recordedIssuer, issuer); } if (freshDiscoveryState) await provider.saveDiscoveryState?.(freshDiscoveryState); const selectedResource = await selectResourceURL(serverUrl, provider, resourceMetadata); const resource = selectedResource && resourceMetadata && !provider.validateResourceURL ? resourceMetadata.resource : selectedResource; if (resource) await provider.saveResourceUrl?.(resourceIndicatorToString(resource)); const resolvedScope = determineScope({ requestedScope: scope, resourceMetadata, authServerMetadata: metadata, clientMetadata: provider.clientMetadata }); const rawClientInfo = await Promise.resolve(provider.clientInformation(infoCtx)); let clientInformation = discardIfIssuerMismatch(rawClientInfo, issuer, { canPersistStamp: provider.saveClientInformation !== void 0 }); if (clientInformation === void 0 && rawClientInfo?.issuer && provider.saveClientInformation === void 0) throw new AuthorizationServerMismatchError(rawClientInfo.issuer, issuer); if (clientInformation && clientInformation.issuer === void 0) { clientInformation = { ...clientInformation, issuer }; await provider.saveClientInformation?.(clientInformation, infoCtx); } if (!clientInformation) { if (authorizationCode !== void 0) throw new Error("Existing OAuth client information is required when exchanging an authorization code"); const supportsUrlBasedClientId = metadata?.client_id_metadata_document_supported === true; const clientMetadataUrl = provider.clientMetadataUrl; if (clientMetadataUrl && !isHttpsUrl(clientMetadataUrl)) throw new require_src.OAuthError(require_src.OAuthErrorCode.InvalidClientMetadata, `clientMetadataUrl must be a valid HTTPS URL with a non-root pathname, got: ${clientMetadataUrl}`); if (supportsUrlBasedClientId && clientMetadataUrl) { clientInformation = { client_id: clientMetadataUrl, issuer }; await provider.saveClientInformation?.(clientInformation, infoCtx); } else { if (!provider.saveClientInformation) throw new Error("OAuth client information must be saveable for dynamic registration"); clientInformation = { ...await registerClient(authorizationServerUrl, { metadata, clientMetadata, scope: resolvedScope, fetchFn }), issuer }; await provider.saveClientInformation(clientInformation, infoCtx); } } const nonInteractiveFlow = !provider.redirectUrl; if (authorizationCode !== void 0 || nonInteractiveFlow) { if (authorizationCode !== void 0) validateAuthorizationResponseIssuer({ iss, expectedIssuer: metadata?.issuer, issParameterSupported: isIssParameterSupported(metadata) }); const tokens$1 = await fetchToken(provider, authorizationServerUrl, { metadata, resource, authorizationCode, iss, scope: resolvedScope, fetchFn }); await provider.saveTokens({ ...tokens$1, issuer }, infoCtx); return "AUTHORIZED"; } let tokens = discardIfIssuerMismatch(await provider.tokens(infoCtx), issuer); if (tokens && tokens.issuer === void 0) { tokens = { ...tokens, issuer }; await provider.saveTokens(tokens, infoCtx); } if (tokens?.refresh_token && !forceReauthorization) { let newTokens; try { newTokens = await refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken: tokens.refresh_token, resource, addClientAuthentication: provider.addClientAuthentication, dpop: await provider.dpop?.(), fetchFn }); } catch (error) { if (error instanceof InsecureTokenEndpointError) throw error; if (!(error instanceof require_src.OAuthError) || error.code === require_src.OAuthErrorCode.ServerError) console.warn(`[mcp-sdk] Could not refresh OAuth tokens; falling back to a new authorization request. Cause: ${JSON.stringify(error instanceof Error ? error.message : String(error))}`); else throw error; } if (newTokens) { await provider.saveTokens({ ...newTokens, issuer }, infoCtx); return "AUTHORIZED"; } } const state = provider.state ? await provider.state() : void 0; const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, { metadata, clientInformation, state, redirectUrl: provider.redirectUrl, scope: resolvedScope, resource }); await provider.saveCodeVerifier(codeVerifier); await provider.redirectToAuthorization(authorizationUrl); return "REDIRECT"; } /** * Validates that the given `clientMetadataUrl` is a valid HTTPS URL with a non-root pathname. * * No-op when `url` is `undefined` or empty (providers that do not use URL-based client IDs * are unaffected). When the value is defined but invalid, throws an {@linkcode OAuthError} * with code {@linkcode OAuthErrorCode.InvalidClientMetadata}. * * {@linkcode OAuthClientProvider} implementations that accept a `clientMetadataUrl` should * call this in their constructors for early validation. * * @param url - The `clientMetadataUrl` value to validate (from `OAuthClientProvider.clientMetadataUrl`) * @throws {OAuthError} When `url` is defined but is not a valid HTTPS URL with a non-root pathname */ function validateClientMetadataUrl(url) { if (url && !isHttpsUrl(url)) throw new require_src.OAuthError(require_src.OAuthErrorCode.InvalidClientMetadata, `clientMetadataUrl must be a valid HTTPS URL with a non-root pathname, got: ${url}`); } /** * SEP-991: URL-based Client IDs * Validate that the `client_id` is a valid URL with `https` scheme */ function isHttpsUrl(value) { if (!value) return false; try { const url = new URL(value); return url.protocol === "https:" && url.pathname !== "/"; } catch { return false; } } /** * Selects the RFC 8707 resource indicator for an MCP server: the provider's * {@linkcode OAuthClientProvider.validateResourceURL | validateResourceURL} result when * implemented, otherwise the protected resource metadata's `resource` (checked against the * server URL with `checkResourceAllowed`), or `undefined` when there is no metadata. * * The result is a parsed `URL`, so a pathless indicator such as `https://example.com` has * the `href` `https://example.com/`. {@linkcode auth} therefore sends the metadata string * verbatim instead of this URL's `href` (#1968); callers that emit the `resource` * parameter themselves should do the same. */ async function selectResourceURL(serverUrl, provider, resourceMetadata) { const defaultResource = require_src.resourceUrlFromServerUrl(serverUrl); if (provider.validateResourceURL) return await provider.validateResourceURL(defaultResource, resourceMetadata?.resource); if (!resourceMetadata) return; if (!require_src.checkResourceAllowed({ requestedResource: defaultResource, configuredResource: resourceMetadata.resource })) throw new Error(`Protected resource ${resourceMetadata.resource} does not match expected ${defaultResource} (or origin)`); return new URL(resourceMetadata.resource); } /** Auth-scheme challenge tokens {@linkcode extractWWWAuthenticateParams} recognizes. */ const RECOGNIZED_CHALLENGE_SCHEMES = new Set(["bearer", "dpop"]); /** * Extract `resource_metadata`, `scope`, `error`, and `error_description` from a * `WWW-Authenticate` header. * * Recognizes both the `Bearer` scheme (RFC 6750) and the `DPoP` scheme (RFC 9449 §7.1, * SEP-1932) — a DPoP-protected resource's challenge carries the same parameters under `DPoP` * instead of `Bearer`, and this must still surface `resource_metadata`/`scope` from it for * discovery and SEP-2350 step-up to work against such a resource. */ function extractWWWAuthenticateParams(res) { const authenticateHeader = res.headers.get("WWW-Authenticate"); if (!authenticateHeader) return {}; const [type, scheme] = authenticateHeader.split(" "); if (!type || !RECOGNIZED_CHALLENGE_SCHEMES.has(type.toLowerCase()) || !scheme) return {}; const resourceMetadataMatch = extractFieldFromWwwAuth(res, "resource_metadata") || void 0; let resourceMetadataUrl; if (resourceMetadataMatch) try { resourceMetadataUrl = new URL(resourceMetadataMatch); } catch {} const scope = extractFieldFromWwwAuth(res, "scope") || void 0; const error = extractFieldFromWwwAuth(res, "error") || void 0; const errorDescription = extractFieldFromWwwAuth(res, "error_description") || void 0; return { resourceMetadataUrl, scope, error, errorDescription }; } /** * Extracts a specific field's value from the `WWW-Authenticate` header string. * * @param response The HTTP response object containing the headers. * @param fieldName The name of the field to extract (e.g., `"realm"`, `"nonce"`). * @returns The field value */ function extractFieldFromWwwAuth(response, fieldName) { const wwwAuthHeader = response.headers.get("WWW-Authenticate"); if (!wwwAuthHeader) return null; const pattern = new RegExp(String.raw`${fieldName}=(?:"([^"]+)"|([^\s,]+))`); const match = wwwAuthHeader.match(pattern); if (match) { const result = match[1] || match[2]; if (result) return result; } return null; } /** * Extract `resource_metadata` from response header. * @deprecated Use {@linkcode extractWWWAuthenticateParams} instead. */ function extractResourceMetadataUrl(res) { const authenticateHeader = res.headers.get("WWW-Authenticate"); if (!authenticateHeader) return; const [type, scheme] = authenticateHeader.split(" "); if (type?.toLowerCase() !== "bearer" || !scheme) return; const match = /resource_metadata="([^"]*)"/.exec(authenticateHeader); if (!match || !match[1]) return; try { return new URL(match[1]); } catch { return; } } /** * Looks up {@link https://datatracker.ietf.org/doc/html/rfc9728 | RFC 9728} * OAuth 2.0 Protected Resource Metadata. * * If the server returns a 404 for the well-known endpoint, this function will * return `undefined`. Any other errors will be thrown as exceptions. */ async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn = fetch) { const response = await discoverMetadataWithFallback(serverUrl, "oauth-protected-resource", fetchFn, { protocolVersion: opts?.protocolVersion, metadataUrl: opts?.resourceMetadataUrl }); if (!response || response.status === 404) { await response?.text?.().catch(() => {}); throw new Error(`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`); } if (!response.ok) { await response.text?.().catch(() => {}); throw new Error(`HTTP ${response.status} trying to load well-known OAuth protected resource metadata.`); } return _modelcontextprotocol_core_internal.OAuthProtectedResourceMetadataSchema.parse(await response.json()); } /** * Fetch with a retry heuristic for CORS errors caused by custom headers. * * In browsers, adding a custom header (e.g. `MCP-Protocol-Version`) triggers a CORS preflight. * If the server doesn't allow that header, the browser throws a `TypeError` before any response * is received. Retrying without custom headers often succeeds because the request becomes * "simple" (no preflight). If the server sends no CORS headers at all, the retry also fails * with `TypeError` and we return `undefined` so callers can fall through to an alternate URL. * * However, `fetch()` also throws `TypeError` for non-CORS failures (DNS resolution, connection * refused, invalid URL). Swallowing those and returning `undefined` masks real errors and can * cause callers to silently fall through to a different discovery URL. CORS is a browser-only * concept, so in non-browser runtimes (Node.js, Workers) a `TypeError` from `fetch` is never a * CORS error — there we propagate the error instead of swallowing it. * * In browsers, we cannot reliably distinguish CORS `TypeError` from network `TypeError` from the * error object alone, so the swallow-and-fallthrough heuristic is preserved there. */ async function fetchWithCorsRetry(url, headers, fetchFn = fetch) { const withinOrigin = require_src.fetchWithinOrigin(fetchFn); try { return await withinOrigin(url, { headers }); } catch (error) { if (!(error instanceof TypeError) || !_modelcontextprotocol_client__shims.CORS_IS_POSSIBLE) throw error; if (headers) try { return await withinOrigin(url, {}); } catch (retryError) { if (!(retryError instanceof TypeError)) throw retryError; return; } return; } } /** * Constructs the well-known path for auth-related metadata discovery */ function buildWellKnownPath(wellKnownPrefix, pathname = "", options = {}) { if (pathname.endsWith("/")) pathname = pathname.slice(0, -1); return options.prependPathname ? `${pathname}/.well-known/${wellKnownPrefix}` : `/.well-known/${wellKnownPrefix}${pathname}`; } /** * Tries to discover OAuth metadata at a specific URL */ async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) { return await fetchWithCorsRetry(url, { "MCP-Protocol-Version": protocolVersion }, fetchFn); } /** * Determines if fallback to root discovery should be attempted */ function shouldAttemptFallback(response, pathname) { if (!response) return true; if (pathname === "/") return false; return !response.ok && response.status < 500 || response.status === 502; } /** * Generic function for discovering OAuth metadata with fallback support */ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, opts) { const issuer = new URL(serverUrl); const protocolVersion = opts?.protocolVersion ?? _modelcontextprotocol_core_internal.LATEST_PROTOCOL_VERSION; let url; if (opts?.metadataUrl) url = new URL(opts.metadataUrl); else { const wellKnownPath = buildWellKnownPath(wellKnownType, issuer.pathname); url = new URL(wellKnownPath, opts?.metadataServerUrl ?? issuer); url.search = issuer.search; } let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn); if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) response = await tryMetadataDiscovery(new URL(`/.well-known/${wellKnownType}`, issuer), protocolVersion, fetchFn); return response; } /** * Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata. * * If the server returns a 404 for the well-known endpoint, this function will * return `undefined`. Any other errors will be thrown as exceptions. * * @deprecated This function is deprecated in favor of {@linkcode discoverAuthorizationServerMetadata}. */ async function discoverOAuthMetadata(issuer, { authorizationServerUrl, protocolVersion } = {}, fetchFn = fetch) { if (typeof issuer === "string") issuer = new URL(issuer); if (!authorizationServerUrl) authorizationServerUrl = issuer; if (typeof authorizationServerUrl === "string") authorizationServerUrl = new URL(authorizationServerUrl); protocolVersion ??= _modelcontextprotocol_core_internal.LATEST_PROTOCOL_VERSION; const response = await discoverMetadataWithFallback(authorizationServerUrl, "oauth-authorization-server", fetchFn, { protocolVersion, metadataServerUrl: authorizationServerUrl }); if (!response || response.status === 404) { await response?.text?.().catch(() => {}); return; } if (!response.ok) { await response.text?.().catch(() => {}); throw new Error(`HTTP ${response.status} trying to load well-known OAuth metadata`); } return _modelcontextprotocol_core_internal.OAuthMetadataSchema.parse(await response.json()); } /** * Builds a list of discovery URLs to try for authorization server metadata. * URLs are returned in priority order: * 1. OAuth metadata at the given URL * 2. OIDC metadata endpoints at the given URL */ function buildDiscoveryUrls(authorizationServerUrl) { const url = typeof authorizationServerUrl === "string" ? new URL(authorizationServerUrl) : authorizationServerUrl; const hasPath = url.pathname !== "/"; const urlsToTry = []; if (!hasPath) { urlsToTry.push({ url: new URL("/.well-known/oauth-authorization-server", url.origin), type: "oauth" }, { url: new URL(`/.well-known/openid-configuration`, url.origin), type: "oidc" }); return urlsToTry; } let pathname = url.pathname; if (pathname.endsWith("/")) pathname = pathname.slice(0, -1); urlsToTry.push({ url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin), type: "oauth" }, { url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin), type: "oidc" }, { url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin), type: "oidc" }); return urlsToTry; } /** * Discovers authorization server metadata with support for * {@link https://datatracker.ietf.org/doc/html/rfc8414 | RFC 8414} OAuth 2.0 * Authorization Server Metadata and * {@link https://openid.net/specs/openid-connect-discovery-1_0.html | OpenID Connect Discovery 1.0} * specifications. * * This function implements a fallback strategy for authorization server discovery: * 1. Attempts RFC 8414 OAuth metadata discovery first * 2. If OAuth discovery fails, falls back to OpenID Connect Discovery * * @param authorizationServerUrl - The authorization server URL obtained from the MCP Server's * protected resource metadata, or the MCP server's URL if the * metadata was not found. * The returned metadata's `issuer` is validated against `authorizationServerUrl` * per RFC 8414 §3.3 (and OIDC Discovery §4.3): if they differ the metadata is * **rejected** with {@linkcode IssuerMismatchError} and not returned. Set * `skipIssuerValidation: true` to suppress this check — **security-weakening**, * intended only for known-misconfigured authorization servers. * * @param options - Configuration options * @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch * @param options.protocolVersion - MCP protocol version to use, defaults to {@linkcode LATEST_PROTOCOL_VERSION} * @param options.skipIssuerValidation - Skip the RFC 8414 §3.3 `issuer` echo check. **Security-weakening.** * @returns Promise resolving to authorization server metadata, or undefined if discovery fails * @throws {IssuerMismatchError} when the metadata's `issuer` does not match `authorizationServerUrl` */ async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn = fetch, protocolVersion = _modelcontextprotocol_core_internal.LATEST_PROTOCOL_VERSION, skipIssuerValidation = false } = {}) { const headers = { "MCP-Protocol-Version": protocolVersion, Accept: "application/json" }; const urlsToTry = buildDiscoveryUrls(authorizationServerUrl); for (const { url: endpointUrl, type } of urlsToTry) { const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn); if (!response) /** * CORS error occurred - don't throw as the endpoint may not allow CORS, * continue trying other possible endpoints */ continue; if (!response.ok) { await response.text?.().catch(() => {}); if (response.status < 500 || response.status === 502) continue; throw new Error(`HTTP ${response.status} trying to load ${type === "oauth" ? "OAuth" : "OpenID provider"} metadata from ${endpointUrl}`); } const parsed = type === "oauth" ? _modelcontextprotocol_core_internal.OAuthMetadataSchema.parse(await response.json()) : _modelcontextprotocol_core_internal.OpenIdProviderDiscoveryMetadataSchema.parse(await response.json()); if (!skipIssuerValidation) { const expectedIssuer = typeof authorizationServerUrl === "string" ? authorizationServerUrl : authorizationServerUrl.href; if (!(parsed.issuer === expectedIssuer || expectedIssuer.endsWith("/") && parsed.issuer === expectedIssuer.slice(0, -1))) throw new IssuerMismatchError("metadata", expectedIssuer, parsed.issuer); } return parsed; } } /** * Discovers the authorization server for an MCP server following * {@link https://datatracker.ietf.org/doc/html/rfc9728 | RFC 9728} (OAuth 2.0 Protected * Resource Metadata), with fallback to treating the server URL as the * authorization server. * * This function combines two discovery steps into one call: * 1. Probes `/.well-known/oauth-protected-resource` on the MCP server to find the * authorization server URL (RFC 9728). * 2. Fetches authorization server metadata from that URL (RFC 8414 / OpenID Connect Discovery). * * Use this when you need the authorization server metadata for operations outside the * {@linkcode auth} orchestrator, such as token refresh or token revocation. * * @param serverUrl - The MCP resource server URL * @param opts - Optional configuration * @param opts.resourceMetadataUrl - Override URL for the protected resource metadata endpoint * @param opts.fetchFn - Custom fetch function for HTTP requests * @returns Authorization server URL, metadata, and resource metadata (if available) */ async function discoverOAuthServerInfo(serverUrl, opts) { let resourceMetadata; let authorizationServerUrl; try { resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn); if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) authorizationServerUrl = resourceMetadata.authorization_servers[0]; } catch (error) { if (error instanceof TypeError) throw error; } if (!authorizationServerUrl) authorizationServerUrl = String(new URL("/", serverUrl)); const authorizationServerMetadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn: opts?.fetchFn, skipIssuerValidation: opts?.skipIssuerMetadataValidation }); return { authorizationServerUrl, authorizationServerMetadata, resourceMetadata }; } function resourceIndicatorToString(resource) { return typeof resource === "string" ? resource : resource.href; } /** * Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL. */ async function startAuthorization(authorizationServerUrl, { metadata, clientInformation, redirectUrl, scope, state, resource }) { let authorizationUrl; if (metadata) { authorizationUrl = new URL(metadata.authorization_endpoint); if (!metadata.response_types_supported.includes(AUTHORIZATION_CODE_RESPONSE_TYPE)) throw new Error(`Incompatible auth server: does not support response type ${AUTHORIZATION_CODE_RESPONSE_TYPE}`); if (metadata.code_challenge_methods_supported && !metadata.code_challenge_methods_supported.includes(AUTHORIZATION_CODE_CHALLENGE_METHOD)) throw new Error(`Incompatible auth server: does not support code challenge method ${AUTHORIZATION_CODE_CHALLENGE_METHOD}`); } else authorizationUrl = new URL("/authorize", authorizationServerUrl); const challenge = await (0, pkce_challenge.default)(); const codeVerifier = challenge.code_verifier; const codeChallenge = challenge.code_challenge; authorizationUrl.searchParams.set("response_type", AUTHORIZATION_CODE_RESPONSE_TYPE); authorizationUrl.searchParams.set("client_id", clientInformation.client_id); authorizationUrl.searchParams.set("code_challenge", codeChallenge); authorizationUrl.searchParams.set("code_challenge_method", AUTHORIZATION_CODE_CHALLENGE_METHOD); authorizationUrl.searchParams.set("redirect_uri", String(redirectUrl)); if (state) authorizationUrl.searchParams.set("state", state); if (scope) authorizationUrl.searchParams.set("scope", scope); if (scope?.split(" ").includes("offline_access")) authorizationUrl.searchParams.append("prompt", "consent"); if (resource) authorizationUrl.searchParams.set("resource", resourceIndicatorToString(resource)); return { authorizationUrl, codeVerifier }; } /** * Prepares token request parameters for an authorization code exchange. * * This is the default implementation used by {@linkcode fetchToken} when the provider * doesn't implement {@linkcode OAuthClientProvider.prepareTokenRequest | prepareTokenRequest}. * * @param authorizationCode - The authorization code received from the authorization endpoint * @param codeVerifier - The PKCE code verifier * @param redirectUri - The redirect URI used in the authorization request * @returns URLSearchParams for the `authorization_code` grant */ function prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, redirectUri) { return new URLSearchParams({ grant_type: "authorization_code", code: authorizationCode, code_verifier: codeVerifier, redirect_uri: String(redirectUri) }); } /** * Internal helper to execute a token request with the given parameters. * Used by {@linkcode exchangeAuthorization}, {@linkcode refreshAuthorization}, and {@linkcode fetchToken}. */ async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams, clientInformation, addClientAuthentication, resource, dpop, fetchFn }) { const tokenUrl = assertSecureTokenEndpoint(metadata?.token_endpoint ?? new URL("/token", authorizationServerUrl)); const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" }); if (resource) tokenRequestParams.set("resource", resourceIndicatorToString(resource)); if (!addClientAuthentication && clientInformation) applyClientAuthentication(selectClientAuthMethod(clientInformation, metadata?.token_endpoint_auth_methods_supported ?? []), clientInformation, headers, tokenRequestParams); const requestOnce = async () => { const requestHeaders = new Headers(headers); if (addClientAuthentication) await addClientAuthentication(requestHeaders, tokenRequestParams, tokenUrl, metadata); if (dpop) requestHeaders.set("DPoP", await dpop.buildProof({ htm: "POST", htu: tokenUrl })); return require_src.fetchWithinOrigin(fetchFn ?? fetch)(tokenUrl, { method: "POST", headers: requestHeaders, body: tokenRequestParams }); }; let response = await requestOnce(); if (dpop && response.status === 400) { if ((await response.clone().json().catch(() => {}))?.error === require_src.OAuthErrorCode.UseDpopNonce) { dpop.observeNonce(response, tokenUrl); response = await requestOnce(); } } dpop?.observeNonce(response, tokenUrl); if (!response.ok) throw await parseErrorResponse(response); const json = await response.json(); try { return _modelcontextprotocol_core_internal.OAuthTokensSchema.parse(require_src.withoutIssuer(json)); } catch (parseError) { if (typeof json === "object" && json !== null && "error" in json) throw await parseErrorResponse(JSON.stringify(json)); throw parseError; } } /** * Exchanges an authorization code for an access token with the given server. * * Supports multiple client authentication methods as specified in OAuth 2.1: * - Automatically selects the best authentication method based on server support * - Falls back to appropriate defaults when server metadata is unavailable * * @param authorizationServerUrl - The authorization server's base URL * @param options - Configuration object containing client info, auth code, etc. * @returns Promise resolving to OAuth tokens * @throws {Error} When token exchange fails or authentication is invalid */ async function exchangeAuthorization(authorizationServerUrl, { metadata, clientInformation, authorizationCode, iss, codeVerifier, redirectUri, resource, addClientAuthentication, dpop, fetchFn }) { validateAuthorizationResponseIssuer({ iss, expectedIssuer: metadata?.issuer, issParameterSupported: isIssParameterSupported(metadata) }); return executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams: prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, redirectUri), clientInformation, addClientAuthentication, resource, dpop, fetchFn }); } /** * Exchange a refresh token for an updated access token. * * Supports multiple client authentication methods as specified in OAuth 2.1: * - Automatically selects the best authentication method based on server support * - Preserves the original refresh token if a new one is not returned * * @param authorizationServerUrl - The authorization server's base URL * @param options - Configuration object containing client info, refresh token, etc. * @returns Promise resolving to OAuth tokens (preserves original `refresh_token` if not replaced) * @throws {Error} When token refresh fails or authentication is invalid */ async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, dpop, fetchFn }) { return { refresh_token: refreshToken, ...await executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken }), clientInformation, addClientAuthentication, resource, dpop, fetchFn }) }; } /** * Unified token fetching that works with any grant type via {@linkcode OAuthClientProvider.prepareTokenRequest | prepareTokenRequest()}. * * This function provides a single entry point for obtaining tokens regardless of the * OAuth grant type. The provider's `prepareTokenRequest()` method determines which grant * to use and supplies the grant-specific parameters. * * @param provider - OAuth client provider that implements `prepareTokenRequest()` * @param authorizationServerUrl - The authorization server's base URL * @param options - Configuration for the token request * @returns Promise resolving to OAuth tokens * @throws {Error} When provider doesn't implement `prepareTokenRequest` or token fetch fails * * @example * ```ts source="./auth.examples.ts#fetchToken_clientCredentials" * // Provider for client_credentials: * class MyProvider extends MyProviderBase implements OAuthClientProvider { * prepareTokenRequest(scope?: string) { * const params = new URLSearchParams({ grant_type: 'client_credentials' }); * if (scope) params.set('scope', scope); * return params; * } * } * * const tokens = await fetchToken(new MyProvider(), authServerUrl, { metadata }); * ``` */ async function fetchToken(provider, authorizationServerUrl, { metadata, resource, authorizationCode, iss, scope, fetchFn } = {}) { if (authorizationCode !== void 0) validateAuthorizationResponseIssuer({ iss, expectedIssuer: metadata?.issuer, issParameterSupported: isIssParameterSupported(metadata) }); const issuer = metadata?.issuer ?? String(authorizationServerUrl); const readClientInformation = async () => { const rawClientInfo = await provider.clientInformation({ issuer }); const checked = discardIfIssuerMismatch(rawClientInfo, issuer, { canPersistStamp: false }); if (rawClientInfo && checked === void 0) throw new AuthorizationServerMismatchError(String(rawClientInfo.issuer), issuer); return checked; }; let clientInformation = await readClientInformation(); const effectiveScope = scope ?? provider.clientMetadata.scope; let tokenRequestParams; if (provider.prepareTokenRequest) tokenRequestParams = await provider.prepareTokenRequest(effectiveScope); if (!tokenRequestParams) { if (!authorizationCode) throw new Error("Either provider.prepareTokenRequest() or authorizationCode is required"); if (!provider.redirectUrl) throw new Error("redirectUrl is required for authorization_code flow"); tokenRequestParams = prepareAuthorizationCodeRequest(authorizationCode, await provider.codeVerifier(), provider.redirectUrl); } clientInformation ??= await readClientInformation(); return executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams, clientInformation, addClientAuthentication: provider.addClientAuthentication, resource, dpop: await provider.dpop?.(), fetchFn }); } /** * Performs OAuth 2.0 Dynamic Client Registration according to * {@link https://datatracker.ietf.org/doc/html/rfc7591 | RFC 7591}. * * If `scope` is provided, it overrides `clientMetadata.scope` in the registration * request body. This allows callers to apply the Scope Selection Strategy (SEP-835) * consistently across both DCR and the subsequent authorization request. * * @deprecated Dynamic Client Registration is deprecated as of protocol version * 2026-07-28 in favor of Client ID Metadata Documents (SEP-991); the deprecation * landed via spec PR * {@link https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2858 | modelcontextprotocol#2858} * (SEP-2577 is the separate roots/sampling/logging deprecation). Remains * functional during the deprecation window — at least twelve months under the * feature lifecycle policy (SEP-2596), so 2027-07-28 is the earliest possible * removal date. Prefer a CIMD URL `client_id` when the authorization server * advertises `client_id_metadata_document_supported`: the built-in `auth()` flow * skips registration for you when that capability is advertised AND your * provider supplies `clientMetadataUrl`, but `registerClient` itself does not * gate — calling it directly always sends the registration request. */ async function registerClient(authorizationServerUrl, { metadata, clientMetadata, scope, fetchFn }) { let registrationUrl; if (metadata) { if (!metadata.registration_endpoint) throw new Error("Incompatible auth server: does not support dynamic client registration"); registrationUrl = new URL(metadata.registration_endpoint); } else registrationUrl = new URL("/register", authorizationServerUrl); const submittedMetadata = { ...clientMetadata, ...scope === void 0 ? {} : { scope } }; const response = await require_src.fetchWithinOrigin(fetchFn ?? fetch)(registrationUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(submittedMetadata) }); if (!response.ok) throw new RegistrationRejectedError({ status: response.status, body: await response.text(), submittedMetadata }); return _modelcontextprotocol_core_internal.OAuthClientInformationFullSchema.parse(require_src.withoutIssuer(await response.json())); } //#endregion //#region src/client/authExtensions.ts /** * Helper to produce a `private_key_jwt` client authentication function. * * @example * ```ts source="./authExtensions.examples.ts#createPrivateKeyJwtAuth_basicUsage" * const addClientAuth = createPrivateKeyJwtAuth({ * issuer: 'my-client', * subject: 'my-client', * privateKey: pemEncodedPrivateKey, * alg: 'RS256' * }); * // pass addClientAuth as provider.addClientAuthentication implementation * ``` */ function createPrivateKeyJwtAuth(options) { return async (_headers, params, url, metadata) => { if (globalThis.crypto === void 0) throw new TypeError("crypto is not available, please ensure you have Web Crypto API support for older Node.js versions (see https://github.com/modelcontextprotocol/typescript-sdk#nodejs-web-crypto-globalthiscrypto-compatibility)"); const jose = await import("jose"); const audience = String(options.audience ?? metadata?.issuer ?? url); const lifetimeSeconds = options.lifetimeSeconds ?? 300; const now = Math.floor(Date.now() / 1e3); const jti = `${Date.now()}-${Math.random().toString(36).slice(2)}`; const baseClaims = { iss: options.issuer, sub: options.subject, aud: audience, exp: now + lifetimeSeconds, iat: now, jti }; const claims = options.claims ? { ...baseClaims, ...options.claims } : baseClaims; const alg = options.alg; let key; if (typeof options.privateKey === "string") if (alg.startsWith("RS") || alg.startsWith("ES") || alg.startsWith("PS")) key = await jose.importPKCS8(options.privateKey, alg); else if (alg.startsWith("HS")) key = new TextEncoder().encode(options.privateKey); else throw new Error(`Unsupported algorithm ${alg}`); else if (options.privateKey instanceof Uint8Array) key = alg.startsWith("HS") ? options.privateKey : await jose.importPKCS8(new TextDecoder().decode(options.privateKey), alg); else key = await jose.importJWK(options.privateKey, alg); const assertion = await new jose.SignJWT(claims).setProtectedHeader({ alg, typ: "JWT" }).setIssuer(options.issuer).setSubject(options.subject).setAudience(audience).setIssuedAt(now).setExpirationTime(now + lifetimeSeconds).setJti(jti).sign(key); params.set("client_assertion", assertion); params.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); }; } /** * The `issuer` stamp for constructor-supplied client information. Omitting `expectedIssuer` is * deprecated: nothing else says which authorization server such credentials belong to. */ function checkedExpectedIssuer(expectedIssuer) { if (expectedIssuer === void 0) console.warn("[mcp-sdk] Omitting `expectedIssuer` is deprecated. Without it, the MCP server decides which authorization server receives this client's credentials; pass your authorization server's issuer URL as `expectedIssuer` so they are only sent there."); else if (typeof expectedIssuer !== "string" || !expectedIssuer) throw new Error("expectedIssuer must be the authorization server's issuer URL"); return expectedIssuer; } /** * OAuth provider for `client_credentials` grant with `client_secret_basic` authentication. * * This provider is designed for machine-to-machine authentication where * the client authenticates using a `client_id` and `client_secret`. * * @example * ```ts source="./authExtensions.examples.ts#ClientCredentialsProvider_basicUsage" * const provider = new ClientCredentialsProvider({ * clientId: 'my-client', * clientSecret: 'my-secret', * expectedIssuer: 'https://auth.example.com' * }); * * const transport = new StreamableHTTPClientTransport(serverUrl, { * authProvider: provider * }); * ``` */ var ClientCredentialsProvider = class { _tokens; _clientInfo; _clientMetadata; constructor(options) { this._clientInfo = { client_id: options.clientId, client_secret: options.clientSecret, issuer: checkedExpectedIssuer(options.expectedIssuer) }; this._clientMetadata = { client_name: options.clientName ?? "client-credentials-client", redirect_uris: [], grant_types: ["client_credentials"], token_endpoint_auth_method: "client_secret_basic", scope: options.scope }; } get redirectUrl() {} get clientMetadata() { return this._clientMetadata; } clientInformation() { return this._clientInfo; } tokens() { return this._tokens; } saveTokens(tokens) { this._tokens = tokens; } redirectToAuthorization() { throw new Error("redirectToAuthorization is not used for client_credentials flow"); } saveCodeVerifier() {} codeVerifier() { throw new Error("codeVerifier is not used for client_credentials flow"); } prepareTokenRequest(scope) { const params = new URLSearchParams({ grant_type: "client_credentials" }); if (scope) params.set("scope", scope); return params; } }; /** * OAuth provider for `client_credentials` grant with `private_key_jwt` authentication. * * This provider is designed for machine-to-machine authentication where * the client authenticates using a signed JWT assertion * ({@link https://datatracker.ietf.org/doc/html/rfc7523#section-2.2 | RFC 7523 Section 2.2}). * * @example * ```ts source="./authExtensions.examples.ts#PrivateKeyJwtProvider_basicUsage" * const provider = new PrivateKeyJwtProvider({ * clientId: 'my-client', * privateKey: pemEncodedPrivateKey, * algorithm: 'RS256', * expectedIssuer: 'https://auth.example.com' * }); * * const transport = new StreamableHTTPClientTransport(serverUrl, { * authProvider: provider * }); * ``` */ var PrivateKeyJwtProvider = class { _tokens; _clientInfo; _clientMetadata; addClientAuthentication; constructor(options) { this._clientInfo = { client_id: options.clientId, issuer: checkedExpectedIssuer(options.expectedIssuer) }; this._clientMetadata = { client_name: options.clientName ?? "private-key-jwt-client", redirect_uris: [], grant_types: ["client_credentials"], token_endpoint_auth_method: "private_key_jwt", scope: options.scope }; this.addClientAuthentication = createPrivateKeyJwtAuth({ issuer: options.clientId, subject: options.clientId, privateKey: options.privateKey, alg: options.algorithm, lifetimeSeconds: options.jwtLifetimeSeconds, claims: options.claims }); } get redirectUrl() {} get clientMetadata() { return this._clientMetadata; } clientInformation() { return this._clientInfo; } tokens() { return this._tokens; } saveTokens(tokens) { this._tokens = tokens; } redirectToAuthorization() { throw new Error("redirectToAuthorization is not used for client_credentials flow"); } saveCodeVerifier() {} codeVerifier() { throw new Error("codeVerifier is not used for client_credentials flow"); } prepareTokenRequest(scope) { const params = new URLSearchParams({ grant_type: "client_credentials" }); if (scope) params.set("scope", scope); return params; } }; /** * OAuth provider for `client_credentials` grant with a static `private_key_jwt` assertion. * * This provider mirrors {@linkcode PrivateKeyJwtProvider} but instead of constructing and * signing a JWT on each request, it accepts a pre-built JWT assertion string and * uses it directly for authentication. */ var StaticPrivateKeyJwtProvider = class { _tokens; _clientInfo; _clientMetadata; addClientAuthentication; constructor(options) { this._clientInfo = { client_id: options.clientId, issuer: checkedExpectedIssuer(options.expectedIssuer) }; this._clientMetadata = { client_name: options.clientName ?? "static-private-key-jwt-client", redirect_uris: [], grant_types: ["client_credentials"], token_endpoint_auth_method: "private_key_jwt", scope: options.scope }; const assertion = options.jwtBearerAssertion; this.addClientAuthentication = async (_headers, params) => { params.set("client_assertion", assertion); params.set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); }; } get redirectUrl() {} get clientMetadata() { return this._clientMetadata; } clientInformation() { return this._clientInfo; } tokens() { return this._tokens; } saveTokens(tokens) { this._tokens = tokens; } redirectToAuthorization() { throw new Error("redirectToAuthorization is not used for client_credentials flow"); } saveCodeVerifier() {} codeVerifier() { throw new Error("codeVerifier is not used for client_credentials flow"); } prepareTokenRequest(scope) { const params = new URLSearchParams({ grant_type: "client_credentials" }); if (scope) params.set("scope", scope); return params; } }; /** * OAuth provider for Cross-App Access (Enterprise Managed Authorization) using JWT Authorization Grant. * * This provider implements the Enterprise Managed Authorization flow (SEP-990) where: * 1. User authenticates with an enterprise IdP and the client obtains an ID Token * 2. Client exchanges the ID Token for a JWT Authorization Grant (ID-JAG) via RFC 8693 token exchange * 3. Client uses the JAG to obtain an access token from the MCP server via RFC 7523 JWT bearer grant * * The provider handles steps 2-3 automatically, with the JAG acquisition delegated to * a callback function that you provide. This allows flexibility in how you obtain and * cache ID Tokens from the IdP. * * @see https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization.mdx * * @example * ```ts * const provider = new CrossAppAccessProvider({ * assertion: async (ctx) => { * const result = await discoverAndRequestJwtAuthGrant({ * idpUrl: 'https://idp.example.com', * audience: ctx.authorizationServerUrl, * resource: ctx.resourceUrl, * idToken: await getIdToken(), // Your function to get ID token * clientId: 'my-idp-client', * clientSecret: 'my-idp-secret', * scope: ctx.scope, * fetchFn: ctx.fetchFn * }); * return result.jwtAuthGrant; * }, * clientId: 'my-mcp-client', * clientSecret: 'my-mcp-secret', * expectedIssuer: 'https://auth.example.com' * }); * * const transport = new StreamableHTTPClientTransport(serverUrl, { * authProvider: provider * }); * ``` */ var CrossAppAccessProvider = class { _tokens; _clientInfo; _clientMetadata; _assertionCallback; _fetchFn; _authorizationServerUrl; _resourceUrl; _scope; constructor(options) { this._clientInfo = { client_id: options.clientId, client_secret: options.clientSecret, issuer: checkedExpectedIssuer(options.expectedIssuer) }; this._clientMetadata = { client_name: options.clientName ?? "cross-app-access-client", redirect_uris: [], grant_types: ["urn:ietf:params:oauth:grant-type:jwt-bearer"], token_endpoint_auth_method: "client_secret_basic" }; this._assertionCallback = options.assertion; this._fetchFn = options.fetchFn ?? fetch; } get redirectUrl() {} get clientMetadata() { return this._clientMetadata; } clientInformation() { return this._clientInfo; } tokens() { return this._tokens; } saveTokens(tokens) { this._tokens = tokens; } redirectToAuthorization() { throw new Error("redirectToAuthorization is not used for jwt-bearer flow"); } saveCodeVerifier() {} codeVerifier() { throw new Error("codeVerifier is not used for jwt-bearer flow"); } /** * Saves the authorization server URL discovered during OAuth flow. * This is called by the auth() function after RFC 9728 discovery. */ saveAuthorizationServerUrl(authorizationServerUrl) { this._authorizationServerUrl = authorizationServerUrl; } /** * Returns the cached authorization server URL if available. */ authorizationServerUrl() { return this._authorizationServerUrl; } /** * Saves the resource URL discovered during OAuth flow. * This is called by the auth() function after RFC 9728 discovery. */ saveResourceUrl(resourceUrl) { this._resourceUrl = resourceUrl; } /** * Returns the cached resource URL if available. */ resourceUrl() { return this._resourceUrl; } async prepareTokenRequest(scope) { const authServerUrl = this._authorizationServerUrl; const resourceUrl = this._resourceUrl; if (!authServerUrl) throw new Error("Authorization server URL not available. Ensure auth() has been called first."); if (!resourceUrl) throw new Error("Resource URL not available — server may not implement RFC 9728 Protected Resource Metadata (required for Cross-App Access), or auth() has not been called"); this._scope = scope; const jwtAuthGrant = await this._assertionCallback({ authorizationServerUrl: authServerUrl, resourceUrl, scope: this._scope, fetchFn: this._fetchFn }); const params = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwtAuthGrant }); if (scope) params.set("scope", scope); return params; } }; //#endregion //#region src/client/responseCache.ts /** * Methods whose entries are exempt from the * {@linkcode InMemoryResponseCacheStoreOptions.maxEntries} cap. Each holds at * most one entry per partition (a small bounded set) and the * `tools/list`-derived index depends on its entry surviving regardless of the * `resources/read` working-set size. Only `resources/read` keys count toward * the cap and are eligible for FIFO eviction. */ const CAP_EXEMPT_METHODS = new Set([ "tools/list", "prompts/list", "resources/list", "resources/templates/list", "server/discover" ]); /** * In-memory default. Bounded by an insertion-ordered size cap (default `512`; * see {@linkcode InMemoryResponseCacheStoreOptions.maxEntries}) on the * `resources/read` keyspace so an unbounded stream of distinct URIs cannot * grow it without limit; the list-singleton methods are exempt and never * evicted by the cap. `Map` preserves insertion order, so the oldest live * capped key is the first matching iteration entry. */ var InMemoryResponseCacheStore = class { _entries = /* @__PURE__ */ new Map(); _maxEntries; _stamp = 0; /** Count of held entries that are subject to the cap (i.e. not in {@linkcode CAP_EXEMPT_METHODS}). */ _cappedSize = 0; constructor(options) { this._maxEntries = options?.maxEntries ?? 512; } /** Number of held entries (for diagnostics / bounding tests). */ get size() { return this._entries.size; } get(key) { return this._entries.get(keyOf(key)); } set(key, entry) { const k = keyOf(key); const exempt = CAP_EXEMPT_METHODS.has(key.method); const isNew = !this._entries.has(k); if (!exempt && isNew && this._maxEntries > 0 && this._cappedSize >= this._maxEntries) { for (const oldKey of this._entries.keys()) if (!CAP_EXEMPT_METHODS.has(oldKey.slice(0, oldKey.indexOf("\0")))) { this._entries.delete(oldKey); this._cappedSize--; break; } } const stamp = ++this._stamp; this._entries.set(k, { ...entry, stamp }); if (isNew && !exempt) this._cappedSize++; return stamp; } delete(key) { if (this._entries.delete(keyOf(key)) && !CAP_EXEMPT_METHODS.has(key.method)) this._cappedSize--; } evict(method) { const prefix = `${method}\0`; const exempt = CAP_EXEMPT_METHODS.has(method); for (const k of this._entries.keys()) if (k.startsWith(prefix)) { this._entries.delete(k); if (!exempt) this._cappedSize--; } } clear() { this._entries.clear(); this._cappedSize = 0; } }; /** * Serialize a {@linkcode CacheKey} for the in-memory map. `method` is always * an SDK-set MCP method string (never contains a NUL), so the `\0` prefix * delimiter is safe and lets {@linkcode InMemoryResponseCacheStore.evict} do a * cheap prefix scan. `partition` (already a JSON-encoded * `[serverIdentity, principal]` pair) and `params` (a resource URI on the * `resources/read` path — caller-controlled) are JSON-array-encoded together, * which is collision-free regardless of any NUL or delimiter characters they * carry. */ function keyOf(key) { return `${key.method}\0${JSON.stringify([key.partition ?? "", key.params ?? ""])}`; } /** * Serialize a `{method, params}` pair for the eviction-generation map. The * list singletons key on `method` alone (their {@linkcode ClientResponseCache.evict} * is whole-method); `resources/read` keys on `` `${method}\0${uri}` `` so * {@linkcode ClientResponseCache.evictKey} bumps a per-URI counter. */ function genKey(method, params) { return params === void 0 ? method : `${method}\0${params}`; } /** * Upper bound on the server-supplied `ttlMs` honoured by * {@linkcode ClientResponseCache} (24h). A server cannot pin an entry * indefinitely. */ const MAX_CACHE_TTL_MS = 864e5; /** * The `Client`'s cache-coordination collaborator. * * Owns the per-connection cache state that used to live as five private * fields on `Client` — the backing {@linkcode ResponseCacheStore}, the * per-method eviction-generation counter, the user-supplied/default flag, and * the stamp-memoized derived indices over the `tools/list` entry. `Client` * holds exactly one instance and never reaches past it to the store. * * Not exported from the package index — internal to the client package. * * @internal */ var ClientResponseCache = class { /** * Per-logical-key eviction-generation counter. {@linkcode evict} (whole * method) and {@linkcode evictKey} (single `{method, params}`) bump it * before touching the store; {@linkcode captureGeneration} reads it before * the request; {@linkcode write} skips when it moved — so a `list_changed` * arriving mid-walk, or a `resources/updated` arriving while a * `readResource()` for the same URI is in flight, is not overwritten by * the in-flight request's stale write. The map key is `method` for the * list singletons and `` `${method}\0${params}` `` for per-URI keys. * * Growth is bounded by keys the CLIENT has issued a `captureGeneration` * for: {@linkcode captureGeneration} records the key (so an interleaved * {@linkcode evictKey} sees there is an in-flight write to suppress); * {@linkcode evictKey} only bumps a key that is already recorded — a * server streaming `notifications/resources/updated` for URIs the client * has never read therefore cannot grow this map. */ _evictionGeneration = /* @__PURE__ */ new Map(); /** * `name → Tool` index derived from the cached `tools/list` entry, memoized * against the entry's `stamp` so it re-derives only when the backing entry * changes (mcp.d's `cachedTool` pattern). */ _toolIndex; /** * `name → compiled output-schema validator` derived from the cached * `tools/list` entry; same stamp-keyed memoization as `_toolIndex`. Typed * `unknown` so this class stays free of any validator-provider dependency * — the compile callback supplied to {@linkcode outputValidator} owns the * concrete type. */ _toolOutputValidatorIndex; /** * The connected server's identity (`serverInfo.name@version`, the * transport's `sessionId`, or a client-generated per-connection * surrogate). Set by the `Client` immediately after a successful connect; * `''` is the pre-connect sentinel. Every storage partition is derived * from this (see `_partitionFor`), so two clients sharing one store but * connected to different servers never collide on `tools/list` and a * server cannot read another server's `'public'` entries. */ _serverIdentity = ""; constructor(_store, _isUserSupplied, _reportError = () => {}, _cachePartition = "", _now = Date.now) { this._store = _store; this._isUserSupplied = _isUserSupplied; this._reportError = _reportError; this._cachePartition = _cachePartition; this._now = _now; } /** The clock used for every freshness computation and check. */ now() { return this._now(); } /** * Record the connected server's identity. Called by `Client` immediately * after a successful connect: `serverInfo.name@version` when the server * identified itself, else the transport's `sessionId`, else a * client-generated per-connection surrogate (`serverInfo` is a spec * SHOULD on 2026-07-28, so anonymous servers exist). Surrogate-keyed * partitions are NOT stable across reconnects — no identity means no * cross-connection cache reuse, and a shared long-lived store should * bound its own size accordingly. Every partition derived after this * call is scoped to this identity; entries written under the pre-connect * `''` sentinel are no longer reachable. */ setServerIdentity(identity) { this._serverIdentity = identity; } /** * Derive the storage partition for `scope`. The encoding is * `JSON.stringify([serverIdentity, principal])` — JSON escaping makes it * collision-free by construction: a malicious server cannot craft a * `serverInfo.name`/`version` whose concatenated form bleeds into another * server's namespace or another principal's slot, regardless of `@` / `|` * / `"` / NUL in the server-controlled strings. `'public'` → * `[serverIdentity, '']` (shared within this server); `'private'` → * `[serverIdentity, cachePartition]`. When `cachePartition` is `''` the * two coincide. */ _partitionFor(scope) { return JSON.stringify([this._serverIdentity, scope === "public" ? "" : this._cachePartition]); } /** * Two-probe lookup: this client's own (private) partition first, then the * connected server's shared (public) partition. The shared probe is gated * on `entry.scope === 'public'` — a co-tenant client that omits * `cachePartition` writes its `'private'`-scoped entries at the public * partition, and serving those to a correctly-partitioned client would * leak private bodies (mcp.d's `cachedEntry` two-probe order; the scope * gate is defence-in-depth on top of the partition split). When * `cachePartition` is `''` the two partitions are identical and only one * probe is issued. */ async _probe(method, params) { const key = { method, params: params ?? "" }; const ownPartition = this._partitionFor("private"); const own = await this._store.get({ ...key, partition: ownPartition }); if (own !== void 0) return own; const sharedPartition = this._partitionFor("public"); if (sharedPartition === ownPartition) return void 0; const shared = await this._store.get({ ...key, partition: sharedPartition }); return shared?.scope === "public" ? shared : void 0; } /** * Bump the per-method generation (so an in-flight {@linkcode write} for the * same method becomes a no-op) and drop the connected server's two list * singletons (own + shared partition; `params: ''`). The generation bump * is unconditional and FIRST — the {@linkcode write} race guard relies on * the bump, not on the store's deletes completing. * * Eviction is scoped to this client's `[serverIdentity, principal]` * partitions (mirroring {@linkcode evictKey}) — the method-wide * `store.evict()` is NOT called, so on a shared store one server's * `list_changed` cannot wipe a co-tenant's entry. A custom store's * `delete()` may throw or reject; each partition is guarded * independently so a failure on one does not skip the other, the failure * is reported via the constructor's sink, and the call resolves so * dispatch proceeds. */ async evict(method) { this._evictionGeneration.set(method, (this._evictionGeneration.get(method) ?? 0) + 1); await this._deleteBoth(method, ""); } /** * Guarded two-partition delete of `{method, params}`: each partition's * `delete` is independently wrapped so a custom store's failure on one is * reported and does not skip the other, and the call always resolves. */ async _deleteBoth(method, params) { const ownPartition = this._partitionFor("private"); const sharedPartition = this._partitionFor("public"); try { await this._store.delete({ method, params, partition: ownPartition }); } catch (error) { this._reportError(error); } if (sharedPartition !== ownPartition) try { await this._store.delete({ method, params, partition: sharedPartition }); } catch (error) { this._reportError(error); } } /** * Drop the single logical entry `{method, params}` from BOTH the private * and public partitions for this client's connected server (mcp.d's * `invalidateLogical`). Used for `notifications/resources/updated`'s * per-URI eviction. The per-key generation is bumped FIRST (so an * in-flight {@linkcode write} for the same `{method, params}` becomes a * no-op and cannot re-cache the now-stale body) but only when the key was * already recorded by {@linkcode captureGeneration} — bounding the map to * keys the client has actually read. A custom store's `delete()` may * throw or reject; each partition's delete is guarded independently so a * failure on one does not skip the other, and the call resolves so * dispatch proceeds. */ async evictKey(method, params) { const gk = genKey(method, params); const current = this._evictionGeneration.get(gk); if (current !== void 0) this._evictionGeneration.set(gk, current + 1); await this._deleteBoth(method, params); } /** * Snapshot the eviction generation for `{method, params}` before issuing * the request (a list walk's page 1, or a `resources/read` for `params`). * Records the key so an interleaved {@linkcode evictKey} for the same * `{method, params}` knows there is an in-flight write to suppress and * bumps; without the record, `evictKey`'s recorded-only bump would skip * and the stale body would be cached. */ captureGeneration(method, params) { const gk = genKey(method, params); const current = this._evictionGeneration.get(gk) ?? 0; this._evictionGeneration.set(gk, current); return current; } /** * Write `value` under `{method}` unless the per-method generation moved * since `capturedGen` was taken — a `list_changed` that landed mid-walk has * already invalidated the result the caller is about to write, and * overwriting the eviction with the stale aggregate would lose the * invalidation. * * The value is stored as its JSON-serialized document; serialization * doubles as the mutation barrier, so a caller mutating the returned * aggregate cannot reach the cache or its derived indices. A value that * is not JSON-serializable (reachable only via in-process transports) * fails the write loudly into the `reportError` sink. A custom store * whose `set()` throws or rejects is routed to the same sink and the * write resolves — cache bookkeeping never costs the caller a result it * already fetched. * * `freshness` carries the client-computed `expiresAt` (absolute ms epoch, * `now + ttlMs`) and the server-reported `cacheScope`. The storage * `partition` is derived from the scope via `_partitionFor`: * `'public'` → `[serverIdentity, '']` (shared within this server); * `'private'` → `[serverIdentity, cachePartition]` (so a shared store * never serves a private entry to another identity). Absent `freshness` * preserves the substrate write (no `expiresAt`, private partition) — the * `tools/list` retain-for-schema posture: never served by * {@linkcode read}'s freshness gate, always readable by * {@linkcode toolDefinition}. * * After storing under the derived partition, the same `{method, params}` * is deleted from the OPPOSITE partition (mirroring {@linkcode evictKey}'s * two-partition posture). A server that flips a result's `cacheScope` for * the same key would otherwise leave the previous entry in the other slot * — and since `_probe` checks own-partition first, a stale private entry * would shadow the fresh public one (or a stale public entry would keep * serving co-tenants). Both store calls are independently guarded so a * custom store's failure on one does not skip the other. */ async write(method, value, capturedGen, freshness) { if ((this._evictionGeneration.get(genKey(method, freshness?.params)) ?? 0) !== capturedGen) return; const params = freshness?.params ?? ""; const ownPartition = this._partitionFor("private"); const sharedPartition = this._partitionFor("public"); const partition = (freshness?.scope ?? "private") === "public" ? sharedPartition : ownPartition; try { await this._store.set({ method, params, partition }, { value: encodeCacheValue(value), expiresAt: freshness?.expiresAt, scope: freshness?.scope }); } catch (error) { this._reportError(error); } if (sharedPartition !== ownPartition) try { await this._store.delete({ method, params, partition: partition === ownPartition ? sharedPartition : ownPartition }); } catch (error) { this._reportError(error); } } /** * Serve the fresh cached result for `{method, params}`, or `undefined`. * Lookup is the two-probe order (own-partition then this server's shared * partition, gated on `scope === 'public'`); freshness is * `entry.expiresAt > now()` (a missing `expiresAt` is never fresh), * checked BEFORE decoding so stale entries cost no parse. Every hit is * freshly parsed, so the caller owns the value outright. An entry whose * document does not parse or is not an object (corrupted external * store) is reported, * deleted, and treated as a miss — deleted because a fresh-but-corrupt * entry would otherwise re-parse and re-report on every read until its * `expiresAt` passes. */ async read(method, params) { const entry = await this._probe(method, params); if (entry?.expiresAt === void 0 || !(entry.expiresAt > this.now())) return void 0; try { const parsed = JSON.parse(entry.value); if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new TypeError("cached document is not an object"); return { value: parsed }; } catch (error) { this._reportError(error); await this._deleteBoth(method, params ?? ""); return; } } /** * Connection reset. The per-instance default store IS cleared * (connection-scoped); a user-supplied store is NOT — that would defeat * the only reason to supply one. The generation map and every derived * index are dropped regardless: they are connection-scoped even when the * backing store survives, so the next read re-derives from whatever the * store still holds. The server identity returns to the pre-connect * sentinel. The default impl is synchronous, so the `MaybePromise` * return is a plain void here and the caller need not await. */ resetForReconnect() { if (!this._isUserSupplied) this._store.clear(); this._evictionGeneration.clear(); this._toolIndex = void 0; this._toolOutputValidatorIndex = void 0; this._serverIdentity = ""; } /** * The descriptor for tool `name` taken from the cached `tools/list` entry. * The `name → Tool` index is memoized against the entry's `stamp` and * re-derived only when the backing entry changes (mcp.d's `cachedTool`). * Returns `undefined` only when no `tools/list` response is held at all, * or the held list does not contain `name`. * * Consumed by `callTool()`'s SEP-2243 `_resolveXMcpHeaderScan` (mirroring) * and, via {@linkcode outputValidator}, its output-schema validation. */ async toolDefinition(name) { const entry = await this._probe("tools/list"); if (entry === void 0) { this._toolIndex = void 0; return; } if (this._toolIndex?.stamp !== entry.stamp) { const list = this._decodeListTools(entry); const byName = /* @__PURE__ */ new Map(); if (list !== void 0) for (const tool of list.tools) byName.set(tool.name, tool); this._toolIndex = { stamp: entry.stamp, byName }; } return this._toolIndex.byName.get(name); } /** * The compiled output-schema validator for tool `name`, derived from the * cached `tools/list` entry — same source and same stamp-keyed * memoization as {@linkcode toolDefinition}. The `name → validator` index * re-derives only when the backing entry's stamp changes (a refetched * `tools/list` recompiles; a `list_changed` eviction drops it). Returns * `undefined` when no `tools/list` is held, the tool is absent, or it has * no `outputSchema`. * * `compile` is the caller-supplied validator-compile callback (the * `Client` passes its `_jsonSchemaValidator` wrapper) so this * class carries no validator-provider dependency. One tool's uncompilable * `outputSchema` (e.g. an invalid `pattern` regex or unresolvable `$ref`) * must not poison every other tool's `callTool` — the callback isolates * that compile error per tool by returning a per-tool error variant which * the index stores alongside the good ones, and `callTool` surfaces it as * a typed `InvalidParams` only for that name. Because the error is held on * this stamp-keyed substrate (not a parallel map), it inherits the * substrate's invalidation lifecycle: a `list_changed` eviction drops it, * a refetched `tools/list` re-derives it, and `resetForReconnect` clears * the lot. */ async outputValidator(name, compile) { const entry = await this._probe("tools/list"); if (entry === void 0) { this._toolOutputValidatorIndex = void 0; return; } if (this._toolOutputValidatorIndex?.stamp !== entry.stamp) { const list = this._decodeListTools(entry) ?? { tools: [] }; const byName = /* @__PURE__ */ new Map(); for (const tool of list.tools) { const compiled = compile(tool); if (compiled !== void 0) byName.set(tool.name, compiled); } this._toolOutputValidatorIndex = { stamp: entry.stamp, byName }; } return this._toolOutputValidatorIndex.byName.get(name); } /** Parse a held `tools/list` document for the index builders; a document * that does not parse OR whose `tools` is not an array of objects * (both mean a corrupted external store) is reported and treated as if * nothing were held. Callers memoize the outcome against the entry's * stamp, so a corrupt document costs one parse + report per stamp, not * per lookup. */ _decodeListTools(entry) { try { const parsed = JSON.parse(entry.value); if (!Array.isArray(parsed?.tools) || !parsed.tools.every((t) => t !== null && typeof t === "object")) throw new TypeError("cached tools/list document has a malformed tools array"); return parsed; } catch (error) { this._reportError(error); return; } } }; /** * Serialize a result for storage; throws TypeError for anything without a * JSON representation (legal wire results always have one). Two failure * modes need folding into that one throw: `JSON.stringify` throws on cycles * and BigInt, but silently returns `undefined` (not a string) for a * top-level function, symbol, or `undefined`. */ function encodeCacheValue(value) { let json; try { json = JSON.stringify(value); } catch (error) { throw new TypeError(`cache value is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`); } if (typeof json !== "string") throw new TypeError("cache value is not JSON-serializable: it has no JSON representation"); return json; } //#endregion //#region src/client/authSeam.ts /** * Auth-seam provenance stamp (internal — not part of the public API). * * Errors escaping a transport auth seam — the `authProvider.token()` read, * an `onUnauthorized` invocation (SDK adapter or custom user callback), the * 403 step-up flow, the transport's own auth-failure constructions — are * stamped at the throw boundary. The version-negotiation probe routes stamped * errors as auth outcomes; provenance is recorded where it is known, never * reconstructed from error types downstream. * * `Symbol.for` uses the global symbol registry, so the stamp survives a * duplicated SDK copy in one process (bundler double-install, version skew) * by design: both copies resolve the same symbol. */ const AUTH_SEAM = Symbol.for("mcp.authSeamEscape"); /** * Stamp `error` as an auth-seam escape and return it — identity-preserving * (the same object flows on, `instanceof` and `.cause` chains intact). A * frozen/sealed object is returned unstamped rather than replaced: identity * outranks provenance. Primitive throws cannot carry the stamp. */ function markAuthSeamEscape(error) { if (typeof error === "object" && error !== null || typeof error === "function") try { Object.defineProperty(error, AUTH_SEAM, { value: true, configurable: true }); } catch {} return error; } /** Whether `error` escaped through a transport auth seam. */ function isAuthSeamEscape(error) { return (typeof error === "object" && error !== null || typeof error === "function") && error[AUTH_SEAM] === true; } //#endregion //#region src/client/probeClassifier.ts /** The `-32022` UnsupportedProtocolVersion protocol error code (negotiation-phase recognition). */ const UNSUPPORTED_PROTOCOL_VERSION = -32022; /** * Deliberately not probe-recognized in either direction: deployed servers * overload `-32001` (the SDK-conventional `Session not found` body on a 2025 * stateful server), and the spec-assigned `-32020` (`HeaderMismatch`) / * `-32021` (`MissingRequiredClientCapability`) are not era evidence — all * fall into the conservative legacy default. */ const NOT_PROBE_RECOGNIZED = new Set([ -32001, -32020, -32021 ]); /** * Classify a single probe outcome. Pure: no I/O, no state — loop-guard and * retry state live in the caller. */ function classifyProbeOutcome(outcome, context) { switch (outcome.kind) { case "result": return classifyResult(outcome.result, context); case "rpc-error": return classifyRpcError(outcome, context); case "http-error": return classifyHttpError(outcome, context); case "network-error": return classifyNetworkError(outcome.error, context); case "unusable-reply": return { kind: "error", error: new require_src.SdkError(require_src.SdkErrorCode.EraNegotiationFailed, `Version negotiation probe failed: the server answered with an unusable reply (${describeError(outcome.error)})`, { cause: outcome.error }, { cause: outcome.error }) }; case "auth-required": return { kind: "error", error: outcome.error }; case "closed": if (context.transportKind === "stdio") return { kind: "legacy" }; return classifyNetworkError(/* @__PURE__ */ new Error("Connection closed during the version negotiation probe"), context); case "timeout": if (context.transportKind === "stdio") return { kind: "legacy" }; return { kind: "error", error: new require_src.SdkError(require_src.SdkErrorCode.RequestTimeout, `Version negotiation probe timed out after ${outcome.timeoutMs}ms`, { timeout: outcome.timeoutMs }) }; } } function classifyResult(result, context) { const parsed = require_src.codecForVersion(require_src.MODERN_WIRE_REVISION).validateResult("server/discover", result); if (!parsed.ok) return { kind: "legacy" }; const supportedVersions = parsed.value.supportedVersions; const overlap = context.clientModernVersions.find((version) => supportedVersions.includes(version)); if (overlap !== void 0) return { kind: "modern", version: overlap, discover: parsed.value }; if (context.fallbackAvailable) return { kind: "legacy" }; return { kind: "error", error: new require_src.UnsupportedProtocolVersionError({ supported: [...supportedVersions], requested: context.requestedVersion }) }; } function classifyRpcError(outcome, context) { const { code, message, data } = outcome; if (code === UNSUPPORTED_PROTOCOL_VERSION) { const supported = parseSupportedList(data); if (supported === void 0) return { kind: "legacy" }; const error = new require_src.UnsupportedProtocolVersionError({ supported, requested: parseRequested(data) ?? context.requestedVersion }, message); const supportedModern = require_src.modernProtocolVersions(supported); const mutual = context.clientModernVersions.find((version) => supportedModern.includes(version)); if (mutual !== void 0) return { kind: "corrective", version: mutual, error }; if (supportedModern.length > 0) return { kind: "error", error }; return context.fallbackAvailable ? { kind: "legacy" } : { kind: "error", error }; } if (NOT_PROBE_RECOGNIZED.has(code)) return { kind: "legacy" }; return { kind: "legacy" }; } function classifyHttpError(outcome, context) { if (outcome.status === 401 || outcome.status === 403) { const isDenial = outcome.status === 403; return { kind: "error", error: new require_src.SdkHttpError(isDenial ? require_src.SdkErrorCode.ClientHttpForbidden : require_src.SdkErrorCode.ClientHttpAuthentication, `Version negotiation failed: ${isDenial ? "the server denied access (HTTP 403)" : "the server requires authorization (HTTP 401)"}`, { status: outcome.status, statusText: outcome.statusText, text: outcome.body }) }; } if (outcome.status >= 500) return { kind: "error", error: new require_src.SdkHttpError(require_src.SdkErrorCode.EraNegotiationFailed, `Version negotiation failed: the server answered the probe with HTTP ${outcome.status}`, { status: outcome.status, statusText: outcome.statusText, text: outcome.body }) }; const rpcError = parseJsonRpcErrorBody(outcome.body); if (rpcError !== void 0) return classifyRpcError(rpcError, context); return { kind: "legacy" }; } function classifyNetworkError(error, context) { if (context.environment === "browser" && isOpaqueFetchTypeError(error)) return { kind: "legacy" }; return { kind: "error", error: new require_src.SdkError(require_src.SdkErrorCode.EraNegotiationFailed, `Version negotiation probe failed: ${describeError(error)}`, { cause: error }, { cause: error }) }; } function isOpaqueFetchTypeError(error) { return error instanceof TypeError || error instanceof Error && error.name === "TypeError"; } function describeError(error) { return error instanceof Error ? error.message : String(error); } function parseSupportedList(data) { if (typeof data !== "object" || data === null) return void 0; const supported = data.supported; if (!Array.isArray(supported) || supported.length === 0 || !supported.every((v) => typeof v === "string")) return; return supported; } function parseRequested(data) { if (typeof data !== "object" || data === null) return void 0; const requested = data.requested; return typeof requested === "string" ? requested : void 0; } function parseJsonRpcErrorBody(body) { if (body === void 0 || body === "") return void 0; let parsed; try { parsed = JSON.parse(body); } catch { return; } if (typeof parsed !== "object" || parsed === null) return void 0; const error = parsed.error; if (typeof error !== "object" || error === null) return void 0; const { code, message, data } = error; if (typeof code !== "number") return void 0; return { code, message: typeof message === "string" ? message : "", data }; } //#endregion //#region src/client/versionNegotiation.ts /** * The default mode when `versionNegotiation` (or its `mode`) is absent; * changing the default later is a flip of this single line. */ const DEFAULT_VERSION_NEGOTIATION_MODE = "legacy"; /** * Resolve the negotiation options into a per-connect plan. The raw (not * defaulted) `supportedProtocolVersions` option supplies the modern offer list; * a list without any legacy version makes this a modern-only client (no fallback). */ function resolveVersionNegotiation(options, supportedProtocolVersionsOption) { const mode = options?.mode ?? DEFAULT_VERSION_NEGOTIATION_MODE; if (mode === "legacy") return { kind: "legacy" }; const probe = options?.probe ?? {}; if (typeof mode === "object") { if (!require_src.isModernProtocolVersion(mode.pin)) throw new TypeError(`versionNegotiation: { pin: '${mode.pin}' } is not a modern protocol revision — pinning is for 2026-07-28 and later; omit versionNegotiation (or use mode: 'legacy') for 2025-era servers.`); return { kind: "pin", version: mode.pin, probe }; } const explicitModern = supportedProtocolVersionsOption ? require_src.modernProtocolVersions(supportedProtocolVersionsOption) : []; return { kind: "auto", modernVersions: explicitModern.length > 0 ? explicitModern : [...require_src.SUPPORTED_MODERN_PROTOCOL_VERSIONS], fallbackAvailable: supportedProtocolVersionsOption ? require_src.legacyProtocolVersions(supportedProtocolVersionsOption).length > 0 : true, probe }; } /** Detect the probe environment for the network-failure row — see {@linkcode ProbeEnvironment}. */ function detectProbeEnvironment() { const g = globalThis; return g.window !== void 0 && g.document !== void 0 ? "browser" : "node"; } /** * Detect the transport class for the transport-aware timeout and closed verdicts (see * {@linkcode ProbeTransportKind}). The stdio child-process transport is * recognized structurally (`stderr`/`pid` accessors, no `instanceof` — safe * across bundles); everything else is treated like HTTP. */ function detectProbeTransportKind(transport) { return "stderr" in transport && "pid" in transport ? "stdio" : "http"; } /** * Temporary ownership of a raw transport for the negotiation exchange, before * the Protocol machinery attaches. `open()` installs the window's handlers and * starts the transport; `release()` detaches them and arms a one-shot `start()` * pass-through so the subsequent Protocol connect (which always starts its * transport) takes over the already-started channel without a double-start error. * * Handlers a caller pre-set on the transport before `connect()` are saved on * open and restored on detach, so `Protocol.connect()` finds and chains them * exactly as it would on a plain (non-negotiating) connect. Error and close * events are forwarded to the saved handlers during the window, so negotiation * does not change what a pre-attached observer sees of the transport * lifecycle. (Inbound messages are deliberately NOT forwarded — the window's * drop-guard is a module invariant, and the probe reply has no plain-connect * counterpart; a pre-set `onmessage` is restored at detach and chained by * `Protocol.connect()` like the others.) */ var ProbeWindow = class ProbeWindow { _pending; _probeCounter = 0; _savedOnMessage; _savedOnError; _savedOnClose; _closeDelivered = false; constructor(_transport) { this._transport = _transport; this._savedOnMessage = _transport.onmessage; this._savedOnError = _transport.onerror; this._savedOnClose = _transport.onclose; } static async open(transport) { const window = new ProbeWindow(transport); transport.onmessage = (message) => { const pending = window._pending; if (pending !== void 0 && (require_src.isJSONRPCResultResponse(message) || require_src.isJSONRPCErrorResponse(message)) && message.id === pending.id) { window._pending = void 0; if (require_src.isJSONRPCResultResponse(message)) pending.resolve({ kind: "response", result: message.result }); else pending.resolve({ kind: "response", error: message.error }); return; } }; transport.onerror = (error) => { window._savedOnError?.(error); }; transport.onclose = () => { const pending = window._pending; if (pending !== void 0) { window._pending = void 0; pending.resolve({ kind: "closed" }); } window._closeDelivered = true; window._savedOnClose?.(); }; try { await transport.start(); } catch (error) { window.detach(); throw error; } return window; } /** * Send one probe request and await its reply. Probe ids are strings, so they * never collide with Protocol's numeric ids (e.g. on a shared stdio pipe). */ async exchange(buildRequest, timeoutMs) { const id = `server-discover-probe-${++this._probeCounter}`; return new Promise((resolve) => { let settled = false; const settle = (reply) => { if (settled) return; settled = true; clearTimeout(timer); if (this._pending?.id === id) this._pending = void 0; resolve(reply); }; const timer = setTimeout(() => settle({ kind: "timeout" }), timeoutMs); this._pending = { id, resolve: settle }; this._transport.send(buildRequest(id)).catch((error) => settle({ kind: "send-error", error })); }); } /** Detach the window's handlers, restoring any the caller pre-set, leaving the transport's own `start` untouched. */ detach() { this._pending = void 0; this._transport.onmessage = this._savedOnMessage; this._transport.onerror = this._savedOnError; if (this._closeDelivered && this._savedOnClose !== void 0) { const saved = this._savedOnClose; const transport = this._transport; let spent = false; const wrapper = () => { if (!spent) { spent = true; return; } saved(); }; transport.onclose = wrapper; pendingSpentCloseGuards.set(transport, () => { if (transport.onclose === wrapper) transport.onclose = saved; }); } else this._transport.onclose = this._savedOnClose; } /** Detach the handlers and arm the one-shot `start()` pass-through for the `Protocol.connect()` handover. */ release() { this.detach(); const transport = this._transport; const originalStart = transport.start; let armed = true; transport.start = async function() { if (armed) { armed = false; transport.start = originalStart; return; } return originalStart.call(transport); }; } }; /** * Spent-close guards from failed negotiations, pending disarm by the cleanup * site (keyed weakly — an abandoned transport carries its guard to GC). */ const pendingSpentCloseGuards = /* @__PURE__ */ new WeakMap(); /** * Disarm a failed negotiation's spent-close guard once the cleanup `close()` * has settled. Any re-delivery of the spent mid-window close happens DURING * that close (HTTP transports re-fire `onclose` there; stdio transports never * do — their close event already fired), so after it every close is genuine * and must reach the caller's pre-set observer. */ function disarmSpentCloseGuard(transport) { const disarm = pendingSpentCloseGuards.get(transport); pendingSpentCloseGuards.delete(transport); disarm?.(); } /** Build the probe request: `server/discover` carrying the full per-request `_meta` envelope. */ function buildProbeRequest(id, protocolVersion, clientInfo, capabilities) { return { jsonrpc: "2.0", id, method: "server/discover", params: { _meta: require_src.codecForVersion(protocolVersion).outboundEnvelope({ protocolVersion, clientInfo, clientCapabilities: capabilities }) } }; } /** True for what the transport throws when the probe was answered 2xx without a usable reply. */ function isUnusableReplyError(error) { if (error instanceof require_src.SdkError && error.code === require_src.SdkErrorCode.ClientHttpUnexpectedContent) return true; return error instanceof Error && error.name === "SyntaxError"; } function normalizeReply(reply, timeoutMs) { switch (reply.kind) { case "response": return reply.error === void 0 ? { kind: "result", result: reply.result } : { kind: "rpc-error", ...reply.error }; case "send-error": { const error = reply.error; if (isAuthSeamEscape(error) || error instanceof UnauthorizedError || error instanceof Error && error.name === "UnauthorizedError") return { kind: "auth-required", error }; if (error instanceof require_src.SdkHttpError) { const text = error.data?.text; return { kind: "http-error", status: error.data.status, body: typeof text === "string" ? text : void 0, statusText: error.data.statusText }; } if (isUnusableReplyError(error)) return { kind: "unusable-reply", error }; return { kind: "network-error", error }; } case "closed": return { kind: "closed" }; case "timeout": return { kind: "timeout", timeoutMs }; } } /** * Run the negotiation probe state machine on a raw (not yet Protocol-connected) * transport. Resolves with the negotiated era; throws typed connect errors. On * return the probe window has been released: the transport is started, * handler-free, and ready for `Protocol.connect()` handover. On throw the * window is detached and the transport's `start` is left untouched. */ async function negotiateEra(negotiation, deps) { const timeoutMs = negotiation.probe.timeoutMs ?? deps.defaultTimeoutMs; const maxRetries = Math.max(0, negotiation.probe.maxRetries ?? 0); const clientModernVersions = negotiation.kind === "pin" ? [negotiation.version] : negotiation.modernVersions; const fallbackAvailable = negotiation.kind === "auto" && negotiation.fallbackAvailable; const window = await ProbeWindow.open(deps.transport); const probe = async () => { let requestedVersion = clientModernVersions[0]; let correctiveUsed = false; let timeoutRetriesRemaining = maxRetries; for (;;) { const reply = await window.exchange((id) => buildProbeRequest(id, requestedVersion, deps.clientInfo, deps.capabilities), timeoutMs); if (reply.kind === "timeout" && timeoutRetriesRemaining > 0) { timeoutRetriesRemaining--; continue; } const outcome = normalizeReply(reply, timeoutMs); const verdict = classifyProbeOutcome(outcome, { clientModernVersions, requestedVersion, fallbackAvailable, environment: deps.environment, transportKind: deps.transportKind }); switch (verdict.kind) { case "modern": return { era: "modern", version: verdict.version, discover: verdict.discover }; case "corrective": if (correctiveUsed) throw verdict.error; correctiveUsed = true; requestedVersion = verdict.version; continue; case "legacy": { const closedCause = outcome.kind === "closed" ? "the connection closed during the server/discover probe" : void 0; if (negotiation.kind === "pin") throw new require_src.SdkError(require_src.SdkErrorCode.EraNegotiationFailed, closedCause === void 0 ? `Version negotiation failed: the server did not offer pinned protocol version ${negotiation.version} via server/discover (no fallback in pin mode)` : `Version negotiation failed: ${closedCause} before the server offered pinned protocol version ${negotiation.version} (no fallback in pin mode)`); if (!negotiation.fallbackAvailable) throw new require_src.SdkError(require_src.SdkErrorCode.EraNegotiationFailed, closedCause === void 0 ? "Version negotiation failed: the server gave no modern evidence and this client supports no pre-2026-07-28 protocol version to fall back to" : `Version negotiation failed: ${closedCause} and this client supports no pre-2026-07-28 protocol version to fall back to`); if (closedCause !== void 0 && deps.disposableProbe !== true) throw new require_src.SdkError(require_src.SdkErrorCode.EraNegotiationFailed, `Version negotiation failed: ${closedCause} (this transport probed in place — the disposable sibling probe requires the SDK's base StdioClientTransport)`); return { era: "legacy" }; } case "error": throw verdict.error; } } }; let result; try { result = await probe(); } catch (error) { window.detach(); throw error; } window.release(); return result; } /** * Structural read of the SDK stdio transport's retained spawn parameters — * `undefined` for stdio-shaped transports that do not expose them, and for * SUBCLASSES of the SDK transport (those probe in place, where a mid-probe * close stays a typed connect error). The sibling path requires the transport * to be exactly the base class: a subclass cannot be faithfully cloned by * re-invoking its constructor with the retained params alone (extra ctor * arguments, transformed state, side effects). Exactness is checked without * importing the class (the negotiation graph must stay runtime-neutral): only * the base class's own prototype carries the internal `_dispose` reaper the * sibling flow depends on — a subclass's prototype does not own it. */ function readStdioServerParams(transport) { const proto = Object.getPrototypeOf(transport); if (proto === null || !Object.prototype.hasOwnProperty.call(proto, "_dispose")) return; const params = transport._serverParams; return typeof params === "object" && params !== null && typeof params.command === "string" ? params : void 0; } /** * stdio era negotiation on a DISPOSABLE SIBLING: stdio servers built on SDKs * that terminate on any pre-`initialize` request exit when the probe arrives, * so the probe must not spend the caller's one process life. `server/discover` * runs on a short-lived sibling spawned from the same parameters; the caller's * transport starts exactly once, after the era is known. The sibling is * invisible infrastructure: its stderr is discarded, and it is reaped before * this resolves (signal escalation awaiting process exit — a helper holding * its pipes never blocks disposal). A caller `close()` on the session * transport during the probe aborts the connect with the typed negotiation * error, and the session transport is never started. */ async function negotiateStdioViaSibling(negotiation, sessionTransport, params, deps) { const SiblingTransport = sessionTransport.constructor; const sibling = new SiblingTransport({ ...params, stderr: "ignore" }); const originalClose = sessionTransport.close; let callerClosed = false; let signalClosed; const closedSignal = new Promise((_, reject) => { signalClosed = () => reject(callerCloseAbortError()); }); sessionTransport.close = async function() { callerClosed = true; signalClosed?.(); return originalClose.call(sessionTransport); }; let result; try { const negotiated = negotiateEra(negotiation, { ...deps, transport: sibling, transportKind: "stdio", disposableProbe: true }); negotiated.catch(() => {}); result = await Promise.race([negotiated, closedSignal]); } finally { await disposeSibling(sibling); sessionTransport.close = originalClose; } if (callerClosed) throw callerCloseAbortError(); return result; } /** The typed abort for a caller `close()` landing while the sibling probes. */ function callerCloseAbortError() { return new require_src.SdkError(require_src.SdkErrorCode.EraNegotiationFailed, "Version negotiation failed: the transport was closed during the server/discover probe"); } /** Reap a probe sibling — best-effort; a sibling that cannot be reaped must not turn a settled verdict into an error. */ async function disposeSibling(sibling) { try { const dispose = sibling._dispose; await (typeof dispose === "function" ? dispose.call(sibling) : sibling.close()); } catch {} } //#endregion //#region src/client/client.ts /** * The server identity a `DiscoverResult` carries in * `_meta['io.modelcontextprotocol/serverInfo']` (a spec SHOULD — absent means * the server offered no identity). Runtime-validated with the spec-type * guard because `connect({ prior })` accepts caller-supplied values that * never saw a wire parse. */ function serverInfoFromDiscover(discover) { const fromMeta = discover._meta?.[_modelcontextprotocol_core_internal.SERVER_INFO_META_KEY]; return require_src.isSpecType.Implementation(fromMeta) ? fromMeta : void 0; } /** * Elicitation default application helper. Applies defaults to the `data` based on the `schema`. * * @param schema - The schema to apply defaults to. * @param data - The data to apply defaults to. */ function applyElicitationDefaults(schema, data) { if (!schema || data === null || typeof data !== "object") return; if (schema.type === "object" && schema.properties && typeof schema.properties === "object") { const obj = data; const props = schema.properties; for (const key of Object.keys(props)) { const propSchema = props[key]; if (obj[key] === void 0 && Object.prototype.hasOwnProperty.call(propSchema, "default")) obj[key] = propSchema.default; if (obj[key] !== void 0) applyElicitationDefaults(propSchema, obj[key]); } } if (Array.isArray(schema.anyOf)) { for (const sub of schema.anyOf) if (typeof sub !== "boolean") applyElicitationDefaults(sub, data); } if (Array.isArray(schema.oneOf)) { for (const sub of schema.oneOf) if (typeof sub !== "boolean") applyElicitationDefaults(sub, data); } } /** * Determines which elicitation modes are supported based on declared client capabilities. * * According to the spec: * - An empty elicitation capability object defaults to form mode support (backwards compatibility) * - URL mode is only supported if explicitly declared * * @param capabilities - The client's elicitation capabilities * @returns An object indicating which modes are supported */ function getSupportedElicitationModes(capabilities) { if (!capabilities) return { supportsFormMode: false, supportsUrlMode: false }; const hasFormCapability = capabilities.form !== void 0; const hasUrlCapability = capabilities.url !== void 0; return { supportsFormMode: hasFormCapability || !hasFormCapability && !hasUrlCapability, supportsUrlMode: hasUrlCapability }; } /** * Rejects malformed persisted blobs with a typed `SdkError` before any state * change: unrecognized `kind`, a legacy verdict also carrying * DiscoverResult-shaped members (a corrupt blob must never era-choose), or a * modern `discover` payload failing the wire path's schema. */ function validatePrior(prior) { if (typeof prior === "object" && prior !== null) { if (prior.kind === "legacy" && !("supportedVersions" in prior) && !("discover" in prior)) return prior; if (prior.kind === "modern" && _modelcontextprotocol_core_internal.DiscoverResultSchema.safeParse(prior.discover).success) return prior; } throw new require_src.SdkError(require_src.SdkErrorCode.EraNegotiationFailed, "connect({ prior }): unrecognized prior — expected { kind: 'modern', discover } or { kind: 'legacy' }"); } /** * `list_changed` notification → response-cache method(s) to evict. `resources` * covers both list verbs (the spec's "relevant notification ⇒ immediately * stale"). */ const LIST_CHANGED_EVICTIONS = { "notifications/tools/list_changed": ["tools/list"], "notifications/prompts/list_changed": ["prompts/list"], "notifications/resources/list_changed": ["resources/list", "resources/templates/list"] }; const DEFAULT_LIST_MAX_PAGES = 64; /** * An MCP client on top of a pluggable transport. * * The client will automatically begin the initialization flow with the server when {@linkcode connect} is called. * * To handle server-initiated requests (sampling, elicitation, roots), call {@linkcode setRequestHandler}. * The client must declare the corresponding capability for the handler to be accepted. For * `sampling/createMessage` and `elicitation/create`, the handler is automatically wrapped with * schema validation for both the incoming request and the returned result. * * Note: the `roots/list` and `sampling/createMessage` handler surfaces (and the corresponding * `roots` and `sampling` capabilities) are deprecated as of protocol version 2026-07-28 * (SEP-2577). They remain functional during the deprecation window (at least twelve months). * Migrate sampling to calling LLM provider APIs directly, and roots to passing paths via tool * parameters, resource URIs, or configuration. * * @example Handling a sampling request * ```ts source="./client.examples.ts#Client_setRequestHandler_sampling" * client.setRequestHandler('sampling/createMessage', async request => { * const lastMessage = request.params.messages.at(-1); * console.log('Sampling request:', lastMessage); * * // In production, send messages to your LLM here * return { * model: 'my-model', * role: 'assistant' as const, * content: { * type: 'text' as const, * text: 'Response from the model' * } * }; * }); * ``` */ var Client = class extends require_src.Protocol { _serverCapabilities; _serverVersion; _capabilities; _instructions; _jsonSchemaValidator; /** * The response-cache substrate. Owns the backing store, the per-method * eviction-generation counter, the user-supplied/default flag, and the * stamp-memoized derived `name → Tool` / `name → output-validator` * indices — the cache-coordination state that used to live as separate * private fields here. The internal aggregating walk writes one entry per * list verb; `list_changed` evicts the matching method; * `_resetConnectionState` resets the lot. {@linkcode callTool}'s * output-schema validation reads the derived `outputValidator` index (the * substrate's first production caller); the stacked SEP-2243 PR wires * `Mcp-Param-*` mirroring through `toolDefinition` on top. */ _cache; _defaultCacheTtlMs; _listMaxPages; _listChangedDebounceTimers = /* @__PURE__ */ new Map(); /** * The constructor `listChanged` configuration. Durable across reconnects: * read fresh on every connect (legacy or modern), never consumed. */ _listChangedConfig; _enforceStrictCapabilities; _versionNegotiation; _supportedProtocolVersionsOption; _inputRequiredDriverConfig; /** * Active subscriptions/listen state, keyed by subscription id (= the * listen request's JSON-RPC id verbatim). The id is a STRING from a * Client-owned counter (`'listen:' + N`) — JSON-RPC permits string ids, * and Protocol's numeric `_requestMessageId` counter only ever issues * numbers, so listen ids cannot collide with ordinary request ids. */ _listenState = /* @__PURE__ */ new Map(); _nextListenId = 0; /** The auto-opened subscription backing ClientOptions.listChanged on a modern connection. */ _autoOpenedSubscription; /** Backing store for {@linkcode getDiscoverResult}. Per-connection. */ _discoverResult; /** * Clears every per-connection field in one place. Called at the start of * each fresh (non-resuming) connect and from `close()`, so a stale * negotiated era / server identity / auto-opened subscription cannot * survive a reconnect. */ _resetConnectionState() { this._negotiatedProtocolVersion = void 0; this._serverCapabilities = void 0; this._serverVersion = void 0; this._instructions = void 0; this._discoverResult = void 0; this._autoOpenedSubscription = void 0; if (this._listenState.size > 0) { const reason = new require_src.SdkError(require_src.SdkErrorCode.ConnectionClosed, "subscriptions/listen: client reconnected or closed; subscription state from the previous connection was reset"); for (const entry of this._listenState.values()) entry.settle({ cause: "remote", error: reason }); } this._listenState.clear(); for (const timer of this._listChangedDebounceTimers.values()) clearTimeout(timer); this._listChangedDebounceTimers.clear(); this._cache.resetForReconnect(); } async close() { try { await super.close(); } finally { this._resetConnectionState(); } } /** * Initializes this client with the given name and version information. */ constructor(_clientInfo, options) { super(options); this._clientInfo = _clientInfo; this._capabilities = options?.capabilities ? { ...options.capabilities } : {}; this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new _modelcontextprotocol_client__shims.DefaultJsonSchemaValidator(); this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false; this._versionNegotiation = options?.versionNegotiation; this._supportedProtocolVersionsOption = options?.supportedProtocolVersions; this._inputRequiredDriverConfig = require_src.resolveInputRequiredDriverConfig(options?.inputRequired); this._cache = new ClientResponseCache(options?.responseCacheStore ?? new InMemoryResponseCacheStore(), options?.responseCacheStore !== void 0, (error) => this._reportStoreError(error), options?.cachePartition ?? ""); this._defaultCacheTtlMs = options?.defaultCacheTtlMs ?? 0; this._listMaxPages = options?.listMaxPages ?? DEFAULT_LIST_MAX_PAGES; if (options?.listChanged) this._listChangedConfig = options.listChanged; } buildContext(ctx, _transportInfo) { return ctx; } /** * Era-keyed direction enforcement for inbound traffic on channels whose * transport does not classify (e.g. stdio): the 2026-07-28 era has no * server→client JSON-RPC request channel — server-to-client interactions * are carried in-band in `input_required` results — and on stdio the * client must never write JSON-RPC responses. An inbound request arriving * on a connection that negotiated a modern era is therefore dropped * (surfaced via `onerror`) rather than answered. Connections on a legacy * era — and all responses and notifications — keep today's dispatch path. */ _shouldDropInbound(message) { if (this._negotiatedProtocolVersion !== void 0 && require_src.isModernProtocolVersion(this._negotiatedProtocolVersion) && require_src.isJSONRPCRequest(message)) return "drop"; } /** * Per-request `_meta` envelope auto-emission (protocol revision 2026-07-28): * on a connection that negotiated a modern era — auto-negotiated or pinned — * every outgoing request and notification automatically carries the reserved * protocol-version / client-info / client-capabilities `_meta` keys (the * same envelope the connect-time `server/discover` probe sends). * User-supplied `_meta` keys take precedence over the auto-attached ones. * * Legacy-era connections return `undefined`: the envelope seam is a no-op * and outbound traffic is byte-identical to a 2025 client (the legacy * `'auto'` fallback included). */ _outboundMetaEnvelope() { const version = this._negotiatedProtocolVersion; if (version === void 0) return void 0; return this._wireCodec().outboundEnvelope({ protocolVersion: version, clientInfo: this._clientInfo, clientCapabilities: this._capabilities }); } /** * Wires the multi-round-trip auto-fulfilment engine (protocol revision * 2026-07-28) into the response funnel: an `input_required` answer is * fulfilled through the registered elicitation/sampling/roots handlers * and the original request retried via `flow.retry`, up to * `inputRequired.maxRounds` rounds. With auto-fulfilment disabled the * response surfaces as a typed error steering to manual mode. */ _resolveNonCompleteResult(decoded, flow) { if (!this._inputRequiredDriverConfig.autoFulfill) return Promise.reject(new require_src.SdkError(require_src.SdkErrorCode.UnsupportedResultType, `Unsupported result type 'input_required' for ${flow.request.method}: multi-round-trip auto-fulfilment is not enabled on this instance — pass allowInputRequired: true to handle it manually, or enable inputRequired.autoFulfill`, { resultType: "input_required", method: flow.request.method })); return require_src.runInputRequiredFlow({ getRequestHandler: (method) => this._getRequestHandler(method), buildContext: (baseCtx) => this.buildContext(baseCtx, void 0), sessionId: this.transport?.sessionId }, this._inputRequiredDriverConfig, decoded, flow); } /** * Set up handlers for list changed notifications based on config and server capabilities. * This should only be called after initialization when server capabilities are known. * Handlers are silently skipped if the server doesn't advertise the corresponding listChanged capability. * @internal */ _setupListChangedHandlers(config) { if (config.tools && this._serverCapabilities?.tools?.listChanged) this._setupListChangedHandler("tools", "notifications/tools/list_changed", config.tools, async () => { return (await this.listTools(void 0, { cacheMode: "refresh" })).tools; }); if (config.prompts && this._serverCapabilities?.prompts?.listChanged) this._setupListChangedHandler("prompts", "notifications/prompts/list_changed", config.prompts, async () => { return (await this.listPrompts(void 0, { cacheMode: "refresh" })).prompts; }); if (config.resources && this._serverCapabilities?.resources?.listChanged) this._setupListChangedHandler("resources", "notifications/resources/list_changed", config.resources, async () => { return (await this.listResources(void 0, { cacheMode: "refresh" })).resources; }); } /** * Registers new capabilities. This can only be called before connecting to a transport. * * The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization). */ registerCapabilities(capabilities) { if (this.transport) throw new Error("Cannot register capabilities after connecting to transport"); this._capabilities = require_src.mergeCapabilities(this._capabilities, capabilities); } /** * Configure protocol version negotiation before connecting (equivalent to * passing `versionNegotiation` at construction time). Can only be called * before connecting to a transport. Passing `undefined` clears a previously * configured negotiation, restoring the default `'legacy'` posture. * * See {@linkcode ClientOptions | ClientOptions.versionNegotiation} for the mode semantics. */ setVersionNegotiation(options) { if (this.transport) throw new Error("Cannot configure version negotiation after connecting to transport"); this._versionNegotiation = options; } /** * Enforces client-side validation for `elicitation/create` and `sampling/createMessage` * regardless of how the handler was registered. */ _wrapHandler(method, handler) { if (method === "elicitation/create") return async (request, ctx) => { const codec = require_src.codecForVersion(this._negotiatedProtocolVersion); let validatedRequest = codec.validateRequest("elicitation/create", request); if (!validatedRequest.ok && validatedRequest.reason === "not-in-era") validatedRequest = codec.validateInputRequest("elicitation/create", request); if (!validatedRequest.ok) throw new require_src.ProtocolError(validatedRequest.reason === "not-in-era" ? require_src.ProtocolErrorCode.InternalError : require_src.ProtocolErrorCode.InvalidParams, validatedRequest.reason === "not-in-era" ? "No wire schema for elicitation/create in the resolved era" : `Invalid elicitation request: ${validatedRequest.message}`); const { params } = validatedRequest.value; params.mode = params.mode ?? "form"; const { supportsFormMode, supportsUrlMode } = getSupportedElicitationModes(this._capabilities.elicitation); if (params.mode === "form" && !supportsFormMode) throw new require_src.ProtocolError(require_src.ProtocolErrorCode.InvalidParams, "Client does not support form-mode elicitation requests"); if (params.mode === "url" && !supportsUrlMode) throw new require_src.ProtocolError(require_src.ProtocolErrorCode.InvalidParams, "Client does not support URL-mode elicitation requests"); const result = await handler(request, ctx); let validationResult = codec.validateResult("elicitation/create", result); if (!validationResult.ok && validationResult.reason === "not-in-era") validationResult = codec.validateInputResponse("elicitation/create", result); if (!validationResult.ok) throw new require_src.ProtocolError(validationResult.reason === "not-in-era" ? require_src.ProtocolErrorCode.InternalError : require_src.ProtocolErrorCode.InvalidParams, validationResult.reason === "not-in-era" ? "No wire schema for elicitation/create in the resolved era" : `Invalid elicitation result: ${validationResult.message}`); const validatedResult = validationResult.value; const requestedSchema = params.mode === "form" ? params.requestedSchema : void 0; if (params.mode === "form" && validatedResult.action === "accept" && validatedResult.content && requestedSchema && this._capabilities.elicitation?.form?.applyDefaults) try { applyElicitationDefaults(requestedSchema, validatedResult.content); } catch {} return validatedResult; }; if (method === "sampling/createMessage") return async (request, ctx) => { const codec = require_src.codecForVersion(this._negotiatedProtocolVersion); let validatedRequest = codec.validateRequest("sampling/createMessage", request); if (!validatedRequest.ok && validatedRequest.reason === "not-in-era") validatedRequest = codec.validateInputRequest("sampling/createMessage", request); if (!validatedRequest.ok) throw new require_src.ProtocolError(validatedRequest.reason === "not-in-era" ? require_src.ProtocolErrorCode.InternalError : require_src.ProtocolErrorCode.InvalidParams, validatedRequest.reason === "not-in-era" ? "No wire schema for sampling/createMessage in the resolved era" : `Invalid sampling request: ${validatedRequest.message}`); const { params } = validatedRequest.value; const result = await handler(request, ctx); const hasTools = Boolean(params.tools || params.toolChoice); let validatedResult = codec.samplingResultVariant(hasTools, result); if (!validatedResult.ok && validatedResult.reason === "not-in-era") validatedResult = codec.validateInputResponse("sampling/createMessage", result); if (!validatedResult.ok) throw new require_src.ProtocolError(validatedResult.reason === "not-in-era" ? require_src.ProtocolErrorCode.InternalError : require_src.ProtocolErrorCode.InvalidParams, validatedResult.reason === "not-in-era" ? "No result schema for sampling/createMessage in the resolved era" : `Invalid sampling result: ${validatedResult.message}`); return validatedResult.value; }; return handler; } assertCapability(capability, method) { if (!this._serverCapabilities?.[capability]) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Server does not support ${capability} (required for ${method})`); } /** * Connects to a server via the given transport and performs the MCP initialization handshake. * * @example Basic usage (stdio) * ```ts source="./client.examples.ts#Client_connect_stdio" * const client = new Client({ name: 'my-client', version: '1.0.0' }); * const transport = new StdioClientTransport({ command: 'my-mcp-server' }); * await client.connect(transport); * ``` * * @example Streamable HTTP with SSE fallback * ```ts source="./client.examples.ts#Client_connect_sseFallback" * const baseUrl = new URL(url); * * try { * // Try modern Streamable HTTP transport first * const client = new Client({ name: 'my-client', version: '1.0.0' }); * const transport = new StreamableHTTPClientTransport(baseUrl); * await client.connect(transport); * return { client, transport }; * } catch { * // Fall back to legacy SSE transport * const client = new Client({ name: 'my-client', version: '1.0.0' }); * const transport = new SSEClientTransport(baseUrl); * await client.connect(transport); * return { client, transport }; * } * ``` */ async connect(transport, options) { if (options?.prior != null) return this._connectFromPrior(transport, validatePrior(options.prior), options); const negotiation = resolveVersionNegotiation(this._versionNegotiation, this._supportedProtocolVersionsOption); if (negotiation.kind !== "legacy") return this._connectNegotiated(transport, negotiation, options); return this._connectPlainLegacy(transport, options); } /** * Plain legacy connect — the pinned 2025 sequence, byte-untouched. The * `mode: 'legacy'` connect body, shared with the `prior` legacy verdict. */ async _connectPlainLegacy(transport, options) { await super.connect(transport); if (transport.sessionId !== void 0) { const negotiatedProtocolVersion = this._negotiatedProtocolVersion; if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion); return; } this._resetConnectionState(); await this._legacyHandshake(transport, options); } /** * The 2025 `initialize` handshake — the body of the plain legacy connect and * the `'auto'`-mode fallback path (same `initialize` body, zero 2026 headers; * on the stdio sibling path it opens the session child's fresh pipe, in the * in-place modes it rides the probed connection). Callers clear the negotiated protocol version before * the handshake; its completion sets the negotiated (legacy) version. */ async _legacyHandshake(transport, options) { const legacyVersions = require_src.legacyProtocolVersions(this._supportedProtocolVersions); try { const offeredVersion = legacyVersions[0]; if (offeredVersion === void 0) throw new require_src.SdkError(require_src.SdkErrorCode.EraNegotiationFailed, "Cannot run the initialize handshake: supportedProtocolVersions contains no pre-2026-07-28 protocol version"); const result = await this.request({ method: "initialize", params: { protocolVersion: offeredVersion, capabilities: this._capabilities, clientInfo: this._clientInfo } }, options); if (result === void 0) throw new Error(`Server sent invalid initialize result: ${result}`); if (!legacyVersions.includes(result.protocolVersion)) throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); this._serverCapabilities = result.capabilities; this._serverVersion = result.serverInfo; this._cache.setServerIdentity(this._deriveServerIdentity(transport)); if (transport.setProtocolVersion) transport.setProtocolVersion(result.protocolVersion); this._instructions = result.instructions; await this.notification({ method: "notifications/initialized" }); this._negotiatedProtocolVersion = result.protocolVersion; if (this._listChangedConfig) this._setupListChangedHandlers(this._listChangedConfig); } catch (error) { this.close(); throw error; } } /** * Negotiated connect (mode `'auto'` or `{ pin }`): probe with `server/discover` * before the Protocol machinery attaches — on a disposable sibling process for * the SDK's stdio transport, in place otherwise — then either establish the * modern era or perform the plain legacy handshake. */ async _connectNegotiated(transport, negotiation, options) { if (transport.sessionId !== void 0) { await super.connect(transport); const negotiatedProtocolVersion = this._negotiatedProtocolVersion; if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion); return; } this._resetConnectionState(); let result; try { const transportKind = detectProbeTransportKind(transport); const baseDeps = { clientInfo: this._clientInfo, capabilities: this._capabilities, environment: detectProbeEnvironment(), defaultTimeoutMs: options?.timeout ?? require_src.DEFAULT_REQUEST_TIMEOUT_MSEC }; const stdioParams = transportKind === "stdio" ? readStdioServerParams(transport) : void 0; result = stdioParams === void 0 ? await negotiateEra(negotiation, { ...baseDeps, transport, transportKind }) : await negotiateStdioViaSibling(negotiation, transport, stdioParams, baseDeps); } catch (error) { await transport.close().catch(() => {}); disarmSpentCloseGuard(transport); throw error; } disarmSpentCloseGuard(transport); await super.connect(transport); if (result.era === "legacy") { await this._legacyHandshake(transport, options); return; } this._serverCapabilities = result.discover.capabilities; this._serverVersion = serverInfoFromDiscover(result.discover); this._cache.setServerIdentity(this._deriveServerIdentity(transport)); this._instructions = result.discover.instructions; this._discoverResult = result.discover; this._negotiatedProtocolVersion = result.version; if (transport.setProtocolVersion) transport.setProtocolVersion(result.version); if (this._listChangedConfig) { const config = this._listChangedConfig; const advertised = this._serverCapabilities; const effective = { ...config.tools && advertised?.tools?.listChanged && { tools: config.tools }, ...config.prompts && advertised?.prompts?.listChanged && { prompts: config.prompts }, ...config.resources && advertised?.resources?.listChanged && { resources: config.resources } }; let handlersRegistered = true; try { this._setupListChangedHandlers(effective); } catch (error) { handlersRegistered = false; this.onerror?.(error instanceof Error ? error : new Error(String(error))); } const filter = handlersRegistered ? { ...effective.tools && { toolsListChanged: true }, ...effective.prompts && { promptsListChanged: true }, ...effective.resources && { resourcesListChanged: true } } : {}; if (Object.keys(filter).length > 0) { const ackAbort = new AbortController(); const onConnectAbort = () => ackAbort.abort(options?.signal?.reason); if (options?.signal?.aborted) onConnectAbort(); options?.signal?.addEventListener("abort", onConnectAbort); try { this._autoOpenedSubscription = await this.listen(filter, { timeout: options?.timeout, signal: ackAbort.signal }); } catch (error) { if (options?.signal?.aborted) { await this.close().catch(() => {}); throw error; } this.onerror?.(error instanceof Error ? error : new Error(String(error))); } finally { options?.signal?.removeEventListener("abort", onConnectAbort); } } } } /** * Connect from a validated {@linkcode PriorDiscovery}: the modern arm * adopts the `DiscoverResult` (zero round trips; `EraNegotiationFailed` * on no 2026-07-28+ overlap), the legacy arm runs the plain legacy connect. */ async _connectFromPrior(transport, prior, options) { if (prior.kind === "legacy") return this._connectPlainLegacy(transport, options); const discover = prior.discover; this._resetConnectionState(); const explicit = this._supportedProtocolVersionsOption; const version = (explicit && require_src.modernProtocolVersions(explicit).length > 0 ? require_src.modernProtocolVersions(explicit) : require_src.SUPPORTED_MODERN_PROTOCOL_VERSIONS).find((v) => discover.supportedVersions.includes(v)); if (version === void 0) throw new require_src.SdkError(require_src.SdkErrorCode.EraNegotiationFailed, "connect({ prior }) with a modern verdict requires a 2026-07-28+ mutual protocol version; the supplied DiscoverResult and this client's supportedProtocolVersions have no modern overlap. For a server known to be legacy, pass prior: { kind: 'legacy' } to skip the probe and initialize directly, or use versionNegotiation: { mode: 'auto' } to re-probe with legacy fallback."); await super.connect(transport); this._discoverResult = discover; this._serverCapabilities = discover.capabilities; this._serverVersion = serverInfoFromDiscover(discover); this._cache.setServerIdentity(this._deriveServerIdentity(transport)); this._instructions = discover.instructions; this._negotiatedProtocolVersion = version; transport.setProtocolVersion?.(version); if (this._listChangedConfig) try { this._setupListChangedHandlers(this._listChangedConfig); } catch (error) { this.onerror?.(error instanceof Error ? error : new Error(String(error))); } } /** * After initialization has completed, this will be populated with the server's reported capabilities. */ getServerCapabilities() { return this._serverCapabilities; } /** * The connected server's self-reported name and version, when it * identified itself: required on the legacy `initialize` result; a spec * SHOULD in the discover result's `_meta` on 2026-07-28, so a successful * modern connect against an anonymous server leaves this `undefined`. */ getServerVersion() { return this._serverVersion; } /** * The connected server's identity for response-cache partitioning. The * `serverInfo` `name@version` pair when available (required on * `initialize`; a SHOULD in the discover result's `_meta` since spec PR * #3002); falls back to the transport's `sessionId`, then to a * per-connection surrogate. The surrogate matters since #3002 made * identity optional: without it, two identity-less servers reached over * sessionId-less transports would share the cache's pre-connect `''` * partition and read each other's entries — no stable identity means no * cross-connection cache reuse. The value itself is server-controlled — * the collision-safety of the storage partition comes from * {@linkcode ClientResponseCache}'s JSON-array encoding around it, not * from any character it does or does not contain. */ _deriveServerIdentity(transport) { const v = this._serverVersion; if (v !== void 0) return `${v.name}@${v.version}`; return transport.sessionId ?? `anonymous:${Date.now()}-${Math.random().toString(36).slice(2)}`; } /** * After initialization has completed, this will be populated with the protocol version negotiated * during the initialize handshake. When manually reconstructing a transport for reconnection, pass this * value to the new transport so it continues sending the required `mcp-protocol-version` header. */ getNegotiatedProtocolVersion() { return this._negotiatedProtocolVersion; } /** * After initialization has completed, this returns the protocol era of the * connection: `'modern'` when the connection negotiated a 2026-07-28+ * revision (via `server/discover`), `'legacy'` for the 2025-era * `initialize` handshake, or `undefined` before the connection is * established. */ getProtocolEra() { const version = this._negotiatedProtocolVersion; if (version === void 0) return void 0; return require_src.isModernProtocolVersion(version) ? "modern" : "legacy"; } /** * After initialization has completed, this may be populated with information about the server's instructions. */ getInstructions() { return this._instructions; } /** * The {@linkcode DiscoverResult} from the last `'auto'`/pinned probe, * {@linkcode discover} call, or `connect({ prior })` that adopted a * modern verdict (a legacy verdict leaves this `undefined` — there is no * `DiscoverResult` on that path). Persistable via `JSON.stringify`; wrap * as `{ kind: 'modern', discover }` and feed to {@linkcode ConnectOptions} * `prior`. */ getDiscoverResult() { return this._discoverResult; } assertCapabilityForMethod(method) { switch (method) { case "logging/setLevel": if (!this._serverCapabilities?.logging) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Server does not support logging (required for ${method})`); break; case "prompts/get": case "prompts/list": if (!this._serverCapabilities?.prompts) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Server does not support prompts (required for ${method})`); break; case "resources/list": case "resources/templates/list": case "resources/read": case "resources/subscribe": case "resources/unsubscribe": if (!this._serverCapabilities?.resources) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Server does not support resources (required for ${method})`); if (method === "resources/subscribe" && !this._serverCapabilities.resources.subscribe) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Server does not support resource subscriptions (required for ${method})`); break; case "tools/call": case "tools/list": if (!this._serverCapabilities?.tools) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Server does not support tools (required for ${method})`); break; case "completion/complete": if (!this._serverCapabilities?.completions) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Server does not support completions (required for ${method})`); break; case "initialize": break; case "server/discover": break; case "ping": break; } } assertNotificationCapability(method) { switch (method) { case "notifications/roots/list_changed": if (!this._capabilities.roots?.listChanged) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Client does not support roots list changed notifications (required for ${method})`); break; case "notifications/initialized": break; case "notifications/cancelled": break; case "notifications/progress": break; } } assertRequestHandlerCapability(method) { switch (method) { case "sampling/createMessage": if (!this._capabilities.sampling) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Client does not support sampling capability (required for ${method})`); break; case "elicitation/create": if (!this._capabilities.elicitation) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Client does not support elicitation capability (required for ${method})`); break; case "roots/list": if (!this._capabilities.roots) throw new require_src.SdkError(require_src.SdkErrorCode.CapabilityNotSupported, `Client does not support roots capability (required for ${method})`); break; case "ping": break; } } async ping(options) { return this.request({ method: "ping" }, options); } /** * Send `server/discover` (2026-07-28+) and record the result for * {@linkcode getDiscoverResult}. */ async discover(options) { const result = await this._requestWithSchema({ method: "server/discover" }, _modelcontextprotocol_core_internal.DiscoverResultSchema, options); this._discoverResult = result; return result; } /** Requests argument autocompletion suggestions from the server for a prompt or resource. */ async complete(params, options) { return this.request({ method: "completion/complete", params }, options); } /** * Sets the minimum severity level for log messages sent by the server. * * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). * Remains functional during the deprecation window (at least twelve months). * Migrate to stderr logging (STDIO servers) or OpenTelemetry. */ async setLoggingLevel(level, options) { return this.request({ method: "logging/setLevel", params: { level } }, options); } /** Retrieves a prompt by name from the server, passing the given arguments for template substitution. */ async getPrompt(params, options) { return this.request({ method: "prompts/get", params }, options); } /** * Lists available prompts. * * Called without a `cursor` (the common case), this walks every page and * returns the complete aggregated list with no `nextCursor`; the * aggregate is also written to the {@linkcode ResponseCacheStore}. Pass an * explicit `{ cursor }` to fetch a single page and walk pagination * yourself — the per-page path returns the server's raw page (with * `nextCursor` for the next call) and does not write the response cache. * The auto-aggregate path is capped by * {@linkcode ClientOptions | ClientOptions.listMaxPages} (default 64); the per-page path * is not. * * Returns an empty list if the server does not advertise prompts capability * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled). * * @example * ```ts source="./client.examples.ts#Client_listPrompts_pagination" * // No cursor → all pages aggregated for you. * const { prompts } = await client.listPrompts(); * console.log( * 'Available prompts:', * prompts.map(p => p.name) * ); * ``` */ async listPrompts(params, options) { if (!this._serverCapabilities?.prompts && !this._enforceStrictCapabilities) { console.debug("Client.listPrompts() called but server does not advertise prompts capability - returning empty list"); return { prompts: [] }; } if (params?.cursor !== void 0) return this.request({ method: "prompts/list", params }, options); const hit = await this._serveFromCache("prompts/list", void 0, options); if (hit !== void 0) return hit; return this._listAllPages("prompts/list", params, options, (r) => r.prompts); } /** * Lists available resources. * * Called without a `cursor` (the common case), this walks every page and * returns the complete aggregated list with no `nextCursor`; the * aggregate is also written to the {@linkcode ResponseCacheStore}. Pass an * explicit `{ cursor }` to fetch a single page and walk pagination * yourself — the per-page path returns the server's raw page (with * `nextCursor` for the next call) and does not write the response cache. * The auto-aggregate path is capped by * {@linkcode ClientOptions | ClientOptions.listMaxPages} (default 64); the per-page path * is not. * * Returns an empty list if the server does not advertise resources capability * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled). * * @example * ```ts source="./client.examples.ts#Client_listResources_pagination" * // No cursor → all pages aggregated for you. * const { resources } = await client.listResources(); * console.log( * 'Available resources:', * resources.map(r => r.name) * ); * ``` */ async listResources(params, options) { if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) { console.debug("Client.listResources() called but server does not advertise resources capability - returning empty list"); return { resources: [] }; } if (params?.cursor !== void 0) return this.request({ method: "resources/list", params }, options); const hit = await this._serveFromCache("resources/list", void 0, options); if (hit !== void 0) return hit; return this._listAllPages("resources/list", params, options, (r) => r.resources); } /** * Lists available resource URI templates for dynamic resources. * * Called without a `cursor`, this walks every page and returns the * complete aggregated list with no `nextCursor`; the aggregate is * also written to the {@linkcode ResponseCacheStore}. Pass an explicit * `{ cursor }` to fetch a single page — see * {@linkcode listResources | listResources()} for the per-page contract. * * Returns an empty list if the server does not advertise resources capability * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled). */ async listResourceTemplates(params, options) { if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) { console.debug("Client.listResourceTemplates() called but server does not advertise resources capability - returning empty list"); return { resourceTemplates: [] }; } if (params?.cursor !== void 0) return this.request({ method: "resources/templates/list", params }, options); const hit = await this._serveFromCache("resources/templates/list", void 0, options); if (hit !== void 0) return hit; return this._listAllPages("resources/templates/list", params, options, (r) => r.resourceTemplates); } /** * Walk every page of a paginated list verb, aggregate, and write ONE * entry to the response cache. Internal — backs the public `list*` * methods' no-`cursor` auto-aggregate path. Page 1's result object is * mutated in place (its items array is extended; `nextCursor` is * cleared); page-1 metadata (`ttlMs`, `cacheScope`, `_meta`) is preserved. * A page with the same items and `nextCursor` as the previous page ends the walk; * {@linkcode ClientOptions.listMaxPages} is a hard cap — hitting it * throws, so a partial aggregate is never cached. The * captured-generation guard skips the write when a `list_changed` landed * mid-walk, so the eviction is never overwritten by a stale aggregate. * `finalize` runs on the complete aggregate before the cache write — the * SEP-2243 invalid-`x-mcp-header` exclusion hooks here so the cached * `tools/list` entry is already filtered. * * The caller's `baseParams` (everything except `cursor`) is threaded into * every page request — page 1 sends `{...baseParams}`, later pages * `{...baseParams, cursor}` — so a typed, documented `_meta` (e.g. W3C * trace context) supplied to the public `list*()` reaches every wire * request the walk issues. */ async _listAllPages(method, baseParams, options, items, finalize) { const bypass = options?.cacheMode === "bypass"; const generation = this._cache.captureGeneration(method); const acc = await this.request({ method, ...baseParams && { params: { ...baseParams } } }, options); let cursor = acc.nextCursor; let previous = acc; let pages = 1; while (cursor !== void 0) { if (this._listMaxPages !== 0 && pages >= this._listMaxPages) throw new require_src.SdkError(require_src.SdkErrorCode.ListPaginationExceeded, `${method}: exceeded listMaxPages (${this._listMaxPages}); server pagination did not terminate`, { method, listMaxPages: this._listMaxPages }); const page = await this.request({ method, params: { ...baseParams, cursor } }, options); if (page.nextCursor === cursor && JSON.stringify(items(page)) === JSON.stringify(items(previous))) break; items(acc).push(...items(page)); previous = page; cursor = page.nextCursor; pages++; } delete acc.nextCursor; finalize?.(acc); if (bypass) return acc; await this._cache.write(method, acc, generation, this._freshness(acc)); return acc; } /** * Compute the {@linkcode ClientResponseCache.write} freshness payload from * a cacheable result body. The single seam through which the client reads * `ttlMs`/`cacheScope` (mcp.d's `cachedFetch` engine). The fields pass * through the loose result schema, so they are read off the runtime body; * a missing `ttlMs` falls back to * {@linkcode ClientOptions | ClientOptions.defaultCacheTtlMs}; an explicit server-sent * `ttlMs` (including `0` — the spec's "immediately stale") is honoured * as-is. The default of `0` means `expiresAt === now()` ⇒ never served, * only stored. A missing `cacheScope` is treated as `'private'` — the * spec's `'public'` grant ("any client … MAY serve to any user") is too * strong to infer by default, and matches this SDK's server-side stamp * default. */ _freshness(result, params) { const body = result; const ttlMs = typeof body.ttlMs === "number" ? body.ttlMs : this._defaultCacheTtlMs; const scope = body.cacheScope === "public" ? "public" : "private"; return { expiresAt: this._cache.now() + Math.min(Math.max(0, ttlMs), MAX_CACHE_TTL_MS), scope, params }; } /** * The cache-serving front of every cacheable verb (mcp.d's `cachedFetch` * read half): under `cacheMode: 'use'` (the default), a fresh held entry * is served and the round trip is skipped. `'refresh'` and `'bypass'` * always fetch (the caller decides whether to write). Freshness and * decoding live in {@linkcode ClientResponseCache.read}; every hit is * freshly parsed, so the caller owns it outright. A custom store * whose `get()` rejects is routed to `onerror` and treated as a miss — * cache bookkeeping never blocks a request from reaching the wire. */ async _serveFromCache(method, params, options) { if (options?.cacheMode === "bypass" || options?.cacheMode === "refresh") return void 0; const hit = await this._cache.read(method, params).catch((error) => void this._reportStoreError(error)); if (hit !== void 0) { if (options?.signal?.aborted) { const reason = options.signal.reason; throw reason instanceof require_src.SdkError ? reason : new require_src.SdkError(require_src.SdkErrorCode.RequestTimeout, String(reason)); } return hit.value; } } /** Route a custom-store failure to `onerror` without aborting the surrounding dispatch. */ _reportStoreError(e) { this.onerror?.(e instanceof Error ? e : new Error(String(e))); } /** * Compile a single tool's `outputSchema`. Passed as the compile callback to * {@linkcode ClientResponseCache.outputValidator} so the cache class stays * free of any validator-provider dependency, and called directly for the * `options.toolDefinition` path of {@linkcode callTool} (a one-off * caller-supplied definition is compiled in isolation and never enters the * cache, so it cannot poison the listed tool of the same name). * * Returns `undefined` when the tool has no `outputSchema`, or a * discriminated `{ok}` result otherwise. SEP-2106: ANY throw from the * validator engine — unsupported `$schema` dialect, invalid `pattern` * regex, unresolvable `$ref`, or any other engine error — is captured as * `{ok: false, compileError}` so one bad schema does not poison the rest * of the listing; `callTool()` surfaces it as an `InvalidParams` error * before the request. The `{ok}` discriminator (not * `compileError !== undefined`) means a custom provider that does * `throw undefined` is still treated as a captured failure. */ _compileOutputValidator(tool) { if (!tool.outputSchema) return void 0; try { return { ok: true, validator: this._jsonSchemaValidator.getValidator(tool.outputSchema) }; } catch (error) { return { ok: false, compileError: error }; } } /** * Resolve the SEP-2243 `x-mcp-header` declaration scan for a tool name. * * The caller-supplied `toolDefinition` escape hatch wins; otherwise the * cached `tools/list` entry (via the cache's `toolDefinition`) is the * source. Freshness is the response cache's lifecycle: `list_changed` * evicts, otherwise the held schema is the best information available * regardless of age, and a stale schema is recovered through the * `HEADER_MISMATCH` → evict-refetch-retry path in {@linkcode callTool}. * On a miss the call proceeds without `Mcp-Param-*` headers (the spec's * "client SHOULD send without custom headers" guidance) and relies on the * same recovery. */ async _resolveXMcpHeaderScan(name, override) { const tool = override ?? await this._cache.toolDefinition(name); return tool === void 0 ? void 0 : require_src.scanXMcpHeaderDeclarations(tool.inputSchema); } /** * Reads the contents of a resource by URI. * * Honours the result's `ttlMs`/`cacheScope` (SEP-2549): a still-fresh * cached body for the same `uri` is returned without a round trip * (`cacheMode: 'use'`, the default). The cache key is `{method, uri}` * partitioned by the resolved scope — `'private'` (the default when the * server omits the field) is stored under this client's * {@linkcode ClientOptions | ClientOptions.cachePartition}, so a shared * store cannot serve one principal's resource body to another. Unlike the * list verbs, a result whose resolved TTL is ≤0 is **not** stored * (`resources/read` has no derived index and the URI keyspace is * unbounded). */ async readResource(params, options) { const hit = await this._serveFromCache("resources/read", params.uri, options); if (hit !== void 0) return hit; const generation = this._cache.captureGeneration("resources/read", params.uri); const result = await this.request({ method: "resources/read", params }, options); if (options?.cacheMode !== "bypass") { const freshness = this._freshness(result, params.uri); if (freshness.expiresAt > this._cache.now()) await this._cache.write("resources/read", result, generation, freshness); else if (options?.cacheMode === "refresh") await this._cache.evictKey("resources/read", params.uri); } return result; } /** Subscribes to change notifications for a resource. The server must support resource subscriptions. */ async subscribeResource(params, options) { return this.request({ method: "resources/subscribe", params }, options); } /** Unsubscribes from change notifications for a resource. */ async unsubscribeResource(params, options) { return this.request({ method: "resources/unsubscribe", params }, options); } /** * Opens a `subscriptions/listen` stream (protocol revision 2026-07-28). * * Resolves once the server's `notifications/subscriptions/acknowledged` * arrives (the standard request timeout applies to this ack phase). Change * notifications delivered on the stream are dispatched to the existing * {@linkcode setNotificationHandler} registrations — the same handlers the * 2025-era unsolicited notifications fire on a legacy connection — so * `listen()` is era-transparent for consumers that already register those. * * `close()` tears the subscription down by aborting the listen request's * `requestSignal` (closes the SSE stream where the transport honors it) * AND sending `notifications/cancelled` referencing the listen request id * — both, unconditionally, so any spec-compliant server on any transport * sees the cancel. No automatic re-listen — call `listen()` again to * re-establish. * * On a 2025-era connection this throws a typed * {@linkcode SdkErrorCode.MethodNotSupportedByProtocolVersion} steering to * `resources/subscribe` and `ClientOptions.listChanged` (the legacy * unsolicited delivery model still applies there); no transparent shim. */ async listen(filter, options) { if (this.transport === void 0) throw new require_src.SdkError(require_src.SdkErrorCode.NotConnected, "Not connected"); const negotiated = this._negotiatedProtocolVersion; if (negotiated === void 0 || !require_src.isModernProtocolVersion(negotiated)) throw new require_src.SdkError(require_src.SdkErrorCode.MethodNotSupportedByProtocolVersion, `subscriptions/listen requires a 2026-07-28-era connection (negotiated: ${negotiated ?? "none"}). On a 2025-era connection, change notifications are delivered unsolicited: use ClientOptions.listChanged and resources/subscribe instead.`, { method: "subscriptions/listen", protocolVersion: negotiated }); if (options?.signal?.aborted) { const reason = options.signal.reason; throw reason instanceof require_src.SdkError ? reason : new require_src.SdkError(require_src.SdkErrorCode.RequestTimeout, String(reason)); } const requestAbort = new AbortController(); const listenId = `listen:${this._nextListenId++}`; let state = "opening"; let ackTimer; let onCallerAbort; let resolveOpening; let rejectOpening; const opening = new Promise((resolve, reject) => { resolveOpening = resolve; rejectOpening = reject; }); let resolveClosed; const closed = new Promise((resolve) => { resolveClosed = resolve; }); const settle = (outcome) => { if (state === "closed") return; const wasOpening = state === "opening"; if (ackTimer !== void 0) { clearTimeout(ackTimer); ackTimer = void 0; } if ("ack" in outcome) { state = "open"; resolveOpening(outcome.ack); return; } state = "closed"; if (onCallerAbort !== void 0) options?.signal?.removeEventListener("abort", onCallerAbort); this._listenState.delete(listenId); requestAbort.abort(); resolveClosed(outcome.cause); if (wasOpening) rejectOpening(outcome.error ?? new require_src.SdkError(require_src.SdkErrorCode.ConnectionClosed, "subscriptions/listen closed before the server acknowledged")); }; const wireTeardown = async () => { requestAbort.abort(); await this.notification({ method: "notifications/cancelled", params: { requestId: listenId } }).catch(() => {}); }; const close = async () => { if (state === "closed") return; settle({ cause: "local" }); await wireTeardown(); }; this._listenState.set(listenId, { settle }); const ackTimeout = options?.timeout ?? require_src.DEFAULT_REQUEST_TIMEOUT_MSEC; ackTimer = setTimeout(() => { settle({ cause: "remote", error: new require_src.SdkError(require_src.SdkErrorCode.RequestTimeout, "subscriptions/listen ack timed out", { timeout: ackTimeout }) }); wireTeardown().catch(() => {}); }, ackTimeout); if (options?.signal) { const callerSignal = options.signal; onCallerAbort = () => { if (state === "closed") return; const reason = callerSignal.reason; settle({ cause: "local", error: reason instanceof Error ? reason : new Error(String(reason ?? "Aborted")) }); wireTeardown().catch(() => {}); }; callerSignal.addEventListener("abort", onCallerAbort, { once: true }); } const jsonrpcRequest = { jsonrpc: "2.0", id: listenId, method: "subscriptions/listen", params: { _meta: { ...this._outboundMetaEnvelope() }, notifications: filter } }; const routeSendFailure = (error) => { settle({ cause: "remote", error: error instanceof Error ? error : new Error(String(error)) }); }; try { this.transport.send(jsonrpcRequest, { requestSignal: requestAbort.signal, onRequestStreamEnd: () => settle({ cause: "remote", error: /* @__PURE__ */ new Error("subscriptions/listen: stream ended") }) }).catch(routeSendFailure); } catch (error) { routeSendFailure(error); } return { honoredFilter: await opening, close, closed }; } /** * The subscription auto-opened by `ClientOptions.listChanged` on a modern * connection — the listen filter is the intersection of the configured * sub-options and the server-advertised `listChanged` capabilities. * `undefined` on a legacy connection, before connect, or when that * intersection is empty (auto-open skipped). Exposed so the consumer can * `close()` it. */ get autoOpenedSubscription() { return this._autoOpenedSubscription; } /** * Transport-level demux for `subscriptions/listen` notifications, before * any decoding/era-gating/handler dispatch. Consumes the leading * `notifications/subscriptions/acknowledged` referencing a live * subscription id (resolves the ack waiter) and an inbound * `notifications/cancelled` referencing a live string-typed subscription * id (server-side teardown on stdio). Change notifications carrying a * subscription id pass through to the existing registered handlers via * `super`. An unmatched ack/cancelled is NOT consumed: it reaches * `setNotificationHandler` / `fallbackNotificationHandler` instead of * being silently swallowed. */ _onnotification(raw, extra) { const evicted = Object.hasOwn(LIST_CHANGED_EVICTIONS, raw.method) ? LIST_CHANGED_EVICTIONS[raw.method] : void 0; if (raw.method === "notifications/resources/updated") { const uri = raw.params?.uri; if (typeof uri === "string") this._cache.evictKey("resources/read", uri); } else if (evicted !== void 0) for (const method of evicted) this._cache.evict(method); if (raw.method === "notifications/subscriptions/acknowledged") { const subscriptionId = raw.params?._meta?.[_modelcontextprotocol_core_internal.SUBSCRIPTION_ID_META_KEY]; const entry = typeof subscriptionId === "string" ? this._listenState.get(subscriptionId) : void 0; if (entry !== void 0) { const honored = this._wireCodec().validateNotification("notifications/subscriptions/acknowledged", raw); entry.settle({ ack: honored.ok ? honored.value.params.notifications : {} }); return; } } if (raw.method === "notifications/cancelled") { const cancelledId = raw.params?.requestId; const entry = typeof cancelledId === "string" ? this._listenState.get(cancelledId) : void 0; if (entry !== void 0) { entry.settle({ cause: "remote", error: /* @__PURE__ */ new Error("subscriptions/listen: server cancelled the subscription") }); return; } } super._onnotification(raw, extra); } /** * Transport-level demux for `subscriptions/listen` responses. A JSON-RPC * ERROR for the listen id is the server's pre-ack capacity/params * rejection; a JSON-RPC RESULT for the listen id is the spec's * `SubscriptionsListenResult` — the server's GRACEFUL-close signal (sent * on shutdown). A string-id response that matches a live `_listenState` * entry is consumed here (Protocol's `_responseHandlers` map is keyed by * NUMBER and never holds a listen id, so passing a string-id response * through would surface as "unknown message ID" via `onerror`). */ _onresponse(response) { const id = response.id; const entry = typeof id === "string" ? this._listenState.get(id) : void 0; if (entry !== void 0) { if (require_src.isJSONRPCErrorResponse(response)) entry.settle({ cause: "remote", error: require_src.ProtocolError.fromError(response.error.code, response.error.message, response.error.data) }); else entry.settle({ cause: "graceful", error: new require_src.SdkError(require_src.SdkErrorCode.ConnectionClosed, "subscriptions/listen: server closed the subscription gracefully before acknowledging") }); return; } super._onresponse(response); } /** * Settle every live per-listen state machine on a transport-initiated * close (the server dropping the connection on stdio/InMemory) before * Protocol's `_onclose` tears the transport down. The base * `_responseHandlers` settlement does not reach `_listenState` (listen * ids are never registered there), so without this override a remote * close would leave an in-flight `listen()` / open `McpSubscription` * hanging. */ _onclose() { if (this._listenState.size > 0) { const reason = new require_src.SdkError(require_src.SdkErrorCode.ConnectionClosed, "Connection closed"); for (const entry of this._listenState.values()) entry.settle({ cause: "remote", error: reason }); this._listenState.clear(); } super._onclose(); } /** * Calls a tool on the connected server and returns the result. Automatically validates structured output * if the tool has an `outputSchema`. * * Tool results have two error surfaces: `result.isError` for tool-level failures (the tool ran but reported * a problem), and thrown {@linkcode ProtocolError} for protocol-level failures or {@linkcode SdkError} for * SDK-level issues (timeouts, missing capabilities). * * @example Basic usage * ```ts source="./client.examples.ts#Client_callTool_basic" * const result = await client.callTool({ * name: 'calculate-bmi', * arguments: { weightKg: 70, heightM: 1.75 } * }); * * // Tool-level errors are returned in the result, not thrown * if (result.isError) { * console.error('Tool error:', result.content); * return; * } * * console.log(result.content); * ``` * * @example Structured output * ```ts source="./client.examples.ts#Client_callTool_structuredOutput" * const result = await client.callTool({ * name: 'calculate-bmi', * arguments: { weightKg: 70, heightM: 1.75 } * }); * * // Machine-readable output for the client application. SEP-2106: structuredContent is * // `unknown` (any JSON value). Check for presence with `!== undefined` and narrow before use. * if (result.structuredContent !== undefined) { * const sc: unknown = result.structuredContent; // e.g. { bmi: 22.86 } * if (typeof sc === 'object' && sc !== null && 'bmi' in sc) { * console.log(sc.bmi); * } * } * ``` */ async callTool(params, options) { const mirroringActive = this.getProtocolEra() === "modern" && detectProbeEnvironment() !== "browser"; const buildSendOptions = async () => { if (!mirroringActive) return options; let scan; try { scan = await this._resolveXMcpHeaderScan(params.name, options?.toolDefinition); } catch (error) { this._reportStoreError(error); } if (!scan?.valid || scan.declarations.length === 0) return options; const paramHeaders = require_src.buildMcpParamHeaders(scan.declarations, params.arguments); return Object.keys(paramHeaders).length === 0 ? options : { ...options, headers: { ...options?.headers, ...paramHeaders } }; }; let compiled = options?.toolDefinition === void 0 ? await this._cache.outputValidator(params.name, (tool) => this._compileOutputValidator(tool)).catch((error) => void this._reportStoreError(error)) : this._compileOutputValidator(options.toolDefinition); const assertCompiled = () => { if (compiled === void 0 || compiled.ok) return; const err = compiled.compileError; const message = (err instanceof Error ? err.message : String(err)).slice(0, 200); throw new require_src.ProtocolError(require_src.ProtocolErrorCode.InvalidParams, `Tool '${params.name}' has an invalid outputSchema: ${message}`); }; assertCompiled(); let result; try { result = await this.request({ method: "tools/call", params }, await buildSendOptions()); } catch (error) { const isHeaderMismatch = error instanceof require_src.ProtocolError && error.code === require_src.HEADER_MISMATCH_ERROR_CODE; if (!mirroringActive || !isHeaderMismatch || options?.toolDefinition !== void 0) throw error; const refreshOptions = { signal: options?.signal, timeout: options?.timeout, cacheMode: "refresh" }; await this._cache.evict("tools/list"); await this.listTools(void 0, refreshOptions).catch((error_) => this._reportStoreError(error_)); compiled = await this._cache.outputValidator(params.name, (tool) => this._compileOutputValidator(tool)).catch((error_) => void this._reportStoreError(error_)); assertCompiled(); result = await this.request({ method: "tools/call", params }, await buildSendOptions()); } const validator = compiled !== void 0 && compiled.ok ? compiled.validator : void 0; if (validator) { if (result.structuredContent === void 0 && !result.isError) throw new require_src.ProtocolError(require_src.ProtocolErrorCode.InvalidRequest, `Tool ${params.name} has an output schema but did not return structured content`); if (result.structuredContent !== void 0 && !result.isError) try { const validationResult = validator(result.structuredContent); if (!validationResult.valid) throw new require_src.ProtocolError(require_src.ProtocolErrorCode.InvalidParams, `Structured content does not match the tool's output schema: ${validationResult.errorMessage}`); } catch (error) { if (error instanceof require_src.ProtocolError) throw error; throw new require_src.ProtocolError(require_src.ProtocolErrorCode.InvalidParams, `Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`); } } return result; } /** * Lists available tools. * * Called without a `cursor` (the common case), this walks every page and * returns the complete aggregated list with no `nextCursor`; the * aggregate is also written to the {@linkcode ResponseCacheStore} (the * source for {@linkcode callTool | callTool()}'s output-schema validation * and SEP-2243 `Mcp-Param-*` header mirroring). Pass an explicit * `{ cursor }` to fetch a single page and walk pagination yourself — the * per-page path returns the server's raw page (with `nextCursor` for the * next call) and does not write the response cache. The auto-aggregate * path is capped by {@linkcode ClientOptions | ClientOptions.listMaxPages} (default 64); * the per-page path is not. * * Returns an empty list if the server does not advertise tools capability * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled). * * @example * ```ts source="./client.examples.ts#Client_listTools_pagination" * // No cursor → all pages aggregated for you. * const { tools } = await client.listTools(); * console.log( * 'Available tools:', * tools.map(t => t.name) * ); * ``` */ async listTools(params, options) { if (!this._serverCapabilities?.tools && !this._enforceStrictCapabilities) { console.debug("Client.listTools() called but server does not advertise tools capability - returning empty list"); return { tools: [] }; } if (params?.cursor !== void 0) { const page = await this.request({ method: "tools/list", params }, options); this._excludeInvalidXMcpHeaderTools(page); return page; } const hit = await this._serveFromCache("tools/list", void 0, options); if (hit !== void 0) return hit; return this._listAllPages("tools/list", params, options, (r) => r.tools, (acc) => this._excludeInvalidXMcpHeaderTools(acc)); } /** * SEP-2243 (protocol revision 2026-07-28): a Streamable HTTP client MUST * exclude tool definitions whose `x-mcp-header` declarations violate the * constraints, and SHOULD log a warning naming the tool and the reason. * Applied to the CACHED aggregated `tools/list` result (so the entry * mirroring reads never holds an unmirrorable tool) AND to every public * per-page {@linkcode listTools | listTools()} return (the spec's MUST * has no carve-out for paginated reads). The gate is era-only on * non-stdio transports — `detectProbeTransportKind` cannot distinguish a * real HTTP transport from in-memory/custom transports (it only * positively recognizes stdio), and over-excluding on a non-HTTP modern * connection is harmless: those transports never carry per-request * headers, so an excluded tool would have been uncallable on a Streamable * HTTP arm of the same server. Mutates `result.tools` in place. */ _excludeInvalidXMcpHeaderTools(result) { if (this.getProtocolEra() !== "modern" || !this.transport || detectProbeTransportKind(this.transport) === "stdio") return; const filtered = result.tools.filter((tool) => { const scan = require_src.scanXMcpHeaderDeclarations(tool.inputSchema); if (!scan.valid) { console.warn(`[mcp-sdk] excluding tool '${tool.name}' from tools/list: invalid x-mcp-header declaration — ${scan.reason}`); return false; } return true; }); if (filtered.length !== result.tools.length) result.tools = filtered; } /** * Set up a single list changed handler. * @internal */ _setupListChangedHandler(listType, notificationMethod, options, fetcher) { const parseResult = require_src.parseSchema(_modelcontextprotocol_core_internal.ListChangedOptionsBaseSchema, options); if (!parseResult.success) throw new Error(`Invalid ${listType} listChanged options: ${parseResult.error.message}`); if (typeof options.onChanged !== "function") throw new TypeError(`Invalid ${listType} listChanged options: onChanged must be a function`); const { autoRefresh, debounceMs } = parseResult.data; const { onChanged } = options; const refresh = async () => { if (!autoRefresh) { onChanged(null, null); return; } try { onChanged(null, await fetcher()); } catch (error) { onChanged(error instanceof Error ? error : new Error(String(error)), null); } }; const handler = () => { if (debounceMs) { const existingTimer = this._listChangedDebounceTimers.get(listType); if (existingTimer) clearTimeout(existingTimer); const timer = setTimeout(refresh, debounceMs); this._listChangedDebounceTimers.set(listType, timer); } else refresh(); }; this.setNotificationHandler(notificationMethod, handler); } /** * Notifies the server that the client's root list has changed. Requires the `roots.listChanged` capability. * * @deprecated Deprecated as of protocol version 2026-07-28 (SEP-2577). * Remains functional during the deprecation window (at least twelve months). * Migrate to passing paths via tool parameters, resource URIs, or configuration. */ async sendRootsListChanged() { return this.notification({ method: "notifications/roots/list_changed" }); } }; //#endregion //#region src/client/crossAppAccess.ts /** * Requests a JWT Authorization Grant (ID-JAG) from an enterprise IdP using RFC 8693 Token Exchange. * * This function performs step 2 of the Enterprise Managed Authorization flow: * exchanges an ID Token for a JWT Authorization Grant that can be used with the target MCP server. * * @param options - Configuration for the token exchange request * @returns The JWT Authorization Grant and related metadata * @throws {Error} If the token exchange fails or returns an error response * * @example * ```ts * const result = await requestJwtAuthorizationGrant({ * tokenEndpoint: 'https://idp.example.com/token', * audience: 'https://auth.chat.example/', * resource: 'https://mcp.chat.example/', * idToken: 'eyJhbGciOiJS...', * clientId: 'my-idp-client', * clientSecret: 'my-idp-secret', * scope: 'chat.read chat.history' * }); * * // Use result.jwtAuthGrant with the MCP server's authorization server * ``` */ async function requestJwtAuthorizationGrant(options) { const { tokenEndpoint, audience, resource, idToken, clientId, clientSecret, scope, fetchFn = fetch } = options; const tokenUrl = assertSecureTokenEndpoint(tokenEndpoint); const params = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", requested_token_type: "urn:ietf:params:oauth:token-type:id-jag", audience: String(audience), resource: String(resource), subject_token: idToken, subject_token_type: "urn:ietf:params:oauth:token-type:id_token", client_id: clientId }); if (clientSecret) params.set("client_secret", clientSecret); if (scope) params.set("scope", scope); const response = await require_src.fetchWithinOrigin(fetchFn)(tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() }); if (!response.ok) { const errorBody = await response.json().catch(() => ({})); const parseResult$1 = _modelcontextprotocol_core_internal.OAuthErrorResponseSchema.safeParse(errorBody); if (parseResult$1.success) { const { error, error_description } = parseResult$1.data; throw new Error(`Token exchange failed: ${error}${error_description ? ` - ${error_description}` : ""}`); } throw new Error(`Token exchange failed with status ${response.status}: ${JSON.stringify(errorBody)}`); } const parseResult = _modelcontextprotocol_core_internal.IdJagTokenExchangeResponseSchema.safeParse(await response.json()); if (!parseResult.success) throw new Error(`Invalid token exchange response: ${parseResult.error.message}`); return { jwtAuthGrant: parseResult.data.access_token, expiresIn: parseResult.data.expires_in, scope: parseResult.data.scope }; } /** * Discovers the IdP's token endpoint and requests a JWT Authorization Grant. * * This is a convenience wrapper around {@linkcode requestJwtAuthorizationGrant} that * first performs OAuth metadata discovery to find the token endpoint. * * @param options - Configuration including IdP URL for discovery * @returns The JWT Authorization Grant and related metadata * @throws {Error} If discovery fails or the token exchange fails * * @example * ```ts * const result = await discoverAndRequestJwtAuthGrant({ * idpUrl: 'https://idp.example.com', * audience: 'https://auth.chat.example/', * resource: 'https://mcp.chat.example/', * idToken: await getIdToken(), * clientId: 'my-idp-client', * clientSecret: 'my-idp-secret' * }); * ``` */ async function discoverAndRequestJwtAuthGrant(options) { const { idpUrl, fetchFn = fetch, ...restOptions } = options; const metadata = await discoverAuthorizationServerMetadata(String(idpUrl), { fetchFn }); if (!metadata?.token_endpoint) throw new Error(`Failed to discover token endpoint for IdP: ${idpUrl}`); return requestJwtAuthorizationGrant({ ...restOptions, tokenEndpoint: metadata.token_endpoint, fetchFn }); } /** * Exchanges a JWT Authorization Grant for an access token at the MCP server's authorization server. * * This function performs step 3 of the Enterprise Managed Authorization flow: * uses the JWT Authorization Grant to obtain an access token from the MCP server. * * @param options - Configuration for the JWT grant exchange * @returns OAuth tokens (access token, token type, etc.) * @throws {Error} If the exchange fails or returns an error response * * Defaults to `client_secret_basic` (HTTP Basic Authorization header), matching * `CrossAppAccessProvider`'s declared `token_endpoint_auth_method` and the * SEP-990 conformance test requirements. Use `authMethod: 'client_secret_post'` only * when the authorization server explicitly requires it. * * @example * ```ts * const tokens = await exchangeJwtAuthGrant({ * tokenEndpoint: 'https://auth.chat.example/token', * jwtAuthGrant: 'eyJhbGci...', * clientId: 'my-mcp-client', * clientSecret: 'my-mcp-secret' * }); * * // Use tokens.access_token to access the MCP server * ``` */ async function exchangeJwtAuthGrant(options) { const { tokenEndpoint, jwtAuthGrant, clientId, clientSecret, authMethod = "client_secret_basic", fetchFn = fetch } = options; const tokenUrl = assertSecureTokenEndpoint(tokenEndpoint); const params = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwtAuthGrant }); const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" }); applyClientAuthentication(authMethod, { client_id: clientId, client_secret: clientSecret }, headers, params); const response = await require_src.fetchWithinOrigin(fetchFn)(tokenUrl, { method: "POST", headers, body: params.toString() }); if (!response.ok) { const errorBody = await response.json().catch(() => ({})); const parseResult$1 = _modelcontextprotocol_core_internal.OAuthErrorResponseSchema.safeParse(errorBody); if (parseResult$1.success) { const { error, error_description } = parseResult$1.data; throw new Error(`JWT grant exchange failed: ${error}${error_description ? ` - ${error_description}` : ""}`); } throw new Error(`JWT grant exchange failed with status ${response.status}: ${JSON.stringify(errorBody)}`); } const responseBody = await response.json(); const parseResult = _modelcontextprotocol_core_internal.OAuthTokensSchema.safeParse(require_src.withoutIssuer(responseBody)); if (!parseResult.success) throw new Error(`Invalid token response: ${parseResult.error.message}`); return parseResult.data; } //#endregion //#region src/client/dpop.ts /** Asymmetric JWS algorithms usable for a DPoP proof (RFC 9449 §11.6 forbids symmetric algs and `none`). */ const DPOP_SUPPORTED_ALGS = [ "ES256", "ES384", "ES512", "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "EdDSA" ]; const DEFAULT_DPOP_ALG = "ES256"; const DPOP_TYP = "dpop+jwt"; async function importJose() { if (globalThis.crypto === void 0) throw new TypeError("crypto is not available, please ensure you have Web Crypto API support for older Node.js versions (see https://github.com/modelcontextprotocol/typescript-sdk#nodejs-web-crypto-globalthiscrypto-compatibility)"); return import("jose"); } /** Generate an asymmetric DPoP signing key pair. Non-extractable by default (RFC 9449 §11). */ async function generateDpopKeyPair(options = {}) { const alg = options.alg ?? DEFAULT_DPOP_ALG; const jose = await importJose(); const { publicKey, privateKey } = await jose.generateKeyPair(alg, { extractable: options.extractable ?? false }); const publicJwk = await jose.exportJWK(publicKey); return { privateKey, publicKey, publicJwk, thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"), alg }; } /** * Compute the `ath` claim for a DPoP proof presented alongside an access token: the * base64url-encoded SHA-256 digest of the ASCII access-token value (RFC 9449 §4.1). * * Uses Web Crypto (`crypto.subtle`) rather than a Node-only hashing API so this stays usable * in browser and edge runtimes. */ async function accessTokenHash(accessToken) { if (globalThis.crypto?.subtle === void 0) throw new TypeError("crypto.subtle is not available, please ensure you have Web Crypto API support for older Node.js versions (see https://github.com/modelcontextprotocol/typescript-sdk#nodejs-web-crypto-globalthiscrypto-compatibility)"); const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(accessToken)); return base64UrlEncode(new Uint8Array(digest)); } function base64UrlEncode(bytes) { let binary = ""; for (const byte of bytes) binary += String.fromCodePoint(byte); return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", ""); } /** Strip the query and fragment from a URL, per RFC 9449 §4.2 (`htu` MUST NOT contain either). */ function stripQueryAndFragment(url) { const u = new URL(url.toString()); return `${u.origin}${u.pathname}`; } /** A fresh, cryptographically random `jti` (RFC 9449 §4.2 — MUST be unique per proof). */ function randomJti() { const bytes = new Uint8Array(16); globalThis.crypto.getRandomValues(bytes); return base64UrlEncode(bytes); } /** * A DPoP signing identity plus the small amount of state RFC 9449 requires across requests: * the key pair and, per origin, the most recently server-supplied nonce (RFC 9449 §8/§9). * * One `DpopSession` is meant to live for the lifetime of a single OAuth client registration — * the token endpoint and the resource server are different origins and get independent nonce * slots, so a nonce challenge from one never leaks into proofs sent to the other. */ var DpopSession = class DpopSession { nonces = /* @__PURE__ */ new Map(); constructor(keyPair) { this.keyPair = keyPair; } /** Create a session with a fresh key pair, or reuse a caller-supplied one. */ static async create(options = {}) { return new DpopSession(options.keyPair ?? await generateDpopKeyPair({ alg: options.alg })); } /** RFC 7638 JWK SHA-256 thumbprint of the signing key — matches the token's `cnf.jkt` once bound. */ get thumbprint() { return this.keyPair.thumbprint; } /** The JWS algorithm this session signs proofs with. */ get alg() { return this.keyPair.alg; } /** The public JWK embedded in every proof's `jwk` header parameter. */ get publicJwk() { return this.keyPair.publicJwk; } /** The remembered nonce for `url`'s origin, if the server has issued one (RFC 9449 §8/§9). */ nonceFor(url) { return this.nonces.get(new URL(url.toString()).origin); } /** Record a server-supplied nonce for `url`'s origin (newest-wins, RFC 9449 §8.2). */ rememberNonce(url, nonce) { this.nonces.set(new URL(url.toString()).origin, nonce); } /** * Capture a `DPoP-Nonce` response header, if present, for `url`'s origin. RFC 9449 §8.2 says a * fresh nonce may ride on *any* response (success or failure), so call this unconditionally * after every request, not only on a `use_dpop_nonce` challenge. */ observeNonce(response, url) { const nonce = response.headers.get("dpop-nonce"); if (nonce) this.rememberNonce(url, nonce); } /** * Build a fresh DPoP proof JWT. Always mints a new `jti` — proofs are never cached or reused, * since RFC 9449 §4.3 step 9 requires each to be presented at most once. */ async buildProof(request) { const jose = await importJose(); const htu = stripQueryAndFragment(request.htu); const nonce = request.nonce ?? this.nonceFor(request.htu); const payload = { jti: randomJti(), htm: request.htm.toUpperCase(), htu, iat: Math.floor(Date.now() / 1e3) }; if (request.accessToken !== void 0) payload.ath = await accessTokenHash(request.accessToken); if (nonce !== void 0) payload.nonce = nonce; return new jose.SignJWT(payload).setProtectedHeader({ alg: this.keyPair.alg, typ: DPOP_TYP, jwk: this.keyPair.publicJwk }).sign(this.keyPair.privateKey); } }; /** * Whether `wwwAuthenticate` advertises a `DPoP` auth-scheme challenge (matched at the start of the * header or after a comma, per RFC 9110 §11.6.1 — a server may list several schemes together, e.g. * `Bearer …, DPoP …`). */ function hasDpopChallenge(wwwAuthenticate) { return /(?:^|,)\s*dpop(?:\s|$|,)/i.test(wwwAuthenticate); } /** * Whether `response` is a resource-server `use_dpop_nonce` challenge (RFC 9449 §9): a `401` whose * `WWW-Authenticate` header carries a `DPoP` challenge with `error="use_dpop_nonce"`. * * A conformant retry re-signs the proof with the nonce {@linkcode DpopSession.observeNonce} just * captured — never resend the original proof: RFC 9449 §4.2 requires a unique `jti` per proof, and * replaying one across the challenge/retry boundary is itself a violation. */ function isDpopNonceChallenge(response) { if (response.status !== 401) return false; const wwwAuthenticate = response.headers.get("www-authenticate") ?? ""; return hasDpopChallenge(wwwAuthenticate) && /use_dpop_nonce/i.test(wwwAuthenticate); } //#endregion //#region src/client/middleware.ts /** * Creates a fetch wrapper that handles OAuth authentication automatically. * * This wrapper will: * - Add `Authorization` headers with access tokens * - Handle 401 responses by attempting re-authentication * - Retry the original request after successful auth * - Handle OAuth errors appropriately ({@linkcode index.OAuthErrorCode.InvalidClient | OAuthErrorCode.InvalidClient}, etc.) * - When {@linkcode OAuthClientProvider.dpop | provider.dpop()} is implemented, present DPoP-bound * tokens with the `DPoP` scheme plus a per-request proof (RFC 9449 / SEP-1932) by composing * {@linkcode withDpopFromProvider} underneath — so a `use_dpop_nonce` challenge is retried * inline on every attempt, independently of the single re-authentication retry here * * The `baseUrl` parameter is optional and defaults to using the domain from the request URL. * However, you should explicitly provide `baseUrl` when: * - Making requests to multiple subdomains (e.g., api.example.com, cdn.example.com) * - Using API paths that differ from OAuth discovery paths (e.g., requesting /api/v1/data but OAuth is at /) * - The OAuth server is on a different domain than your API requests * - You want to ensure consistent OAuth behavior regardless of request URLs * * For MCP transports, set `baseUrl` to the same URL you pass to the transport constructor. * * Note: This wrapper is designed for general-purpose fetch operations. * MCP transports (SSE and StreamableHTTP) already have built-in OAuth handling * and should not need this wrapper. * * @param provider - OAuth client provider for authentication * @param baseUrl - Base URL for OAuth server discovery (defaults to request URL domain) * @returns A fetch middleware function */ const withOAuth = (provider, baseUrl) => (baseNext) => { const next = provider.dpop ? withDpopFromProvider(provider)(baseNext) : baseNext; return async (input, init) => { const makeRequest = async () => { const headers = new Headers(init?.headers); const tokens = await provider.tokens(); if (tokens) headers.set("Authorization", `Bearer ${tokens.access_token}`); return await next(input, { ...init, headers }); }; let response = await makeRequest(); if (response.status === 401) try { const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); const result = await auth(provider, { serverUrl: baseUrl || (typeof input === "string" ? new URL(input).origin : input.origin), resourceMetadataUrl, scope, fetchFn: baseNext }); if (result === "REDIRECT") throw new UnauthorizedError("Authentication requires user authorization - redirect initiated"); if (result !== "AUTHORIZED") throw new UnauthorizedError(`Authentication failed with result: ${result}`); response = await makeRequest(); } catch (error) { if (error instanceof UnauthorizedError) throw error; throw new UnauthorizedError(`Failed to re-authenticate: ${error instanceof Error ? error.message : String(error)}`); } if (response.status === 401) throw new UnauthorizedError(`Authentication failed for ${typeof input === "string" ? input : input.toString()}`); return response; }; }; /** * Creates a fetch wrapper that presents an access token using the `DPoP` Authorization scheme * (RFC 9449 / SEP-1932) instead of `Bearer`: every request carries `Authorization: DPoP ` * plus a fresh `DPoP` proof bound to that request's method and URL, a resource-server * `use_dpop_nonce` challenge (RFC 9449 §9) is retried once, inline, with the server-supplied nonce, * and a `DPoP-Nonce` delivered on any response is remembered for the next proof (RFC 9449 §8.2). * * Because it wraps `fetch` itself, the proof is always bound to the request actually sent and every * response is observed — which is why the MCP transports apply this wrapper internally (via * {@linkcode withDpopFromProvider}) when their `authProvider` implements * {@linkcode OAuthClientProvider.dpop | dpop()}, rather than threading request context through * {@linkcode AuthProvider}. * * Use this directly when you already manage the access token yourself (a non-OAuth token source, * or credentials obtained out-of-band) and only need DPoP's request-signing behavior — e.g. * `fetch: withDpop(session, getToken)(fetch)` alongside a minimal `authProvider: { token }`. * * @param session - The DPoP signing session (key pair + nonce state), or a function resolving to * it. Reuse the same session across requests to the same server so its nonce state persists. * When the function resolves to `undefined` the request passes through unchanged. * @param getToken - Returns the current access token, or `undefined` if none is available (the * request passes through unchanged — any `Authorization` header already on it is left as is). * @returns A fetch middleware function */ const withDpop = (session, getToken) => (next) => { const resolveSession = typeof session === "function" ? session : () => session; return async (input, init) => { const method = (init?.method ?? "GET").toUpperCase(); const url = new URL(input.toString()); const activeSession = await resolveSession(); if (!activeSession) return next(input, init); const makeRequest = async () => { const accessToken = await getToken(); if (!accessToken) return next(input, init); const headers = new Headers(init?.headers); const proof = await activeSession.buildProof({ htm: method, htu: url, accessToken }); headers.set("Authorization", `DPoP ${accessToken}`); headers.set("DPoP", proof); return next(input, { ...init, headers }); }; let response = await makeRequest(); if (isDpopNonceChallenge(response) && response.headers.has("dpop-nonce")) { activeSession.observeNonce(response, url); await response.text?.().catch(() => {}); response = await makeRequest(); } activeSession.observeNonce(response, url); return response; }; }; /** * {@linkcode withDpop} driven by an {@linkcode OAuthClientProvider}: the session comes from * {@linkcode OAuthClientProvider.dpop | provider.dpop()} and the token from * {@linkcode OAuthClientProvider.tokens | provider.tokens()} — presented with the DPoP scheme only * when the AS actually issued `token_type: "DPoP"` (RFC 9449 §7.1); a Bearer token passes through * untouched. * * This is what the MCP transports and {@linkcode withOAuth} compose internally when the provider * implements `dpop()`. Use it directly only when building your own fetch pipeline around an * `OAuthClientProvider` (e.g. a custom re-authorization middleware) — place it *innermost*, below * whatever sets `Authorization: Bearer` and handles 401 re-authentication. */ const withDpopFromProvider = (provider) => withDpop(async () => { try { return await provider.dpop?.(); } catch (error) { throw markAuthSeamEscape(error); } }, async () => { let tokens; try { tokens = await provider.tokens(); } catch (error) { throw markAuthSeamEscape(error); } return tokens?.token_type?.toLowerCase() === "dpop" ? tokens.access_token : void 0; }); /** * Creates a fetch middleware that logs HTTP requests and responses. * * When called without arguments `withLogging()`, it uses the default logger that: * - Logs successful requests (2xx) to `console.log` * - Logs error responses (4xx/5xx) and network errors to `console.error` * - Logs all requests regardless of status (`statusLevel: 0`) * - Does not include request or response headers in logs * - Measures and displays request duration in milliseconds * * Important: the default logger uses both `console.log` and `console.error` so it should not be used with * `stdio` transports and applications. * * @param options - Logging configuration options * @returns A fetch middleware function */ const withLogging = (options = {}) => { const { logger, includeRequestHeaders = false, includeResponseHeaders = false, statusLevel = 0 } = options; const defaultLogger = (input) => { const { method, url, status, statusText, duration, requestHeaders, responseHeaders, error } = input; let message = error ? `HTTP ${method} ${url} failed: ${error.message} (${duration}ms)` : `HTTP ${method} ${url} ${status} ${statusText} (${duration}ms)`; if (includeRequestHeaders && requestHeaders) { const reqHeaders = [...requestHeaders.entries()].map(([key, value]) => `${key}: ${value}`).join(", "); message += `\n Request Headers: {${reqHeaders}}`; } if (includeResponseHeaders && responseHeaders) { const resHeaders = [...responseHeaders.entries()].map(([key, value]) => `${key}: ${value}`).join(", "); message += `\n Response Headers: {${resHeaders}}`; } if (error || status >= 400) console.error(message); else console.log(message); }; const logFn = logger || defaultLogger; return (next) => async (input, init) => { const startTime = performance.now(); const method = init?.method || "GET"; const url = typeof input === "string" ? input : input.toString(); const requestHeaders = includeRequestHeaders ? new Headers(init?.headers) : void 0; try { const response = await next(input, init); const duration = performance.now() - startTime; if (response.status >= statusLevel) logFn({ method, url, status: response.status, statusText: response.statusText, duration, requestHeaders, responseHeaders: includeResponseHeaders ? response.headers : void 0 }); return response; } catch (error) { logFn({ method, url, status: 0, statusText: "Network Error", duration: performance.now() - startTime, requestHeaders, error }); throw error; } }; }; /** * Composes multiple fetch middleware functions into a single middleware pipeline. * Middleware are applied in the order they appear, creating a chain of handlers. * * @example * ```ts source="./middleware.examples.ts#applyMiddlewares_basicUsage" * // Create a middleware pipeline that handles both OAuth and logging * const enhancedFetch = applyMiddlewares(withOAuth(oauthProvider, 'https://api.example.com'), withLogging({ statusLevel: 400 }))(fetch); * * // Use the enhanced fetch - it will handle auth and log errors * const response = await enhancedFetch('https://api.example.com/data'); * ``` * * @param middleware - Array of fetch middleware to compose into a pipeline * @returns A single composed middleware function */ const applyMiddlewares = (...middleware) => { return (next) => { let handler = next; for (const mw of middleware) handler = mw(handler); return handler; }; }; /** * Helper function to create custom fetch middleware with cleaner syntax. * Provides the next handler and request details as separate parameters for easier access. * * @example * ```ts source="./middleware.examples.ts#createMiddleware_examples" * // Create custom authentication middleware * const customAuthMiddleware = createMiddleware(async (next, input, init) => { * const headers = new Headers(init?.headers); * headers.set('X-Custom-Auth', 'my-token'); * * const response = await next(input, { ...init, headers }); * * if (response.status === 401) { * console.log('Authentication failed'); * } * * return response; * }); * * // Create conditional middleware * const conditionalMiddleware = createMiddleware(async (next, input, init) => { * const url = typeof input === 'string' ? input : input.toString(); * * // Only add headers for API routes * if (url.includes('/api/')) { * const headers = new Headers(init?.headers); * headers.set('X-API-Version', 'v2'); * return next(input, { ...init, headers }); * } * * // Pass through for non-API routes * return next(input, init); * }); * * // Create caching middleware * const cacheMiddleware = createMiddleware(async (next, input, init) => { * const cacheKey = typeof input === 'string' ? input : input.toString(); * * // Check cache first * const cached = await getFromCache(cacheKey); * if (cached) { * return new Response(cached, { status: 200 }); * } * * // Make request and cache result * const response = await next(input, init); * if (response.ok) { * await saveToCache(cacheKey, await response.clone().text()); * } * * return response; * }); * ``` * * @param handler - Function that receives the next handler and request parameters * @returns A fetch middleware function */ const createMiddleware = (handler) => { return (next) => (input, init) => handler(next, input, init); }; //#endregion //#region src/client/sse.ts var SseError = class extends Error { static { Object.defineProperty(this, "mcpBrand", { value: "mcp.SseError" }); } static [Symbol.hasInstance](value) { return require_src.brandedHasInstance(this, value); } /** * Brand-based type guard: equivalent to `value instanceof this`, as an * explicit static predicate (the axios/AWS-SDK `isInstance` style). Reads * the caller's own brand via `this`, so every branded subclass gets a * correctly-scoped guard by inheritance. Must be invoked on the class — * in callback position write `v => SdkError.isInstance(v)`, not * `.filter(SdkError.isInstance)` (detached calls throw rather than * silently matching nothing). */ static isInstance(value) { if (typeof this !== "function") throw new TypeError("isInstance must be called on the class (e.g. `SdkError.isInstance(value)`); for callbacks use `v => SdkError.isInstance(v)`"); return require_src.brandedHasInstance(this, value); } constructor(code, message, event) { super(`SSE error: ${message}`); this.code = code; this.event = event; require_src.stampErrorBrands(this, new.target); } }; /** * Client transport for SSE: this will connect to a server using Server-Sent Events for receiving * messages and make separate `POST` requests for sending messages. * @deprecated SSEClientTransport is deprecated. Prefer to use {@linkcode index.StreamableHTTPClientTransport | StreamableHTTPClientTransport} where possible instead. Note that because some servers are still using SSE, clients may need to support both transports during the migration period. */ var SSEClientTransport = class { _eventSource; _endpoint; _abortController; _url; _resourceMetadataUrl; _scope; _eventSourceInit; _requestInit; _authProvider; _oauthProvider; _skipIssuerMetadataValidation; _fetch; _fetchWithInit; _redirectPolicy; _dpop; _protocolVersion; onclose; onerror; onmessage; constructor(url, opts) { this._url = url; this._resourceMetadataUrl = void 0; this._scope = void 0; this._eventSourceInit = opts?.eventSourceInit; this._requestInit = opts?.requestInit; this._skipIssuerMetadataValidation = opts?.skipIssuerMetadataValidation; this._fetch = opts?.fetch; if (isOAuthClientProvider(opts?.authProvider)) { this._oauthProvider = opts.authProvider; this._authProvider = adaptOAuthProvider(opts.authProvider, { skipIssuerMetadataValidation: opts.skipIssuerMetadataValidation }); if (opts.authProvider.dpop) { this._dpop = withDpopFromProvider(opts.authProvider); this._fetch = this._dpop(opts.fetch ?? fetch); } } else this._authProvider = opts?.authProvider; this._redirectPolicy = opts?.redirectPolicy; this._fetchWithInit = require_src.createFetchWithInit(opts?.fetch, opts?.requestInit); if (this._redirectPolicy === "follow") this._fetchWithInit = require_src.fetchLeavingRedirects(this._fetchWithInit); } _last401Response; _connectAuthRetried = false; /** `baseFetch` with redirects handled as `redirectPolicy` says. */ _redirects(baseFetch) { return this._redirectPolicy === "follow" ? baseFetch : require_src.fetchWithinOrigin(baseFetch); } /** Error text for a redirect `response` that was not followed, or `undefined` for any other response. */ _unfollowedRedirect(url, response) { const text = this._redirectPolicy === "follow" ? void 0 : require_src.unfollowedRedirect(url, response); return text && `${text} (redirectPolicy: 'same-origin')`; } async _commonHeaders() { const headers = new Headers(this._requestInit?.headers || void 0); let token; try { token = await this._authProvider?.token(); } catch (error) { throw markAuthSeamEscape(error); } if (token) headers.set("Authorization", `Bearer ${token}`); if (this._protocolVersion) headers.set("mcp-protocol-version", this._protocolVersion); return headers; } _startOrAuth() { const eventSourceFetch = this._eventSourceInit?.fetch; const fetchImpl = this._redirects(eventSourceFetch ? this._dpop?.(eventSourceFetch) ?? eventSourceFetch : this._fetch ?? fetch); let redirect; return new Promise((resolve, reject) => { this._eventSource = new eventsource.EventSource(this._url.href, { ...this._eventSourceInit, fetch: async (url, init) => { const headers = await this._commonHeaders(); headers.set("Accept", "text/event-stream"); const response = await fetchImpl(url, { ...init, headers }); redirect = this._unfollowedRedirect(url, response); if (response.status === 401) { this._last401Response = response; if (response.headers.has("www-authenticate")) { const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); this._resourceMetadataUrl = resourceMetadataUrl; this._scope = scope; } } return response; } }); this._abortController = new AbortController(); this._eventSource.onerror = (event) => { if (event.code === 401 && this._authProvider) { if (this._authProvider.onUnauthorized && this._last401Response && !this._connectAuthRetried) { const response = this._last401Response; this._last401Response = void 0; this._connectAuthRetried = true; this._eventSource?.close(); this._authProvider.onUnauthorized({ response, serverUrl: this._url, fetchFn: this._fetchWithInit }).then(() => this._startOrAuth().then(resolve, reject), (error$2) => { this._connectAuthRetried = false; markAuthSeamEscape(error$2); this.onerror?.(error$2); reject(error$2); }); return; } const retried = this._connectAuthRetried; this._connectAuthRetried = false; const error$1 = markAuthSeamEscape(retried ? new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { status: 401, statusText: this._last401Response?.statusText ?? "" }) : new UnauthorizedError()); this._last401Response = void 0; reject(error$1); this.onerror?.(error$1); return; } this._connectAuthRetried = false; const error = new SseError(event.code, redirect ?? event.message, event); reject(error); this.onerror?.(error); }; this._eventSource.onopen = () => { this._connectAuthRetried = false; }; this._eventSource.addEventListener("endpoint", (event) => { const messageEvent = event; try { this._endpoint = new URL(messageEvent.data, this._url); if (this._endpoint.origin !== this._url.origin) throw new Error(`Endpoint origin does not match connection origin: ${this._endpoint.origin}`); } catch (error) { reject(error); this.onerror?.(error); this.close(); return; } resolve(); }); this._eventSource.onmessage = (event) => { const messageEvent = event; let message; try { message = _modelcontextprotocol_core_internal.JSONRPCMessageSchema.parse(JSON.parse(messageEvent.data)); } catch (error) { this.onerror?.(error); return; } this.onmessage?.(message); }; }); } async start() { if (this._eventSource) throw new Error("SSEClientTransport already started! If using Client class, note that connect() calls start() automatically."); return await this._startOrAuth(); } async finishAuth(codeOrParams, iss) { if (!this._oauthProvider) throw new UnauthorizedError("finishAuth requires an OAuthClientProvider"); const { authorizationCode, iss: issParam } = await resolveAuthorizationCallbackParams(codeOrParams, iss, this._oauthProvider, this._url, { fetchFn: this._fetchWithInit, resourceMetadataUrl: this._resourceMetadataUrl }); if (await auth(this._oauthProvider, { serverUrl: this._url, authorizationCode, iss: issParam, resourceMetadataUrl: this._resourceMetadataUrl, scope: this._scope, fetchFn: this._fetchWithInit, skipIssuerMetadataValidation: this._skipIssuerMetadataValidation }) !== "AUTHORIZED") throw new UnauthorizedError("Failed to authorize"); } async close() { this._abortController?.abort(); this._eventSource?.close(); this.onclose?.(); } async send(message) { return this._send(message, false); } async _send(message, isAuthRetry) { if (!this._endpoint) throw new require_src.SdkError(require_src.SdkErrorCode.NotConnected, "Not connected"); try { const headers = await this._commonHeaders(); headers.set("content-type", "application/json"); const init = { ...this._requestInit, method: "POST", headers, body: JSON.stringify(message), signal: this._abortController?.signal }; const response = await this._redirects(this._fetch ?? fetch)(this._endpoint, init); if (!response.ok) { if (response.status === 401 && this._authProvider) { if (response.headers.has("www-authenticate")) { const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); this._resourceMetadataUrl = resourceMetadataUrl; this._scope = scope; } if (this._authProvider.onUnauthorized && !isAuthRetry) { try { await this._authProvider.onUnauthorized({ response, serverUrl: this._url, fetchFn: this._fetchWithInit }); } catch (error) { throw markAuthSeamEscape(error); } await response.text?.().catch(() => {}); return this._send(message, true); } await response.text?.().catch(() => {}); if (isAuthRetry) throw markAuthSeamEscape(new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { status: 401, statusText: response.statusText })); throw markAuthSeamEscape(new UnauthorizedError()); } const text = await response.text?.().catch(() => null); throw new Error(`Error POSTing to endpoint (HTTP ${response.status}): ${this._unfollowedRedirect(this._endpoint, response) ?? text}`); } await response.text?.().catch(() => {}); } catch (error) { this.onerror?.(error); throw error; } } setProtocolVersion(version) { this._protocolVersion = version; } }; //#endregion //#region src/client/streamableHttp.ts /** Default cap on step-up re-authorization retries within a single send/stream-open. */ const DEFAULT_MAX_STEP_UP_RETRIES = 1; const DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS = { initialReconnectionDelay: 1e3, maxReconnectionDelay: 3e4, reconnectionDelayGrowFactor: 1.5, maxRetries: 2 }; /** * Standard/auth header names the transport owns. The per-request * `TransportSendOptions.headers` carrier MUST NOT be able to override these — * they are derived from connection state (`authorization`, `mcp-session-id`) * or from the message body itself (`mcp-protocol-version`, `mcp-method`, * `mcp-name`), and a per-request override would let a caller produce a * header/body disagreement the server's SEP-2243 cross-checks reject. */ const RESERVED_REQUEST_HEADER_NAMES = new Set([ "authorization", "dpop", "content-type", "mcp-protocol-version", "mcp-method", "mcp-name", "mcp-session-id" ]); /** * `AbortSignal.any` with a manual fallback. `AbortSignal.any` landed in * Node 20.3; this package's `engines` floor is `>=20`, so 20.0–20.2 must be * served by the fallback combinator (a controller that aborts on the first * of `a` or `b`). The native path is preferred because it propagates the * originating signal's `reason` and participates in GC the way the spec * defines. */ function anySignal(a, b) { if (typeof AbortSignal.any === "function") return AbortSignal.any([a, b]); const controller = new AbortController(); if (a.aborted) return controller.abort(a.reason), controller.signal; if (b.aborted) return controller.abort(b.reason), controller.signal; const cleanup = () => { a.removeEventListener("abort", onA); b.removeEventListener("abort", onB); }; function onA() { cleanup(); controller.abort(a.reason); } function onB() { cleanup(); controller.abort(b.reason); } a.addEventListener("abort", onA, { once: true }); b.addEventListener("abort", onB, { once: true }); return controller.signal; } /** * Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification. * It will connect to a server using HTTP `POST` for sending messages and HTTP `GET` with Server-Sent Events * for receiving messages. */ var StreamableHTTPClientTransport = class { _abortController; _url; _resourceMetadataUrl; _scope; _requestInit; _authProvider; _oauthProvider; _skipIssuerMetadataValidation; _fetch; _fetchWithInit; _redirectPolicy; _sessionId; _reconnectionOptions; _protocolVersion; _onInsufficientScope; _maxStepUpRetries; _serverRetryMs; _reconnectionScheduler; _cancelReconnection; onclose; onerror; onmessage; /** * Streamable HTTP opens one POST (and SSE response stream) per outbound * request and honors `TransportSendOptions.requestSignal`. On a 2026-era * connection the protocol layer aborts that per-request stream as the * spec cancellation signal instead of POSTing `notifications/cancelled`. */ hasPerRequestStream = true; constructor(url, opts) { this._url = url; this._resourceMetadataUrl = void 0; this._scope = void 0; this._requestInit = opts?.requestInit; this._skipIssuerMetadataValidation = opts?.skipIssuerMetadataValidation; this._fetch = opts?.fetch; if (isOAuthClientProvider(opts?.authProvider)) { this._oauthProvider = opts.authProvider; this._authProvider = adaptOAuthProvider(opts.authProvider, { skipIssuerMetadataValidation: opts.skipIssuerMetadataValidation }); if (opts.authProvider.dpop) this._fetch = withDpopFromProvider(opts.authProvider)(opts.fetch ?? fetch); } else this._authProvider = opts?.authProvider; this._redirectPolicy = opts?.redirectPolicy; this._fetchWithInit = require_src.createFetchWithInit(opts?.fetch, opts?.requestInit); if (this._redirectPolicy === "follow") this._fetchWithInit = require_src.fetchLeavingRedirects(this._fetchWithInit); this._sessionId = opts?.sessionId; this._protocolVersion = opts?.protocolVersion; this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS; this._reconnectionScheduler = opts?.reconnectionScheduler; this._onInsufficientScope = opts?.onInsufficientScope ?? "reauthorize"; this._maxStepUpRetries = Math.max(0, opts?.maxStepUpRetries ?? DEFAULT_MAX_STEP_UP_RETRIES); } /** * SEP-2350 step-up: compute the union scope, decide whether refresh must be * bypassed, and run {@linkcode auth}. Returns the auth result so the caller * can decide whether to retry. Shared by the POST `_send` path and the GET * `_startOrAuthSse` path so both apply the same `'throw'` short-circuit, * the same superset-gated refresh bypass, and the same retry cap. */ async _stepUpAuthorize(challenge, stepUpRetries) { try { return await this._stepUpAuthorizeInner(challenge, stepUpRetries); } catch (error) { throw markAuthSeamEscape(error); } } async _stepUpAuthorizeInner(challenge, stepUpRetries) { if (this._onInsufficientScope === "throw") throw new InsufficientScopeError({ requiredScope: challenge.scope, resourceMetadataUrl: challenge.resourceMetadataUrl, errorDescription: challenge.errorDescription }); if (!this._oauthProvider) throw new InsufficientScopeError({ requiredScope: challenge.scope, resourceMetadataUrl: challenge.resourceMetadataUrl, errorDescription: challenge.errorDescription }); if (stepUpRetries >= this._maxStepUpRetries) throw new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpForbidden, `Server returned 403 insufficient_scope after step-up re-authorization (retry limit ${this._maxStepUpRetries} reached)`, { status: 403, statusText: challenge.statusText ?? "Forbidden", text: challenge.text }); if (challenge.resourceMetadataUrl) this._resourceMetadataUrl = challenge.resourceMetadataUrl; const tokens = await this._oauthProvider.tokens(); const unionScope = computeScopeUnion(this._scope, tokens?.scope, challenge.scope); this._scope = unionScope; const forceReauthorization = isStrictScopeSuperset(unionScope, tokens?.scope); return auth(this._oauthProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl, scope: unionScope, forceReauthorization, fetchFn: this._fetchWithInit, skipIssuerMetadataValidation: this._skipIssuerMetadataValidation }); } /** `baseFetch` with redirects handled as `redirectPolicy` says. */ _redirects(baseFetch) { return this._redirectPolicy === "follow" ? baseFetch : require_src.fetchWithinOrigin(baseFetch); } /** Error text for a redirect `response` that was not followed, or `undefined` for any other response. */ _unfollowedRedirect(url, response) { const text = this._redirectPolicy === "follow" ? void 0 : require_src.unfollowedRedirect(url, response); return text && `${text} (redirectPolicy: 'same-origin')`; } async _commonHeaders() { const headers = new Headers(this._requestInit?.headers || void 0); let token; try { token = await this._authProvider?.token(); } catch (error) { throw markAuthSeamEscape(error); } if (token) headers.set("Authorization", `Bearer ${token}`); if (this._sessionId) headers.set("mcp-session-id", this._sessionId); if (this._protocolVersion) headers.set("mcp-protocol-version", this._protocolVersion); return headers; } /** * Body-derived per-request headers: when an outgoing request carries a * protocol-version claim in its `_meta` envelope (the version negotiation * probe is the first such sender), `MCP-Protocol-Version` and `Mcp-Method` * derive from the message itself. The connection-level version slot is * neither consulted nor mutated; messages without an envelope claim are * untouched, so no 2026 header can appear on a legacy exchange. */ _applyBodyDerivedHeaders(headers, message) { if (Array.isArray(message) || !require_src.isJSONRPCRequest(message)) return; const envelopeVersion = (message.params?._meta)?.[_modelcontextprotocol_core_internal.PROTOCOL_VERSION_META_KEY]; if (typeof envelopeVersion !== "string") return; headers.set("mcp-protocol-version", envelopeVersion); headers.set("mcp-method", message.method); const source = require_src.mcpNameSource(message.method, message.params); if (source?.value !== void 0) headers.set("mcp-name", require_src.encodeMcpParamValue(source.value)); } /** * `true` when the outbound message is a single request carrying a * modern-era protocol-version envelope claim — the same predicate that * gates body-derived `mcp-method`/`mcp-name` emission. Used to confine the * 400-body-as-ProtocolError delivery to modern-era exchanges only. */ _isModernEnvelopedRequest(message) { if (Array.isArray(message) || !require_src.isJSONRPCRequest(message)) return false; const v = (message.params?._meta)?.[_modelcontextprotocol_core_internal.PROTOCOL_VERSION_META_KEY]; return typeof v === "string" && require_src.isModernProtocolVersion(v); } async _startOrAuthSse(options, isAuthRetry = false, stepUpRetries = 0) { const { resumptionToken, requestSignal } = options; const isIntentionalAbort = () => this._abortController?.signal.aborted === true || requestSignal?.aborted === true; try { const headers = await this._commonHeaders(); const types = [...headers.get("accept")?.split(",").map((s) => s.trim().toLowerCase()) ?? [], "text/event-stream"]; headers.set("accept", [...new Set(types)].join(", ")); if (resumptionToken) headers.set("last-event-id", resumptionToken); const transportSignal = this._abortController?.signal; const signal = requestSignal !== void 0 && transportSignal !== void 0 ? anySignal(transportSignal, requestSignal) : requestSignal ?? transportSignal; const response = await this._redirects(this._fetch ?? fetch)(this._url, { ...this._requestInit, method: "GET", headers, signal }); if (!response.ok) { if (response.status === 401 && this._authProvider) { if (response.headers.has("www-authenticate")) { const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); this._resourceMetadataUrl = resourceMetadataUrl; this._scope = computeScopeUnion(this._scope, scope); } if (this._authProvider.onUnauthorized && !isAuthRetry) { try { await this._authProvider.onUnauthorized({ response, serverUrl: this._url, fetchFn: this._fetchWithInit }); } catch (error) { throw markAuthSeamEscape(error); } await response.text?.().catch(() => {}); return this._startOrAuthSse(options, true, stepUpRetries); } await response.text?.().catch(() => {}); if (isAuthRetry) throw markAuthSeamEscape(new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { status: 401, statusText: response.statusText })); throw markAuthSeamEscape(new UnauthorizedError()); } if (response.status === 403) { const { resourceMetadataUrl, scope, error, errorDescription } = extractWWWAuthenticateParams(response); if (error === "insufficient_scope") { const text = await response.text?.().catch(() => null); if (await this._stepUpAuthorize({ scope, resourceMetadataUrl, errorDescription, statusText: response.statusText, text }, stepUpRetries) !== "AUTHORIZED") throw markAuthSeamEscape(new UnauthorizedError()); return this._startOrAuthSse(options, isAuthRetry, stepUpRetries + 1); } } await response.text?.().catch(() => {}); if (response.status === 405) { options.onRequestStreamEnd?.(); return; } const reason = this._unfollowedRedirect(this._url, response) ?? response.statusText; throw new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpFailedToOpenStream, `Failed to open SSE stream: ${reason}`, { status: response.status, statusText: response.statusText }); } this._handleSseStream(response.body, options, true); } catch (error) { if (!isIntentionalAbort()) this.onerror?.(error); throw error; } } /** * Calculates the next reconnection delay using a backoff algorithm * * @param attempt Current reconnection attempt count for the specific stream * @returns Time to wait in milliseconds before next reconnection attempt */ _getNextReconnectionDelay(attempt) { if (this._serverRetryMs !== void 0) return this._serverRetryMs; const initialDelay = this._reconnectionOptions.initialReconnectionDelay; const growFactor = this._reconnectionOptions.reconnectionDelayGrowFactor; const maxDelay = this._reconnectionOptions.maxReconnectionDelay; return Math.min(initialDelay * Math.pow(growFactor, attempt), maxDelay); } /** * Schedule a reconnection attempt using server-provided retry interval or backoff * * @param lastEventId The ID of the last received event for resumability * @param attemptCount Current reconnection attempt count for this specific stream */ _scheduleReconnection(options, attemptCount = 0) { const maxRetries = this._reconnectionOptions.maxRetries; if (attemptCount >= maxRetries) { this.onerror?.(/* @__PURE__ */ new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`)); options.onRequestStreamEnd?.(); return; } const delay = this._getNextReconnectionDelay(attemptCount); const reconnect = () => { this._cancelReconnection = void 0; if (this._abortController?.signal.aborted || options.requestSignal?.aborted) return; this._startOrAuthSse(options).catch((error) => { if (this._abortController?.signal.aborted || options.requestSignal?.aborted) return; this.onerror?.(/* @__PURE__ */ new Error(`Failed to reconnect SSE stream: ${error instanceof Error ? error.message : String(error)}`)); try { this._scheduleReconnection(options, attemptCount + 1); } catch (scheduleError) { this.onerror?.(scheduleError instanceof Error ? scheduleError : new Error(String(scheduleError))); } }); }; if (this._reconnectionScheduler) { const cancel = this._reconnectionScheduler(reconnect, delay, attemptCount); this._cancelReconnection = typeof cancel === "function" ? cancel : void 0; } else { const handle = setTimeout(reconnect, delay); this._cancelReconnection = () => clearTimeout(handle); } } _handleSseStream(stream, options, isReconnectable) { if (!stream) { options.onRequestStreamEnd?.(); return; } const { onresumptiontoken, replayMessageId, requestSignal, onRequestStreamEnd } = options; const isIntentionalAbort = () => this._abortController?.signal.aborted === true || requestSignal?.aborted === true; let lastEventId; let hasPrimingEvent = false; let receivedResponse = false; const processStream = async () => { try { const reader = stream.pipeThrough(new TextDecoderStream()).pipeThrough(new eventsource_parser_stream.EventSourceParserStream({ onRetry: (retryMs) => { this._serverRetryMs = retryMs; } })).getReader(); while (true) { const { value: event, done } = await reader.read(); if (done) break; if (event.id) { lastEventId = event.id; hasPrimingEvent = true; onresumptiontoken?.(event.id); } if (!event.data) continue; if (!event.event || event.event === "message") try { const message = _modelcontextprotocol_core_internal.JSONRPCMessageSchema.parse(JSON.parse(event.data)); if (require_src.isJSONRPCResultResponse(message) || require_src.isJSONRPCErrorResponse(message)) { receivedResponse = true; if (replayMessageId !== void 0) message.id = replayMessageId; } this.onmessage?.(message); } catch (error) { this.onerror?.(error); } } if ((isReconnectable || hasPrimingEvent) && !receivedResponse && this._abortController && !isIntentionalAbort()) this._scheduleReconnection({ resumptionToken: lastEventId, onresumptiontoken, replayMessageId, requestSignal, onRequestStreamEnd }, 0); else if (!isIntentionalAbort()) onRequestStreamEnd?.(); } catch (error) { if (isIntentionalAbort()) return; this.onerror?.(/* @__PURE__ */ new Error(`SSE stream disconnected: ${error}`)); if ((isReconnectable || hasPrimingEvent) && !receivedResponse && this._abortController && !isIntentionalAbort()) try { this._scheduleReconnection({ resumptionToken: lastEventId, onresumptiontoken, replayMessageId, requestSignal, onRequestStreamEnd }, 0); } catch (error$1) { this.onerror?.(/* @__PURE__ */ new Error(`Failed to reconnect: ${error$1 instanceof Error ? error$1.message : String(error$1)}`)); onRequestStreamEnd?.(); } else onRequestStreamEnd?.(); } }; processStream(); } async start() { if (this._abortController) throw new Error("StreamableHTTPClientTransport already started! If using Client class, note that connect() calls start() automatically."); this._abortController = new AbortController(); } async finishAuth(codeOrParams, iss) { if (!this._oauthProvider) throw new UnauthorizedError("finishAuth requires an OAuthClientProvider"); const { authorizationCode, iss: issParam } = await resolveAuthorizationCallbackParams(codeOrParams, iss, this._oauthProvider, this._url, { fetchFn: this._fetchWithInit, resourceMetadataUrl: this._resourceMetadataUrl }); if (await auth(this._oauthProvider, { serverUrl: this._url, authorizationCode, iss: issParam, resourceMetadataUrl: this._resourceMetadataUrl, scope: this._scope, fetchFn: this._fetchWithInit, skipIssuerMetadataValidation: this._skipIssuerMetadataValidation }) !== "AUTHORIZED") throw new UnauthorizedError("Failed to authorize"); } async close() { try { this._cancelReconnection?.(); } finally { this._cancelReconnection = void 0; this._abortController?.abort(); this.onclose?.(); } } async send(message, options) { return this._send(message, options, false); } async _send(message, options, isAuthRetry, stepUpRetries = 0) { try { const { resumptionToken, onresumptiontoken } = options || {}; if (resumptionToken) { this._startOrAuthSse({ resumptionToken, replayMessageId: require_src.isJSONRPCRequest(message) ? message.id : void 0, requestSignal: options?.requestSignal }).catch((error) => this.onerror?.(error)); return; } const headers = await this._commonHeaders(); this._applyBodyDerivedHeaders(headers, message); const isHandshake = Array.isArray(message) ? message.some((m) => require_src.isInitializeRequest(m)) : require_src.isInitializeRequest(message); if (isHandshake) headers.delete("mcp-session-id"); if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) { if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue; headers.set(name, value); } headers.set("content-type", "application/json"); const types = [ ...headers.get("accept")?.split(",").map((s) => s.trim().toLowerCase()) ?? [], "application/json", "text/event-stream" ]; headers.set("accept", [...new Set(types)].join(", ")); const transportSignal = this._abortController?.signal; const signal = options?.requestSignal !== void 0 && transportSignal !== void 0 ? anySignal(transportSignal, options.requestSignal) : options?.requestSignal ?? transportSignal; const init = { ...this._requestInit, method: "POST", headers, body: JSON.stringify(message), signal }; const response = await this._redirects(this._fetch ?? fetch)(this._url, init); if (isHandshake && response.ok) this._sessionId = response.headers.get("mcp-session-id") || void 0; if (!response.ok) { if (response.status === 401 && this._authProvider) { if (response.headers.has("www-authenticate")) { const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); this._resourceMetadataUrl = resourceMetadataUrl; this._scope = computeScopeUnion(this._scope, scope); } if (this._authProvider.onUnauthorized && !isAuthRetry) { try { await this._authProvider.onUnauthorized({ response, serverUrl: this._url, fetchFn: this._fetchWithInit }); } catch (error) { throw markAuthSeamEscape(error); } await response.text?.().catch(() => {}); return this._send(message, options, true, stepUpRetries); } await response.text?.().catch(() => {}); if (isAuthRetry) throw markAuthSeamEscape(new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { status: 401, statusText: response.statusText })); throw markAuthSeamEscape(new UnauthorizedError()); } const text = await response.text?.().catch(() => null); if (response.status === 403) { const { resourceMetadataUrl, scope, error, errorDescription } = extractWWWAuthenticateParams(response); if (error === "insufficient_scope") { if (await this._stepUpAuthorize({ scope, resourceMetadataUrl, errorDescription, statusText: response.statusText, text }, stepUpRetries) !== "AUTHORIZED") throw markAuthSeamEscape(new UnauthorizedError()); return this._send(message, options, isAuthRetry, stepUpRetries + 1); } } if (response.status === 400 && typeof text === "string" && this._isModernEnvelopedRequest(message)) try { const parsed = _modelcontextprotocol_core_internal.JSONRPCMessageSchema.parse(JSON.parse(text)); const requests = (Array.isArray(message) ? message : [message]).filter((m) => require_src.isJSONRPCRequest(m)); if (require_src.isJSONRPCErrorResponse(parsed) && requests.some((r) => r.id === parsed.id)) { this.onmessage?.(parsed); return; } } catch {} const reason = this._unfollowedRedirect(this._url, response) ?? text; throw new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpNotImplemented, `Error POSTing to endpoint: ${reason}`, { status: response.status, statusText: response.statusText, text }); } if (response.status === 202) { await response.text?.().catch(() => {}); if (require_src.isInitializedNotification(message)) this._startOrAuthSse({ resumptionToken: void 0 }).catch((error) => this.onerror?.(error)); return; } const hasRequests = (Array.isArray(message) ? message : [message]).some((msg) => "method" in msg && "id" in msg && msg.id !== void 0); const contentType = response.headers.get("content-type"); const responseMediaType = require_src.mediaTypeEssence(contentType); if (hasRequests) if (responseMediaType === "text/event-stream") this._handleSseStream(response.body, { onresumptiontoken, requestSignal: options?.requestSignal, onRequestStreamEnd: options?.onRequestStreamEnd }, false); else if (responseMediaType === "application/json") { const data = await response.json(); const responseMessages = Array.isArray(data) ? data.map((msg) => _modelcontextprotocol_core_internal.JSONRPCMessageSchema.parse(msg)) : [_modelcontextprotocol_core_internal.JSONRPCMessageSchema.parse(data)]; for (const msg of responseMessages) this.onmessage?.(msg); } else { await response.text?.().catch(() => {}); throw new require_src.SdkError(require_src.SdkErrorCode.ClientHttpUnexpectedContent, `Unexpected content type: ${contentType}`, { contentType }); } else await response.text?.().catch(() => {}); } catch (error) { if (options?.requestSignal?.aborted !== true) this.onerror?.(error); throw error; } } get sessionId() { return this._sessionId; } /** * Terminates the current session by sending a `DELETE` request to the server. * * Clients that no longer need a particular session * (e.g., because the user is leaving the client application) SHOULD send an * HTTP `DELETE` to the MCP endpoint with the `Mcp-Session-Id` header to explicitly * terminate the session. * * The server MAY respond with HTTP `405 Method Not Allowed`, indicating that * the server does not allow clients to terminate sessions. */ async terminateSession() { if (!this._sessionId) return; try { const headers = await this._commonHeaders(); const init = { ...this._requestInit, method: "DELETE", headers, signal: this._abortController?.signal }; const response = await this._redirects(this._fetch ?? fetch)(this._url, init); await response.text?.().catch(() => {}); if (!response.ok && response.status !== 405) throw new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpFailedToTerminateSession, `Failed to terminate session: ${this._unfollowedRedirect(this._url, response) ?? response.statusText}`, { status: response.status, statusText: response.statusText }); this._sessionId = void 0; } catch (error) { this.onerror?.(error); throw error; } } setProtocolVersion(version) { this._protocolVersion = version; } get protocolVersion() { return this._protocolVersion; } /** * Resume an SSE stream from a previous event ID. * Opens a `GET` SSE connection with `Last-Event-ID` header to replay missed events. * * @param lastEventId The event ID to resume from * @param options Optional callback to receive new resumption tokens */ async resumeStream(lastEventId, options) { await this._startOrAuthSse({ resumptionToken: lastEventId, onresumptiontoken: options?.onresumptiontoken }); } }; //#endregion //#region src/fromJsonSchema.ts let _defaultValidator; function fromJsonSchema(schema, validator) { return require_src.fromJsonSchema(schema, validator ?? (_defaultValidator ??= new _modelcontextprotocol_client__shims.DefaultJsonSchemaValidator())); } //#endregion exports.AuthorizationServerMismatchError = AuthorizationServerMismatchError; Object.defineProperty(exports, 'BAGGAGE_META_KEY', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.BAGGAGE_META_KEY; } }); Object.defineProperty(exports, 'CLIENT_CAPABILITIES_META_KEY', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.CLIENT_CAPABILITIES_META_KEY; } }); Object.defineProperty(exports, 'CLIENT_INFO_META_KEY', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.CLIENT_INFO_META_KEY; } }); exports.Client = Client; exports.ClientCredentialsProvider = ClientCredentialsProvider; exports.CrossAppAccessProvider = CrossAppAccessProvider; Object.defineProperty(exports, 'DEFAULT_NEGOTIATED_PROTOCOL_VERSION', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.DEFAULT_NEGOTIATED_PROTOCOL_VERSION; } }); exports.DEFAULT_REQUEST_TIMEOUT_MSEC = require_src.DEFAULT_REQUEST_TIMEOUT_MSEC; exports.DPOP_SUPPORTED_ALGS = DPOP_SUPPORTED_ALGS; exports.DpopSession = DpopSession; Object.defineProperty(exports, 'INTERNAL_ERROR', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.INTERNAL_ERROR; } }); Object.defineProperty(exports, 'INVALID_PARAMS', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.INVALID_PARAMS; } }); Object.defineProperty(exports, 'INVALID_REQUEST', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.INVALID_REQUEST; } }); exports.InMemoryResponseCacheStore = InMemoryResponseCacheStore; exports.InMemoryTransport = require_src.InMemoryTransport; exports.InsecureTokenEndpointError = InsecureTokenEndpointError; exports.InsufficientScopeError = InsufficientScopeError; exports.IssuerMismatchError = IssuerMismatchError; Object.defineProperty(exports, 'JSONRPC_VERSION', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.JSONRPC_VERSION; } }); Object.defineProperty(exports, 'LATEST_PROTOCOL_VERSION', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.LATEST_PROTOCOL_VERSION; } }); Object.defineProperty(exports, 'LOG_LEVEL_META_KEY', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.LOG_LEVEL_META_KEY; } }); exports.MAX_CACHE_TTL_MS = MAX_CACHE_TTL_MS; Object.defineProperty(exports, 'METHOD_NOT_FOUND', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.METHOD_NOT_FOUND; } }); exports.MissingRequiredClientCapabilityError = require_src.MissingRequiredClientCapabilityError; exports.OAuthClientFlowError = OAuthClientFlowError; exports.OAuthError = require_src.OAuthError; exports.OAuthErrorCode = require_src.OAuthErrorCode; Object.defineProperty(exports, 'PARSE_ERROR', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.PARSE_ERROR; } }); Object.defineProperty(exports, 'PROTOCOL_VERSION_META_KEY', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.PROTOCOL_VERSION_META_KEY; } }); exports.PrivateKeyJwtProvider = PrivateKeyJwtProvider; exports.Protocol = require_src.Protocol; exports.ProtocolError = require_src.ProtocolError; exports.ProtocolErrorCode = require_src.ProtocolErrorCode; Object.defineProperty(exports, 'RELATED_TASK_META_KEY', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.RELATED_TASK_META_KEY; } }); exports.ReadBuffer = require_src.ReadBuffer; exports.RegistrationRejectedError = RegistrationRejectedError; exports.ResourceNotFoundError = require_src.ResourceNotFoundError; Object.defineProperty(exports, 'SERVER_INFO_META_KEY', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.SERVER_INFO_META_KEY; } }); exports.SSEClientTransport = SSEClientTransport; exports.STDIO_DEFAULT_MAX_BUFFER_SIZE = require_src.STDIO_DEFAULT_MAX_BUFFER_SIZE; Object.defineProperty(exports, 'SUBSCRIPTION_ID_META_KEY', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.SUBSCRIPTION_ID_META_KEY; } }); Object.defineProperty(exports, 'SUPPORTED_PROTOCOL_VERSIONS', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.SUPPORTED_PROTOCOL_VERSIONS; } }); exports.SdkError = require_src.SdkError; exports.SdkErrorCode = require_src.SdkErrorCode; exports.SdkHttpError = require_src.SdkHttpError; exports.SseError = SseError; exports.StaticPrivateKeyJwtProvider = StaticPrivateKeyJwtProvider; exports.StreamableHTTPClientTransport = StreamableHTTPClientTransport; Object.defineProperty(exports, 'TRACEPARENT_META_KEY', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.TRACEPARENT_META_KEY; } }); Object.defineProperty(exports, 'TRACESTATE_META_KEY', { enumerable: true, get: function () { return _modelcontextprotocol_core_internal.TRACESTATE_META_KEY; } }); exports.UnauthorizedError = UnauthorizedError; exports.UnsupportedProtocolVersionError = require_src.UnsupportedProtocolVersionError; exports.UriTemplate = require_src.UriTemplate; exports.UrlElicitationRequiredError = require_src.UrlElicitationRequiredError; exports.accessTokenHash = accessTokenHash; exports.applyMiddlewares = applyMiddlewares; exports.assertCompleteRequestPrompt = require_src.assertCompleteRequestPrompt; exports.assertCompleteRequestResourceTemplate = require_src.assertCompleteRequestResourceTemplate; exports.assertSecureTokenEndpoint = assertSecureTokenEndpoint; exports.auth = auth; exports.buildDiscoveryUrls = buildDiscoveryUrls; exports.checkResourceAllowed = require_src.checkResourceAllowed; exports.computeScopeUnion = computeScopeUnion; exports.createFetchWithInit = require_src.createFetchWithInit; exports.createMiddleware = createMiddleware; exports.createPrivateKeyJwtAuth = createPrivateKeyJwtAuth; exports.deserializeMessage = require_src.deserializeMessage; exports.discoverAndRequestJwtAuthGrant = discoverAndRequestJwtAuthGrant; exports.discoverAuthorizationServerMetadata = discoverAuthorizationServerMetadata; exports.discoverOAuthMetadata = discoverOAuthMetadata; exports.discoverOAuthProtectedResourceMetadata = discoverOAuthProtectedResourceMetadata; exports.discoverOAuthServerInfo = discoverOAuthServerInfo; exports.exchangeAuthorization = exchangeAuthorization; exports.exchangeJwtAuthGrant = exchangeJwtAuthGrant; exports.extractResourceMetadataUrl = extractResourceMetadataUrl; exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams; exports.fetchToken = fetchToken; exports.fromJsonSchema = fromJsonSchema; exports.generateDpopKeyPair = generateDpopKeyPair; exports.getDisplayName = require_src.getDisplayName; exports.getSupportedElicitationModes = getSupportedElicitationModes; exports.isCallToolResult = require_src.isCallToolResult; exports.isDpopNonceChallenge = isDpopNonceChallenge; exports.isHttpsUrl = isHttpsUrl; exports.isInitializeRequest = require_src.isInitializeRequest; exports.isInitializedNotification = require_src.isInitializedNotification; exports.isInputRequiredResult = require_src.isInputRequiredResult; exports.isJSONRPCErrorResponse = require_src.isJSONRPCErrorResponse; exports.isJSONRPCNotification = require_src.isJSONRPCNotification; exports.isJSONRPCRequest = require_src.isJSONRPCRequest; exports.isJSONRPCResponse = require_src.isJSONRPCResponse; exports.isJSONRPCResultResponse = require_src.isJSONRPCResultResponse; exports.isJsonContentType = require_src.isJsonContentType; exports.isSpecType = require_src.isSpecType; exports.isStrictScopeSuperset = isStrictScopeSuperset; exports.isTaskAugmentedRequestParams = require_src.isTaskAugmentedRequestParams; exports.mergeCapabilities = require_src.mergeCapabilities; exports.parseErrorResponse = parseErrorResponse; exports.parseJSONRPCMessage = require_src.parseJSONRPCMessage; exports.preloadSchemas = require_src.preloadSchemas; exports.prepareAuthorizationCodeRequest = prepareAuthorizationCodeRequest; exports.refreshAuthorization = refreshAuthorization; exports.registerClient = registerClient; exports.requestJwtAuthorizationGrant = requestJwtAuthorizationGrant; exports.resolveClientMetadata = resolveClientMetadata; exports.resourceUrlFromServerUrl = require_src.resourceUrlFromServerUrl; exports.selectClientAuthMethod = selectClientAuthMethod; exports.selectResourceURL = selectResourceURL; exports.serializeMessage = require_src.serializeMessage; exports.specTypeSchemas = require_src.specTypeSchemas; exports.startAuthorization = startAuthorization; exports.validateAuthorizationResponseIssuer = validateAuthorizationResponseIssuer; exports.validateClientMetadataUrl = validateClientMetadataUrl; exports.withDpop = withDpop; exports.withDpopFromProvider = withDpopFromProvider; exports.withInputRequired = require_src.withInputRequired; exports.withLogging = withLogging; exports.withOAuth = withOAuth; //# sourceMappingURL=index.cjs.map