@ai-sdk/mcp
Version:
3,315 lines • 133 kB
JavaScript
import { EventSourceParserStream, asSchema, convertBase64ToUint8Array, convertUint8ArrayToBase64, delay, dynamicTool, fetchUntrustedUrl, fetchWithValidatedEndpoint, getRuntimeEnvironmentUserAgent, isRecord, jsonSchema, parseJSON, retryWithExponentialBackoff, safeParseJSON, safeValidateTypes, tool, validateDownloadUrl, withUserAgentSuffix } from "@ai-sdk/provider-utils";
import * as z4 from "zod/v4";
import { z } from "zod/v4";
import { AISDKError, isJSONObject } from "@ai-sdk/provider";
import pkceChallenge from "pkce-challenge";
//#region src/tool/types.ts
const LATEST_PROTOCOL_VERSION = "2026-07-28";
const LATEST_LEGACY_PROTOCOL_VERSION = "2025-11-25";
const SUPPORTED_PROTOCOL_VERSIONS = [
LATEST_PROTOCOL_VERSION,
LATEST_LEGACY_PROTOCOL_VERSION,
"2025-06-18",
"2025-03-26",
"2024-11-05"
];
/** MCP tool metadata - keys should follow MCP _meta key format specification */
const ToolMetaSchema = z.optional(z.record(z.string(), z.unknown()));
const ClientOrServerImplementationSchema = z.looseObject({
name: z.string(),
version: z.string(),
title: z.optional(z.string())
});
const BaseParamsSchema = z.looseObject({ _meta: z.optional(z.object({}).loose()) });
const ResultSchema = BaseParamsSchema.extend({ resultType: z.optional(z.string()) });
const RequestSchema = z.object({
method: z.string(),
params: z.optional(BaseParamsSchema)
});
/** @see https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation */
const ElicitationCapabilitySchema = z.object({ applyDefaults: z.optional(z.boolean()) }).loose();
const ServerCapabilitiesSchema = z.looseObject({
events: z.looseObject({ listChanged: z.boolean().nullish() }).nullish(),
experimental: z.optional(z.object({}).loose()),
logging: z.optional(z.object({}).loose()),
completions: z.optional(z.object({}).loose()),
prompts: z.optional(z.looseObject({ listChanged: z.optional(z.boolean()) })),
resources: z.optional(z.looseObject({
subscribe: z.optional(z.boolean()),
listChanged: z.optional(z.boolean())
})),
tools: z.optional(z.looseObject({ listChanged: z.optional(z.boolean()) })),
elicitation: z.optional(ElicitationCapabilitySchema)
});
z.object({ elicitation: z.optional(ElicitationCapabilitySchema) }).loose();
const DiscoverResultSchema = ResultSchema.extend({
supportedVersions: z.array(z.string()),
capabilities: ServerCapabilitiesSchema,
instructions: z.optional(z.string()),
ttlMs: z.optional(z.number()),
cacheScope: z.optional(z.union([z.literal("public"), z.literal("private")]))
});
const InitializeResultSchema = ResultSchema.extend({
protocolVersion: z.string(),
capabilities: ServerCapabilitiesSchema,
serverInfo: ClientOrServerImplementationSchema,
instructions: z.optional(z.string())
});
const PaginatedResultSchema = ResultSchema.extend({ nextCursor: z.optional(z.string()) });
const ToolSchema = z.object({
name: z.string(),
/**
* @see https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool
*/
title: z.optional(z.string()),
description: z.optional(z.string()),
inputSchema: z.looseObject({
type: z.optional(z.unknown()),
properties: z.optional(z.object({}).loose())
}),
/**
* @see https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema
*/
outputSchema: z.optional(z.object({}).loose()),
annotations: z.optional(z.object({
title: z.optional(z.string()),
readOnlyHint: z.optional(z.boolean()),
destructiveHint: z.optional(z.boolean()),
idempotentHint: z.optional(z.boolean()),
openWorldHint: z.optional(z.boolean())
}).loose()),
_meta: ToolMetaSchema
}).loose();
const ListToolsResultSchema = PaginatedResultSchema.extend({ tools: z.array(ToolSchema) });
const TextContentSchema = z.object({
type: z.literal("text"),
text: z.string()
}).loose();
const ImageContentSchema = z.object({
type: z.literal("image"),
data: z.base64(),
mimeType: z.string()
}).loose();
const ResourceSchema = z.object({
uri: z.string(),
name: z.string(),
title: z.optional(z.string()),
description: z.optional(z.string()),
mimeType: z.optional(z.string()),
size: z.optional(z.number())
}).loose();
const ListResourcesResultSchema = PaginatedResultSchema.extend({ resources: z.array(ResourceSchema) });
const ResourceContentsSchema = z.object({
/**
* The URI of this resource.
*/
uri: z.string(),
/**
* Optional display name of the resource content.
*/
name: z.optional(z.string()),
/**
* Optional human readable title.
*/
title: z.optional(z.string()),
/**
* The MIME type of this resource, if known.
*/
mimeType: z.optional(z.string())
}).loose();
const TextResourceContentsSchema = ResourceContentsSchema.extend({ text: z.string() });
const BlobResourceContentsSchema = ResourceContentsSchema.extend({ blob: z.base64() });
const EmbeddedResourceSchema = z.object({
type: z.literal("resource"),
resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema])
}).loose();
const ResourceLinkContentSchema = z.object({
type: z.literal("resource_link"),
uri: z.string(),
name: z.string(),
description: z.optional(z.string()),
mimeType: z.optional(z.string())
}).loose();
const UnknownContentSchema = z.object({ type: z.string() }).loose().refine((content) => ![
"text",
"image",
"resource",
"resource_link"
].includes(content.type), { message: "Known content types must match their schema" });
const CallToolResultWithContentSchema = ResultSchema.extend({
content: z.array(z.union([
TextContentSchema,
ImageContentSchema,
EmbeddedResourceSchema,
ResourceLinkContentSchema,
UnknownContentSchema
])),
/**
* @see https://modelcontextprotocol.io/specification/2026-07-28/server/tools#structured-content
*/
structuredContent: z.optional(z.unknown()),
isError: z.boolean().default(false).optional()
});
const CallToolResultWithStructuredContentSchema = ResultSchema.extend({
content: z.never().optional(),
structuredContent: z.json(),
isError: z.boolean().default(false).optional()
}).transform((result) => ({
...result,
content: [{
type: "text",
text: JSON.stringify(result.structuredContent)
}]
})).pipe(CallToolResultWithContentSchema);
const CallToolResultSchema = CallToolResultWithContentSchema.or(CallToolResultWithStructuredContentSchema).or(ResultSchema.extend({ toolResult: z.unknown().nonoptional() }));
const ResourceTemplateSchema = z.object({
uriTemplate: z.string(),
name: z.string(),
title: z.optional(z.string()),
description: z.optional(z.string()),
mimeType: z.optional(z.string())
}).loose();
const ListResourceTemplatesResultSchema = ResultSchema.extend({ resourceTemplates: z.array(ResourceTemplateSchema) });
const ReadResourceResultSchema = ResultSchema.extend({ contents: z.array(z.union([TextResourceContentsSchema, BlobResourceContentsSchema])) });
const PromptReferenceSchema = z.object({
type: z.literal("ref/prompt"),
name: z.string()
}).loose();
const ResourceReferenceSchema = z.object({
type: z.literal("ref/resource"),
uri: z.string()
}).loose();
const CompletionArgumentSchema = z.object({
name: z.string(),
value: z.string()
}).loose();
BaseParamsSchema.extend({
ref: z.union([PromptReferenceSchema, ResourceReferenceSchema]),
argument: CompletionArgumentSchema,
context: z.optional(z.object({ arguments: z.record(z.string(), z.string()) }).loose())
});
const CompleteResultSchema = ResultSchema.extend({ completion: z.object({
values: z.array(z.string()).max(100),
total: z.optional(z.number().int()),
hasMore: z.optional(z.boolean())
}).loose() });
const PromptArgumentSchema = z.object({
name: z.string(),
description: z.optional(z.string()),
required: z.optional(z.boolean())
}).loose();
const PromptSchema = z.object({
name: z.string(),
title: z.optional(z.string()),
description: z.optional(z.string()),
arguments: z.optional(z.array(PromptArgumentSchema))
}).loose();
const ListPromptsResultSchema = PaginatedResultSchema.extend({ prompts: z.array(PromptSchema) });
const PromptMessageSchema = z.object({
role: z.union([z.literal("user"), z.literal("assistant")]),
content: z.union([
TextContentSchema,
ImageContentSchema,
EmbeddedResourceSchema,
ResourceLinkContentSchema
])
}).loose();
const GetPromptResultSchema = ResultSchema.extend({
description: z.optional(z.string()),
messages: z.array(PromptMessageSchema)
});
const ElicitationRequestParamsSchema = BaseParamsSchema.extend({
message: z.string(),
requestedSchema: z.unknown()
});
const ElicitationRequestSchema = RequestSchema.extend({
method: z.literal("elicitation/create"),
params: ElicitationRequestParamsSchema
});
const ElicitResultSchema = ResultSchema.extend({
action: z.union([
z.literal("accept"),
z.literal("decline"),
z.literal("cancel")
]),
content: z.optional(z.record(z.string(), z.unknown()))
});
const InputRequiredResultSchema = ResultSchema.extend({
resultType: z.literal("input_required"),
inputRequests: z.record(z.string(), RequestSchema).optional(),
requestState: z.string().optional()
}).refine((result) => result.inputRequests !== void 0 || result.requestState !== void 0, { message: "input_required must include inputRequests or requestState" });
const InputRequiredElicitationSchema = ElicitationRequestSchema.extend({ params: ElicitationRequestParamsSchema.extend({
mode: z.literal("form").optional(),
requestedSchema: z.looseObject({ type: z.literal("object") })
}) });
//#endregion
//#region src/tool/json-rpc-message.ts
const JSONRPC_VERSION = "2.0";
const JSONRPCRequestSchema = z.object({
jsonrpc: z.literal(JSONRPC_VERSION),
id: z.union([z.string(), z.number().int()])
}).merge(RequestSchema).strict();
const JSONRPCResponseSchema = z.object({
jsonrpc: z.literal(JSONRPC_VERSION),
id: z.union([z.string(), z.number().int()]),
result: ResultSchema
}).strict();
const JSONRPCErrorSchema = z.object({
jsonrpc: z.literal(JSONRPC_VERSION),
id: z.optional(z.union([z.string(), z.number().int()])),
error: z.object({
code: z.number().int(),
message: z.string(),
data: z.optional(z.unknown())
})
}).strict();
const JSONRPCNotificationSchema = z.object({ jsonrpc: z.literal(JSONRPC_VERSION) }).merge(z.object({
method: z.string(),
params: z.optional(BaseParamsSchema)
})).strict();
const JSONRPCMessageSchema = z.union([
JSONRPCRequestSchema,
JSONRPCNotificationSchema,
JSONRPCResponseSchema,
JSONRPCErrorSchema
]);
function validateJSONRPCMessage(message) {
return JSONRPCMessageSchema.parse(message);
}
async function parseJSONRPCMessage(text) {
return validateJSONRPCMessage(await parseJSON({ text }));
}
//#endregion
//#region src/error/mcp-client-error.ts
const marker$1 = `vercel.ai.error.AI_MCPClientError`;
const symbol$1 = Symbol.for(marker$1);
/**
* An error occurred with the MCP client.
*/
var MCPClientError = class extends AISDKError {
constructor({ name = "MCPClientError", message, cause, data, code, statusCode, url, responseBody }) {
super({
name,
message,
cause
});
this[symbol$1] = true;
this.data = data;
this.code = code;
this.statusCode = statusCode;
this.url = url;
this.responseBody = responseBody;
}
static isInstance(error) {
return AISDKError.hasMarker(error, marker$1);
}
};
//#endregion
//#region src/tool/mcp-event-signature.ts
function createMCPEventSecret() {
return `whsec_${convertUint8ArrayToBase64(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)))}`;
}
function decodeMCPEventSecret(secret) {
const encoded = secret.slice(6);
if (!secret.startsWith("whsec_") || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) throw new MCPClientError({ message: "Invalid MCP event signing secret" });
const bytes = convertBase64ToUint8Array(encoded);
if (bytes.length < 24 || bytes.length > 64) throw new MCPClientError({ message: "MCP event signing secrets must contain 24–64 random bytes" });
return bytes;
}
/** Standard Webhooks v1: authenticate the exact body bytes using Web Crypto. */
async function verifyMCPEventSignature({ headers, body, secret }) {
const id = headers.get("webhook-id");
const timestamp = headers.get("webhook-timestamp");
const signatures = headers.get("webhook-signature");
if (!id || !timestamp || !signatures || !/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1e3 - Number(timestamp)) > 300) return false;
const key = await crypto.subtle.importKey("raw", new Uint8Array(decodeMCPEventSecret(secret)), {
name: "HMAC",
hash: "SHA-256"
}, false, ["verify"]);
const prefix = new TextEncoder().encode(`${id}.${timestamp}.`);
const message = new Uint8Array(prefix.length + body.length);
message.set(prefix);
message.set(body, prefix.length);
for (const signature of signatures.split(" ")) {
const [version, value] = signature.split(",");
if (version !== "v1" || !value) continue;
try {
if (await crypto.subtle.verify("HMAC", key, new Uint8Array(convertBase64ToUint8Array(value)), message)) return true;
} catch {}
}
return false;
}
//#endregion
//#region src/tool/mcp-event-types.ts
const JsonSchemaSchema = z.union([z.boolean(), z.record(z.string(), z.unknown())]);
const MCPEventDefinitionSchema = z.looseObject({
name: z.string().min(1),
description: z.string().nullish(),
delivery: z.array(z.enum([
"webhook",
"poll",
"push"
])).min(1),
inputSchema: JsonSchemaSchema,
payloadSchema: JsonSchemaSchema
});
const ListEventsResultSchema = ResultSchema.extend({
events: z.array(MCPEventDefinitionSchema),
nextCursor: z.string().nullish()
});
const SubscribeEventResultSchema = ResultSchema.extend({
id: z.string().min(1),
refreshBefore: z.iso.datetime({ offset: true }).nullable(),
cursor: z.string().nullish().transform((cursor) => cursor ?? null),
truncated: z.boolean()
});
const MCPEventSchema = z.looseObject({
eventId: z.string().min(1),
name: z.string().min(1),
timestamp: z.iso.datetime({ offset: true }),
data: z.record(z.string(), z.unknown()),
cursor: z.string().nullish()
});
const MCPEventControlSchema = z.discriminatedUnion("type", [z.looseObject({
type: z.literal("gap"),
cursor: z.string()
}), z.looseObject({
type: z.literal("terminated"),
error: z.looseObject({
code: z.number().int(),
message: z.string(),
data: z.unknown().nullish()
})
})]).transform((control) => control.type === "gap" ? {
...control,
truncated: true
} : control);
//#endregion
//#region src/tool/mcp-events.ts
const MCP_EVENT_CALLBACK_KEY = "mcp_event_subscription";
function createMCPEvents({ request, store, validateArguments }) {
function getStore() {
if (!store) throw new MCPClientError({ message: "Configure experimental_events.store to manage direct event subscriptions" });
return store;
}
async function getSubscription({ id, key }) {
const storage = getStore();
const subscription = id !== void 0 ? await storage.getById(id) : await storage.get(key);
if (!subscription) throw new MCPClientError({ message: `Unknown MCP event subscription: ${id ?? key}` });
return subscription;
}
async function subscribe(subscription, options) {
const result = await request({
request: {
method: "events/subscribe",
params: {
name: subscription.name,
arguments: subscription.arguments,
delivery: subscription.delivery,
cursor: subscription.cursor,
...subscription.ttlMs === void 0 ? {} : { ttlMs: subscription.ttlMs },
...subscription.maxAgeMs === void 0 ? {} : { maxAgeMs: subscription.maxAgeMs }
}
},
resultSchema: SubscribeEventResultSchema,
options
});
await getStore().update(subscription.key, {
id: result.id,
refreshBefore: result.refreshBefore,
cursor: result.cursor,
truncated: result.truncated,
status: "active"
});
return result;
}
const events = {
list({ params, options } = {}) {
return request({
request: {
method: "events/list",
params
},
resultSchema: ListEventsResultSchema,
options
});
},
async subscribe({ name, arguments: args = {}, delivery, cursor = null, ttlMs, maxAgeMs, options }) {
const storage = getStore();
const url = new URL(delivery.url);
const localHttp = delivery.allowInsecureLocalhost === true && url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "[::1]");
if (url.protocol !== "https:" && !localHttp || url.username || url.password || url.hash) throw new MCPClientError({ message: "MCP event callback URLs must use HTTPS without credentials or fragments" });
if (delivery.mode !== "webhook") throw new MCPClientError({ message: "Only webhook event delivery is supported" });
if (ttlMs != null && (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) || maxAgeMs != null && (!Number.isSafeInteger(maxAgeMs) || maxAgeMs < 0)) throw new MCPClientError({ message: "Invalid MCP event subscription lifetime or replay age" });
let pageCursor;
const visited = /* @__PURE__ */ new Set();
let definition;
do {
const page = await events.list({
params: { cursor: pageCursor },
options
});
definition = page.events.find((event) => event.name === name);
if (definition) break;
pageCursor = page.nextCursor ?? void 0;
if (pageCursor != null) {
if (visited.has(pageCursor)) throw new MCPClientError({ message: "Repeated MCP events pagination cursor" });
visited.add(pageCursor);
}
} while (pageCursor != null);
if (!definition) throw new MCPClientError({ message: `Unknown MCP event: ${name}` });
if (!definition.delivery.includes("webhook")) throw new MCPClientError({ message: `MCP event ${name} does not support webhook delivery` });
await validateArguments?.({
definition,
arguments: args
});
const secret = delivery.secret ?? createMCPEventSecret();
decodeMCPEventSecret(secret);
const key = crypto.randomUUID();
url.searchParams.set(MCP_EVENT_CALLBACK_KEY, key);
const subscription = {
key,
name,
arguments: structuredClone(args),
definition,
delivery: {
mode: "webhook",
url: url.href,
secret
},
cursor,
truncated: false,
ttlMs,
maxAgeMs,
status: "pending"
};
await storage.set(subscription);
return subscribe(subscription, options);
},
async refresh(args) {
const subscription = await getSubscription(args);
await getStore().update(subscription.key, { status: "pending" });
return subscribe(subscription, args.options);
},
async unsubscribe(args) {
const subscription = await getSubscription(args);
await request({
request: {
method: "events/unsubscribe",
params: {
name: subscription.name,
arguments: subscription.arguments,
delivery: {
mode: "webhook",
url: subscription.delivery.url
}
}
},
resultSchema: ResultSchema,
options: args.options
});
await getStore().delete(subscription.key);
}
};
return events;
}
//#endregion
//#region src/tool/mcp-events-adapter.ts
/** Validate JavaScript integrations before opening the MCP transport. */
function validateMCPEventOperations(operations) {
if (operations != null && (typeof operations === "object" || typeof operations === "function") && "then" in operations && typeof operations.then === "function") {
Promise.resolve(operations).catch(() => {});
throw new MCPClientError({ message: "experimental_events.adapter.createAdapter() must return operations synchronously. Promise and thenable results are not supported." });
}
if (typeof operations !== "object" || operations === null || !("subscribe" in operations) || typeof operations.subscribe !== "function" || !("getSubscription" in operations) || typeof operations.getSubscription !== "function" || !("listSubscriptions" in operations) || typeof operations.listSubscriptions !== "function" || !("unsubscribe" in operations) || typeof operations.unsubscribe !== "function") throw new MCPClientError({ message: "experimental_events.adapter.createAdapter() must return an object implementing subscribe, getSubscription, listSubscriptions, and unsubscribe." });
}
function createManagedMCPEvents(operations, list) {
return {
list,
subscribe: (input) => operations.subscribe(input),
getSubscription: (input) => operations.getSubscription(input),
listSubscriptions: (input) => operations.listSubscriptions(input),
unsubscribe: (input) => operations.unsubscribe(input)
};
}
//#endregion
//#region src/version.ts
const VERSION = "2.0.99";
//#endregion
//#region src/tool/oauth-types.ts
/**
* Reusable URL validation that disallows javascript: scheme
*/
const SafeUrlSchema = z.string().url().superRefine((val, ctx) => {
if (!URL.canParse(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "URL must be parseable",
fatal: true
});
return z.NEVER;
}
}).refine((url) => {
const parsedUrl = new URL(url);
return parsedUrl.protocol !== "javascript:" && parsedUrl.protocol !== "data:" && parsedUrl.protocol !== "vbscript:";
}, { message: "URL cannot use javascript:, data:, or vbscript: scheme" });
/**
* OAuth 2.1 token response
*/
const OAuthTokensSchema = z.object({
access_token: z.string(),
id_token: z.string().optional(),
token_type: z.string(),
expires_in: z.number().optional(),
scope: z.string().optional(),
refresh_token: z.string().optional(),
issuer: SafeUrlSchema.optional(),
authorization_server: SafeUrlSchema.optional(),
token_endpoint: SafeUrlSchema.optional()
}).strip();
const OAuthProtectedResourceMetadataSchema = z.looseObject({
resource: z.string().url(),
authorization_servers: z.array(SafeUrlSchema).optional(),
jwks_uri: z.string().url().optional(),
scopes_supported: z.array(z.string()).optional(),
bearer_methods_supported: z.array(z.string()).optional(),
resource_signing_alg_values_supported: z.array(z.string()).optional(),
resource_name: z.string().optional(),
resource_documentation: z.string().optional(),
resource_policy_uri: z.string().url().optional(),
resource_tos_uri: z.string().url().optional(),
tls_client_certificate_bound_access_tokens: z.boolean().optional(),
authorization_details_types_supported: z.array(z.string()).optional(),
dpop_signing_alg_values_supported: z.array(z.string()).optional(),
dpop_bound_access_tokens_required: z.boolean().optional()
});
const OAuthMetadataSchema = z.looseObject({
issuer: z.string(),
authorization_endpoint: SafeUrlSchema,
token_endpoint: SafeUrlSchema,
registration_endpoint: SafeUrlSchema.optional(),
authorization_response_iss_parameter_supported: z.boolean().optional(),
client_id_metadata_document_supported: z.boolean().optional(),
scopes_supported: z.array(z.string()).optional(),
response_types_supported: z.array(z.string()),
grant_types_supported: z.array(z.string()).optional(),
code_challenge_methods_supported: z.array(z.string()).optional(),
token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
token_endpoint_auth_signing_alg_values_supported: z.array(z.string()).optional()
});
/**
* OpenID Connect Discovery metadata that may include OAuth 2.0 fields
* This schema represents the real-world scenario where OIDC providers
* return a mix of OpenID Connect and OAuth 2.0 metadata fields
*/
const OpenIdProviderDiscoveryMetadataSchema = z.looseObject({
issuer: z.string(),
authorization_endpoint: SafeUrlSchema,
token_endpoint: SafeUrlSchema,
userinfo_endpoint: SafeUrlSchema.optional(),
jwks_uri: SafeUrlSchema,
registration_endpoint: SafeUrlSchema.optional(),
authorization_response_iss_parameter_supported: z.boolean().optional(),
client_id_metadata_document_supported: z.boolean().optional(),
scopes_supported: z.array(z.string()).optional(),
response_types_supported: z.array(z.string()),
grant_types_supported: z.array(z.string()).optional(),
subject_types_supported: z.array(z.string()),
id_token_signing_alg_values_supported: z.array(z.string()),
claims_supported: z.array(z.string()).optional(),
token_endpoint_auth_methods_supported: z.array(z.string()).optional()
}).merge(OAuthMetadataSchema.pick({ code_challenge_methods_supported: true }));
const OAuthClientInformationSchema = z.object({
client_id: z.string(),
client_secret: z.string().optional(),
client_id_issued_at: z.number().optional(),
client_secret_expires_at: z.number().optional(),
issuer: SafeUrlSchema.optional(),
authorization_server: SafeUrlSchema.optional(),
token_endpoint: SafeUrlSchema.optional()
}).strip();
const OAuthClientMetadataSchema = z.object({
redirect_uris: z.array(SafeUrlSchema),
application_type: z.union([z.literal("native"), z.literal("web")]).optional(),
token_endpoint_auth_method: z.string().optional(),
grant_types: z.array(z.string()).optional(),
response_types: z.array(z.string()).optional(),
client_name: z.string().optional(),
client_uri: SafeUrlSchema.optional(),
logo_uri: SafeUrlSchema.optional(),
scope: z.string().optional(),
contacts: z.array(z.string()).optional(),
tos_uri: SafeUrlSchema.optional(),
policy_uri: z.string().optional(),
jwks_uri: SafeUrlSchema.optional(),
jwks: z.any().optional(),
software_id: z.string().optional(),
software_version: z.string().optional(),
software_statement: z.string().optional()
}).strip();
const OAuthErrorResponseSchema = z.object({
error: z.string(),
error_description: z.string().optional(),
error_uri: z.string().optional()
});
const OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge(OAuthClientInformationSchema);
//#endregion
//#region src/error/oauth-error.ts
const marker = `vercel.ai.error.AI_MCPClientOAuthError`;
const symbol = Symbol.for(marker);
/**
* An error occurred with the MCP client within the OAuth flow.
*/
var MCPClientOAuthError = class extends AISDKError {
constructor({ name = "MCPClientOAuthError", message, cause }) {
super({
name,
message,
cause
});
this[symbol] = true;
}
static isInstance(error) {
return AISDKError.hasMarker(error, marker);
}
};
var ServerError = class extends MCPClientOAuthError {
static {
this.errorCode = "server_error";
}
};
var InvalidClientError = class extends MCPClientOAuthError {
static {
this.errorCode = "invalid_client";
}
};
var InvalidGrantError = class extends MCPClientOAuthError {
static {
this.errorCode = "invalid_grant";
}
};
var UnauthorizedClientError = class extends MCPClientOAuthError {
static {
this.errorCode = "unauthorized_client";
}
};
const mismatchMarker = `vercel.ai.error.AI_AuthorizationServerMismatchError`;
const mismatchSymbol = Symbol.for(mismatchMarker);
var AuthorizationServerMismatchError = class extends MCPClientOAuthError {
static {
this.errorCode = "authorization_server_mismatch";
}
constructor({ message, cause }) {
super({
name: "AuthorizationServerMismatchError",
message,
cause
});
this[mismatchSymbol] = true;
}
static isInstance(error) {
return AISDKError.hasMarker(error, mismatchMarker);
}
};
const OAUTH_ERRORS = {
[ServerError.errorCode]: ServerError,
[InvalidClientError.errorCode]: InvalidClientError,
[InvalidGrantError.errorCode]: InvalidGrantError,
[UnauthorizedClientError.errorCode]: UnauthorizedClientError
};
//#endregion
//#region src/util/oauth-util.ts
/**
* Utilities for handling OAuth resource URIs.
*/
/**
* Converts a server URL to a resource URL by removing the fragment.
* RFC 8707 section 2 states that resource URIs "MUST NOT include a fragment component".
* Keeps everything else unchanged (scheme, domain, port, path, query).
*/
function resourceUrlFromServerUrl(url) {
const resourceURL = typeof url === "string" ? new URL(url) : new URL(url.href);
resourceURL.hash = "";
return resourceURL;
}
/**
* Serializes a resource URL to a string, removing the trailing slash that
* URL.href adds to pathless URLs. Per the MCP spec, implementations SHOULD
* use the form without the trailing slash for better interoperability.
*/
function resourceUrlStripSlash(resource) {
const href = resource.href;
if (resource.pathname === "/" && href.endsWith("/")) return href.slice(0, -1);
return href;
}
/**
* Checks if a requested resource URL matches a configured resource URL.
* A requested resource matches if it has the same scheme, domain, port,
* and its path starts with the configured resource's path.
*
* @param requestedResource The resource URL being requested
* @param configuredResource The resource URL that has been configured
* @returns true if the requested resource matches the configured resource, false otherwise
*/
function checkResourceAllowed({ requestedResource, configuredResource }) {
const requested = typeof requestedResource === "string" ? new URL(requestedResource) : new URL(requestedResource.href);
const configured = typeof configuredResource === "string" ? new URL(configuredResource) : new URL(configuredResource.href);
if (requested.origin !== configured.origin) return false;
if (requested.pathname.length < configured.pathname.length) return false;
const requestedPath = requested.pathname.endsWith("/") ? requested.pathname : requested.pathname + "/";
const configuredPath = configured.pathname.endsWith("/") ? configured.pathname : configured.pathname + "/";
return requestedPath.startsWith(configuredPath);
}
//#endregion
//#region src/tool/oauth.ts
var UnauthorizedError = class extends Error {
constructor(message = "Unauthorized") {
super(message);
this.name = "UnauthorizedError";
}
};
function normalizeUrl(url) {
return new URL(url).href;
}
/** Allow loopback HTTP(S) for local MCP OAuth (RFC 8252 §7.3, RFC 6761 §6.3). */
function isOAuthLoopbackHost(hostname) {
const normalized = hostname.toLowerCase().replace(/\.+$/, "");
return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "127.0.0.1" || normalized === "[::1]" || normalized === "::1";
}
/**
* Guards metadata-derived OAuth URLs before they are requested. Loopback is
* allowed only when the caller has established that it belongs to a locally
* configured OAuth server; every other target uses the shared URL guard.
*
* Credential POSTs use `fetchWithValidatedEndpoint`, which enforces
* `redirect: 'error'` so the authorization code, PKCE verifier, and client
* secret are never replayed to a redirect target.
*/
function assertSafeOAuthEndpoint(endpointUrl, { allowLoopback = false } = {}) {
if (allowLoopback && (endpointUrl.protocol === "http:" || endpointUrl.protocol === "https:") && isOAuthLoopbackHost(endpointUrl.hostname)) return;
try {
validateDownloadUrl(endpointUrl.href);
} catch (error) {
throw new MCPClientOAuthError({
message: `OAuth endpoint URL is not allowed: ${endpointUrl.href}`,
cause: error
});
}
}
function getTrustedLoopbackOrigin(authorizationServerUrl) {
const url = new URL(authorizationServerUrl);
return isOAuthLoopbackHost(url.hostname) ? url.origin : void 0;
}
function validateAuthorizationResponseIssuer({ callbackIssuer, expectedIssuer }) {
if (callbackIssuer != null && callbackIssuer !== expectedIssuer) throw new MCPClientOAuthError({ message: `OAuth authorization response issuer ${callbackIssuer} does not match expected issuer ${expectedIssuer}` });
}
function createAuthorizationServerInformation(authorizationServerUrl, metadata) {
return {
issuer: metadata?.issuer ?? String(authorizationServerUrl),
authorizationServerUrl: normalizeUrl(authorizationServerUrl),
tokenEndpoint: normalizeUrl(metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl))
};
}
function addAuthorizationServerInformationToTokens(tokens, authorizationServerInformation) {
return {
...tokens,
issuer: authorizationServerInformation.issuer,
authorization_server: authorizationServerInformation.authorizationServerUrl,
token_endpoint: authorizationServerInformation.tokenEndpoint
};
}
function addAuthorizationServerInformationToClientInformation(clientInformation, authorizationServerInformation) {
return {
...clientInformation,
issuer: authorizationServerInformation.issuer,
authorization_server: authorizationServerInformation.authorizationServerUrl,
token_endpoint: authorizationServerInformation.tokenEndpoint
};
}
function getAuthorizationServerInformationFromCredentials(credentials) {
if (!credentials?.authorization_server || !credentials.token_endpoint) return;
return {
issuer: credentials.issuer,
authorizationServerUrl: normalizeUrl(credentials.authorization_server),
tokenEndpoint: normalizeUrl(credentials.token_endpoint)
};
}
async function getStoredAuthorizationServerInformation({ provider, clientInformation, tokens }) {
const tokenAuthorizationServerInformation = getAuthorizationServerInformationFromCredentials(tokens);
if (tokenAuthorizationServerInformation) return tokenAuthorizationServerInformation;
const providerAuthorizationServerInformation = await provider.authorizationServerInformation?.();
if (providerAuthorizationServerInformation) return {
issuer: providerAuthorizationServerInformation.issuer,
authorizationServerUrl: normalizeUrl(providerAuthorizationServerInformation.authorizationServerUrl),
tokenEndpoint: normalizeUrl(providerAuthorizationServerInformation.tokenEndpoint)
};
return getAuthorizationServerInformationFromCredentials(clientInformation);
}
async function saveAuthorizationServerInformation({ provider, clientInformation, authorizationServerInformation }) {
if (provider.saveAuthorizationServerInformation) {
await provider.saveAuthorizationServerInformation(authorizationServerInformation);
return true;
}
if (provider.saveClientInformation) {
await provider.saveClientInformation(addAuthorizationServerInformationToClientInformation(clientInformation, authorizationServerInformation));
return true;
}
return false;
}
function assertResourceMetadataUrlSameOrigin(serverUrl, resourceMetadataUrl) {
if (!resourceMetadataUrl) return;
const expectedOrigin = new URL(serverUrl).origin;
if (resourceMetadataUrl.origin !== expectedOrigin) throw new MCPClientOAuthError({ message: `OAuth protected resource metadata URL ${resourceMetadataUrl.href} must have the same origin as the MCP server URL ${expectedOrigin}` });
}
function assertAuthorizationServerInformationMatches({ storedAuthorizationServerInformation, currentAuthorizationServerInformation }) {
if (storedAuthorizationServerInformation.issuer != null && currentAuthorizationServerInformation.issuer != null && storedAuthorizationServerInformation.issuer !== currentAuthorizationServerInformation.issuer || storedAuthorizationServerInformation.authorizationServerUrl !== currentAuthorizationServerInformation.authorizationServerUrl || storedAuthorizationServerInformation.tokenEndpoint !== currentAuthorizationServerInformation.tokenEndpoint) throw new AuthorizationServerMismatchError({ message: "OAuth authorization server metadata does not match the metadata that issued the stored credentials" });
}
function extractWWWAuthenticateParams(response) {
const header = response.headers.get("www-authenticate") ?? response.headers.get("WWW-Authenticate");
if (!header) return {};
const [type, scheme] = header.split(" ");
if (type.toLowerCase() !== "bearer" || !scheme) return {};
const resourceMetadataMatch = header.match(/(?:^|[,\s])resource_metadata="([^"]*)"/i);
const scope = header.match(/(?:^|[,\s])scope="([^"]*)"/i)?.[1];
let resourceMetadataUrl;
try {
resourceMetadataUrl = resourceMetadataMatch ? new URL(resourceMetadataMatch[1]) : void 0;
} catch {}
return {
resourceMetadataUrl,
scope
};
}
function selectScope({ scope, resourceMetadata, clientMetadata }) {
if (scope) return scope;
const resourceScopes = resourceMetadata?.scopes_supported?.join(" ");
if (resourceScopes) return resourceScopes;
return clientMetadata.scope;
}
/**
* 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}`;
}
async function fetchWithCorsRetry(url, headers, fetchFn = fetch, trustedOrigin) {
try {
return await fetchUntrustedUrl({
url: url.href,
fetch: async (input, init) => fetchWithValidatedEndpoint({
url: input,
init: {
...init,
headers: new Headers(init?.headers).has("MCP-Protocol-Version") ? headers : void 0
},
fetch: fetchFn,
trustedOrigin,
redirect: "manual",
requireDnsValidation: true
}),
headers,
trustedOrigin,
untrustedFirstHopHeaders: ["mcp-protocol-version"]
});
} catch (error) {
if (error instanceof TypeError) {
if (headers) return fetchWithCorsRetry(url, void 0, fetchFn, trustedOrigin);
else return;
}
throw error;
}
}
/**
* Tries to discover OAuth metadata at a specific URL
*/
async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch, trustedOrigin) {
return await fetchWithCorsRetry(url, { "MCP-Protocol-Version": protocolVersion }, fetchFn, trustedOrigin);
}
/**
* Determines if fallback to root discovery should be attempted
*/
function shouldAttemptFallback(response, pathname) {
return !response || response.status >= 400 && response.status < 500 && pathname !== "/";
}
/**
* 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 ?? "2026-07-28";
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, opts?.trustedOrigin);
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) response = await tryMetadataDiscovery(new URL(`/.well-known/${wellKnownType}`, issuer), protocolVersion, fetchFn, opts?.trustedOrigin);
return response;
}
async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn = fetch) {
const response = await discoverMetadataWithFallback(serverUrl, "oauth-protected-resource", fetchFn, {
protocolVersion: opts?.protocolVersion,
metadataUrl: opts?.resourceMetadataUrl,
trustedOrigin: new URL(serverUrl).origin
});
if (!response || response.status === 404) throw new Error(`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`);
if (!response.ok) throw new Error(`HTTP ${response.status} trying to load well-known OAuth protected resource metadata.`);
return OAuthProtectedResourceMetadataSchema.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. OAuth metadata at root (if URL has path)
* 3. OIDC metadata endpoints
*/
function buildDiscoveryUrls(authorizationServerUrl) {
const url = typeof authorizationServerUrl === "string" ? new URL(authorizationServerUrl) : authorizationServerUrl;
const hasPath = url.pathname !== "/";
const rootIssuer = url.origin;
const urlsToTry = [];
if (!hasPath) {
urlsToTry.push({
url: new URL("/.well-known/oauth-authorization-server", url.origin),
type: "oauth",
expectedIssuer: rootIssuer
});
urlsToTry.push({
url: new URL("/.well-known/openid-configuration", url.origin),
type: "oidc",
expectedIssuer: rootIssuer
});
return urlsToTry;
}
let pathname = url.pathname;
if (pathname.endsWith("/")) pathname = pathname.slice(0, -1);
const pathIssuer = `${url.origin}${pathname}`;
urlsToTry.push({
url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin),
type: "oauth",
expectedIssuer: pathIssuer
});
urlsToTry.push({
url: new URL("/.well-known/oauth-authorization-server", url.origin),
type: "oauth",
expectedIssuer: rootIssuer
});
urlsToTry.push({
url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
type: "oidc",
expectedIssuer: pathIssuer
});
const oidcDiscoveryUrl = new URL(url.origin);
oidcDiscoveryUrl.pathname = `${pathname}/.well-known/openid-configuration`;
urlsToTry.push({
url: oidcDiscoveryUrl,
type: "oidc",
expectedIssuer: pathIssuer
});
return urlsToTry;
}
function assertMetadataIssuerMatches(metadata, expectedIssuer) {
if (!(metadata.issuer === expectedIssuer || expectedIssuer === new URL(expectedIssuer).origin && metadata.issuer === `${expectedIssuer}/`)) throw new MCPClientOAuthError({ message: `OAuth authorization server metadata issuer ${metadata.issuer} does not match expected issuer ${expectedIssuer}` });
}
async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn = fetch, protocolVersion = LATEST_PROTOCOL_VERSION, trustedOrigin } = {}) {
const headers = { "MCP-Protocol-Version": protocolVersion };
const authorizationServerOrigin = new URL(authorizationServerUrl).origin;
const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
if (urlsToTry.some(({ url }) => url.origin !== authorizationServerOrigin)) throw new MCPClientOAuthError({ message: "OAuth discovery URL changed authorization-server origin" });
for (const { url: endpointUrl, type, expectedIssuer } of urlsToTry) {
const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn, trustedOrigin);
if (!response)
/**
* CORS error occurred - don't throw as the endpoint may not allow CORS,
* continue trying other possible endpoints
*/
continue;
if (!response.ok) {
if (response.status >= 400 && response.status < 500) continue;
throw new Error(`HTTP ${response.status} trying to load ${type === "oauth" ? "OAuth" : "OpenID provider"} metadata from ${endpointUrl}`);
}
if (type === "oauth") {
const metadata = OAuthMetadataSchema.parse(await response.json());
assertMetadataIssuerMatches(metadata, expectedIssuer);
return metadata;
} else {
const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
assertMetadataIssuerMatches(metadata, expectedIssuer);
if (!metadata.code_challenge_methods_supported?.includes("S256")) throw new Error(`Incompatible OIDC provider at ${endpointUrl}: does not support S256 code challenge method required by MCP specification`);
return metadata;
}
}
}
async function startAuthorization(authorizationServerUrl, { metadata, clientInformation, redirectUrl, scope, state, resource }) {
const responseType = "code";
const codeChallengeMethod = "S256";
let authorizationUrl;
if (metadata) {
authorizationUrl = new URL(metadata.authorization_endpoint);
if (!metadata.response_types_supported.includes(responseType)) throw new Error(`Incompatible auth server: does not support response type ${responseType}`);
if (!metadata.code_challenge_methods_supported || !metadata.code_challenge_methods_supported.includes(codeChallengeMethod)) throw new Error(`Incompatible auth server: does not support code challenge method ${codeChallengeMethod}`);
} else authorizationUrl = new URL("/authorize", authorizationServerUrl);
const challenge = await pkceChallenge();
const codeVerifier = challenge.code_verifier;
const codeChallenge = challenge.code_challenge;
authorizationUrl.searchParams.set("response_type", responseType);
authorizationUrl.searchParams.set("client_id", clientInformation.client_id);
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
authorizationUrl.searchParams.set("code_challenge_method", codeChallengeMethod);
authorizationUrl.searchParams.set("redirect_uri", String(redirectUrl));
if (state) authorizationUrl.searchParams.set("state", state);
if (scope) authorizationUrl.searchParams.set("scope", scope);
if (scope?.includes("offline_access")) authorizationUrl.searchParams.append("prompt", "consent");
if (resource) authorizationUrl.searchParams.set("resource", resourceUrlStripSlash(resource));
return {
authorizationUrl,
codeVerifier
};
}
/**
* 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 (supportedMethods.length === 0) return hasClientSecret ? "client_secret_post" : "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}`);
}
}
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);
}
/**
* 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 instance of the appropriate OAuthError subclass will be returned.
* If parsing fails, it falls back to a generic 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 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 { error, error_description, error_uri } = OAuthErrorResponseSchema.parse(await parseJSON({ text: body }));
return new (OAUTH_ERRORS[error] || ServerError)({
message: error_description || "",
cause: error_uri
});
} catch (error) {
return new ServerError({ message: `${statusCode ? `HTTP ${statusCode}: ` : ""}Invalid OAuth error response: ${error}. Raw body: ${body}` });
}
}
/**
* 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, codeVerifier, redirectUri, resource, addClientAuthentication, fetchFn, trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl) }) {
const grantType = "authorization_code";
const tokenUrl = metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl);
assertSafeOAuthEndpoint(tokenUrl, { allowLoopback: tokenUrl.origin === trustedOrigin });
if (metadata?.grant_types_supported && !metadata.grant_types_supported.includes(grantType)) throw new Error(`Incompatible auth server: does not support grant type ${grantType}`);
const headers = new Headers({
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
});
const params = new URLSearchParams({
grant_type: grantType,
code: authorizationCode,
code_verifier: codeVerifier,
redirect_uri: String(redirectUri)
});
if (addClientAuthentication) await addClientAuthentication(headers, params, authorizationServerUrl, metadata);
else applyClientAuthentication(selectClientAuthMethod(clientInformation, metadata?.token_endpoint_auth_methods_supported ?? []), clientInformation, headers, params);
if (resource) params.set("resource", resourceUrlStripSlash(resource));
const response = await fetchWithValidatedEndpoint({
url: tokenUrl,
init: {
method: "POST",
headers,
body: params
},
fetch: fetchFn,
trustedOrigin,
requireDnsValidation: true
});
if (!response.ok) throw await parseErrorResponse(response);
return OAuthTokensSchema.parse(await response.json());
}
/**
* 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, fetchFn, trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl) }) {
const grantType = "refresh_token";
let tokenUrl;
if (metadata) {
tokenUrl = new URL(metadata.token_endpoint);
if (metadata.grant_types_supported && !metadata.grant_types_supported.includes(grantType)) throw new Error(`Incompatible auth server: does not support grant type ${grantType}`);
} else tokenUrl = new URL("/token", authorizationServerUrl);
assertSafeOAuthEndpoint(tokenUrl, { allowLoopback: tokenUrl.origin === trustedOrigin });
const headers = new Headers({
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
});
const params = new URLSearchParams({
grant_type: grantType,
refresh_token: refreshToken
});
if (addClientAuthentication) await addClientAuthentication(headers, params, authorizationServerUrl, metadata);
else applyClientAuthentication(selectClientAuthMethod(clientInformation, metadata?.token_endpoint_auth_methods_supported ?? []), clientInformation, headers, params);
if (resource) params.set("resource", resourceUrlStripSlash(resource));
const response = await fetchWithValidatedEndpoint({
url: tokenUrl,
init: {
method: "POST",
headers,
body: params
},
fetch: fetchFn,
trustedOrigin,
requireDnsValidation: true
});
if (!response.ok) throw await parseErrorResponse(response);
return OAuthTokensSchema.parse({
refresh_token: refreshToken,
...await response.json()
});
}
/**
* Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591.
*/
async function registerClient(authorizationServerUrl, { metadata, clientMetadata, fetchFn, trustedOrigin = getTrustedLoopbackOrigin(authorizationServerUrl) }) {
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);
assertSafeOAuthEndpoint(registrationUrl, { allowLoopback: registrationUrl.origin === trustedOrigin });
const applicationType = clientMetadata.application_type ?? inferOAuthApplicationType(clientMetadata.redirect_uris);
const response = await fetchWithValidatedEndpoint({
url: registrationUrl,
init: {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...clientMetadata,
application_type: applicationType
})
},
fetch: fetchFn,
trustedOrigin,
requireDnsValidation: true
});
if (!response.ok) throw await parseErrorResponse(response);
return OAuthClientInformationFullSchema.parse(await response.json());
}
function inferOAuthApplicationType(redirectUris) {
const isNativeRedirectUri = (redirectUri) => {
const url = new URL(redirectUri);
return (url.protocol === "http:" || url.protocol === "https:") && isOAuthLoopbackHost(url.hostname) || url.protocol !== "http:" && url.protocol !== "https:";
};
return redirectUris.every(isNativeRedirectUri) ? "native" : "web";
}
async function auth(provider, options) {
const refreshAttempt = {};
try {
return await authInternal(provider, options, refreshAttempt);
} catch (error) {
if (error instanceof InvalidClientError || error instanceof UnauthorizedClientError) {
if (options.authorizationCode !== void 0 || !await provider.isClientInformationDynamicallyRegistered?.()) throw error;
await provider.invalidateCredentials?.("all");
return await authInternal(provider, options);
} else if (error instanceof InvalidGrantError && refreshAttempt.tokens) {
const retryAuthorizationServerInformation = getAuthorizationServerInformationFromCredentials(refreshAttempt.tokens);
await provider.invalidateCredentials?.("tokens", { tokens: refreshAttempt.tokens });
return await authInternal(provider, options, void 0, retryAuthorizationServerInformation);
}
throw error;
}
}
async function selectResourceURL(serverUrl, provider, resourceMetadata) {
const defaultResource = resourceUrlFromServerUrl(serverUrl);
if (provider.validateResourceURL) return await provider.validateResourceURL(defaultResource, resourceMetadata?.resource) ?? defaultResource;
if (!resourceMetadata) return defaultResource;
if (!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);
}
async function authInternal(provider, { serverUrl, authorizationCode, callbackState, callbackIssuer, scope, resourceMetadataUrl, fetchFn }, refreshAttempt, retryAuthorizationServerInformation) {
let resourceMetadata;
let authorizationServerUrl;
let clientInformation;
let callbackAuthorizationServerInformation;
/** Reject Protected Resource Metadata URLs outside the configured MCP server origin. */
assertResourceMetadataUrlSameOrigin(serverUrl, resourceMetadataUrl);
/** Discover PRM and select its advertised authorization server. */
try {
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl }, fetchFn);
if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) authorizationServerUrl = resourceMetadata.authorization_servers[0];
} catch {}
/**
* A callback may not have the original PRM URL from the authentication
* challenge. Use the authorization server pinned before redirecting when
* rediscovery does not select one.
*/
if (authorizationCode !== void 0) {
clientInformation = await Promise.resolve(provider.clientInformation());
if (clientInformation) callbackAuthorizationServerInformation = await getStoredAuthorizationServerInformation({
provider,
clientInformation
});
}
/** Reuse the callback pin, then fall back to the legacy MCP-as-AS behavior. */
if (!authorizationServerUrl) authorizationServerUrl = callbackAuthorizationServerInformation?.authorizationServerUrl ?? serverUrl;
const parsedServerUrl = new URL(serverUrl);
const parsedAuthorizationServerUrl = new URL(authorizationServerUrl);
const serverOrigin = parsedServerUrl.origin;
const authorizationServerOrigin = parsedAuthorizationServerUrl.origin;
const trustedAuthorizationServerOrigin = authorizationServerOrigin === serverOrigin || isOAuthLoopbackHost(parsedServerUrl.hostname) && isOAuthLoopbackHost(parsedAuthorizationServerUrl.hostname) ? authorizationServerOrigin : void 0;
if (!trustedAuthorizationServerOrigin) assertSafeOAuthEndpoint(new URL(authorizationServerUrl));
/** Validate and select the resource value sent to the AS */
const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
/** Let applications constrain discovered AS URLs before metadata fetches. */
await provider.validateAuthorizationServerURL?.(serverUrl, authorizationServerUrl);
/** Discover AS metadata and derive the credential pin for this flow */
const metadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, {
fetchFn,
trustedOrigin: trustedAuthorizationServerOrigin
});
const currentAuthorizationServerInformation = createAuthorizationServerInformation(authorizationServerUrl, metadata);
if (retryAuthorizationServerInformation) assertAuthorizationServerInformationMatches({
storedAuthorizationServerInformation: retryAuthorizationServerInformation,
currentAuthorizationServerInformation
});
const clientMetadata = provider.clientMetadata;
const selectedScope = selectScope({
scope,
resourceMetadata,
clientMetadata
});
/** Load or register client credentials with the AS pin attached. */
if (authorizationCode === void 0) clientInformation = await Promise.resolve(provider.clientInformation());
const tokens = authorizationCode === void 0 ? await provider.tokens() : void 0;
if (clientInformation) {
if (clientInformation.issuer != null && clientInformation.issuer !== currentAuthorizationServerInformation.issuer) throw new AuthorizationServerMismatchError({ message: "OAuth authorization server does not match the issuer of the client credentials" });
const clientAuthorizationServerInformation = getAuthorizationServerInformationFromCredentials(clientInformation);
if (clientAuthorizationServerInformation) assertAuthorizationServerInformationMatches({
storedAuthorizationServerInformation: clientAuthorizationServerInformation,
currentAuthorizationServerInformation
});
const storedAuthorizationServerInformation = callbackAuthorizationServerInformation ?? await getStoredAuthorizationServerInformation({
provider,
clientInformation,
tokens
}) ?? retryAuthorizationServerInformation;
if (storedAuthorizationServerInformation) assertAuthorizationServerInformationMatches({
storedAuthorizationServerInformation,
currentAuthorizationServerInformation
});
if ((clientInformation.client_secret != null || provider.addClientAuthentication) && clientInformation.issuer == null && !storedAuthorizationServerInformation) throw new AuthorizationServerMismatchError({ message: "OAuth client credentials must be bound to an authorization server: configure their issuer or store authorization server information" });
}
const refreshAuthorizationServerInformation = tokens?.refresh_token ? await getStoredAuthorizationServerInformation({
provider,
clientInformation,
tokens
}) : void 0;
if (tokens?.refresh_token) {
if (refreshAuthorizationServerInformation) assertAuthorizationServerInformationMatches({
storedAuthorizationServerInformation: refreshAuthorizationServerInformation,
currentAuthorizationServerInformation
});
else {
await provider.invalidateCredentials?.("tokens", { tokens: { ...tokens } });
if ((await provider.tokens())?.refresh_token) throw new MCPClientOAuthError({ message: "Stored OAuth tokens without an authorization server binding must be cleared before starting authorization" });
}
}
if (!clientInformation) {
if (authorizationCode !== void 0) throw new Error("Existing OAuth client information is required when exchanging an authorization code");
if (!provider.saveClientInformation) throw new Error("OAuth client information must be saveable for dynamic registration");
clientInformation = addAuthorizationServerInformationToClientInformation(await registerClient(authorizationServerUrl, {
metadata,
clientMetadata: {
...clientMetadata,
scope: selectedScope
},
fetchFn,
trustedOrigin: trustedAuthorizationServerOrigin
}), currentAuthorizationServerInformation);
await provider.saveClientInformation(clientInformation);
}
/** On callback, validate state and AS pin before code exchange */
if (authorizationCode !== void 0) {
if (provider.storedState) {
const expectedState = await provider.storedState();
if (expectedState !== void 0 && expectedState !== callbackState) throw new Error("OAuth state parameter mismatch - possible CSRF attack");
}
const storedAuthorizationServerInformation = callbackAuthorizationServerInformation ?? await getStoredAuthorizationServerInformation({
provider,
clientInformation
});
if (!storedAuthorizationServerInformation) throw new MCPClientOAuthError({ message: "Stored OAuth authorization server metadata is required when exchanging an authorization code" });
validateAuthorizationResponseIssuer({
callbackIssuer,
expectedIssuer: storedAuthorizationServerInformation.issuer ?? metadata?.issuer ?? String(authorizationServerUrl)
});
assertAuthorizationServerInformationMatches({
storedAuthorizationServerInformation,
currentAuthorizationServerInformation
});
const codeVerifier = await provider.codeVerifier();
const tokens = await exchangeAuthorization(authorizationServerUrl, {
metadata,
clientInformation,
authorizationCode,
codeVerifier,
redirectUri: provider.redirectUrl,
resource,
addClientAuthentication: provider.addClientAuthentication,
fetchFn,
trustedOrigin: trustedAuthorizationServerOrigin
});
await provider.saveTokens(addAuthorizationServerInformationToTokens(tokens, currentAuthorizationServerInformation));
return "AUTHORIZED";
}
/** Refresh only when stored credentials match the current AS pin */
if (tokens?.refresh_token && refreshAuthorizationServerInformation) try {
if (refreshAttempt) refreshAttempt.tokens = { ...tokens };
const newTokens = await refreshAuthorization(authorizationServerUrl, {
metadata,
clientInformation,
refreshToken: tokens.refresh_token,
resource,
addClientAuthentication: provider.addClientAuthentication,
fetchFn,
trustedOrigin: trustedAuthorizationServerOrigin
});
await provider.saveTokens(addAuthorizationServerInformationToTokens(newTokens, currentAuthorizationServerInformation));
return "AUTHORIZED";
} catch (error) {
if (!(error instanceof MCPClientOAuthError) || error instanceof ServerError) {} else throw error;
}
/** Start authorization and persist the AS pin before redirecting */
const state = provider.state ? await provider.state() : void 0;
if (state && provider.saveState) await provider.saveState(state);
const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, {
metadata,
clientInformation,
state,
redirectUrl: provider.redirectUrl,
scope: selectedScope,
resource
});
if (!await saveAuthorizationServerInformation({
provider,
clientInformation,
authorizationServerInformation: currentAuthorizationServerInformation
})) throw new MCPClientOAuthError({ message: "OAuth authorization server metadata must be saveable before starting authorization" });
await provider.saveCodeVerifier(codeVerifier);
await provider.redirectToAuthorization(authorizationUrl);
return "REDIRECT";
}
//#endregion
//#region src/tool/mcp-sse-transport.ts
function isMessageEvent$1(event) {
return event === void 0 || event === "message";
}
var SseMCPTransport = class {
constructor({ url, headers, authProvider, redirect = "error", fetch: fetchFn }) {
this.connected = false;
this.url = new URL(url);
this.headers = headers;
this.authProvider = authProvider;
this.redirectMode = redirect;
this.fetchFn = fetchFn ?? globalThis.fetch.bind(globalThis);
this.customFetch = fetchFn;
}
setProtocolVersion(version) {
this.protocolVersion = version;
}
async commonHeaders(base) {
const headers = {
...this.headers,
...base,
"mcp-protocol-version": this.protocolVersion ?? "2025-11-25"
};
if (this.authProvider) {
const tokens = await this.authProvider.tokens();
if (tokens?.access_token) headers["Authorization"] = `Bearer ${tokens.access_token}`;
}
return withUserAgentSuffix(headers, `ai-sdk/${VERSION}`, getRuntimeEnvironmentUserAgent());
}
/**
* Runs a single OAuth recovery flow for concurrent 401 responses.
*/
authorizeOnce(resourceMetadataUrl, scope) {
if (!this.authProvider) return Promise.resolve("REDIRECT");
if (!this.authPromise) this.authPromise = auth(this.authProvider, {
serverUrl: this.url,
resourceMetadataUrl,
scope,
fetchFn: this.customFetch
}).finally(() => {
this.authPromise = void 0;
});
return this.authPromise;
}
async accessTokenChanged(requestAuthorization) {
const accessToken = (await this.authProvider?.tokens())?.access_token;
return accessToken != null && requestAuthorization !== `Bearer ${accessToken}`;
}
async start() {
return new Promise((resolve, reject) => {
if (this.connected) return resolve();
this.abortController = new AbortController();
const establishConnection = async (triedAuth = false) => {
try {
const headers = await this.commonHeaders({ Accept: "text/event-stream" });
const response = await this.fetchFn(this.url.href, {
headers,
signal: this.abortController?.signal,
redirect: this.redirectMode
});
if (response.status === 401 && this.authProvider && !triedAuth) {
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
this.resourceMetadataUrl = resourceMetadataUrl;
try {
if (await this.authorizeOnce(this.resourceMetadataUrl, scope) !== "AUTHORIZED") {
const error = new UnauthorizedError();
this.onerror?.(error);
return reject(error);
}
} catch (error) {
this.onerror?.(error);
return reject(error);
}
return establishConnection(true);
}
if (!response.ok || !response.body) {
const text = await response.text().catch(() => null);
let errorMessage = `MCP SSE Transport Error: ${response.status} ${response.statusText}${text ? `: ${text}` : ""}`;
if (response.status === 405) errorMessage += ". This server does not support SSE transport. Try using `http` transport instead";
const error = new MCPClientError({
message: errorMessage,
statusCode: response.status,
url: this.url.href,
responseBody: text ?? void 0
});
this.onerror?.(error);
return reject(error);
}
const reader = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream()).getReader();
const processEvents = async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (this.abortController?.signal.aborted) return;
if (this.connected) {
this.connected = false;
throw new MCPClientError({ message: "MCP SSE Transport Error: Connection closed unexpectedly" });
}
throw new MCPClientError({ message: "MCP SSE Transport Error: Connection closed before endpoint event was received" });
}
const { event, data } = value;
if (event === "endpoint") {
if (this.endpoint) continue;
const endpoint = new URL(data, this.url);
if (endpoint.origin !== this.url.origin) {
this.connected = false;
this.endpoint = void 0;
this.sseConnection?.close();
this.abortController?.abort();
throw new MCPClientError({ message: `MCP SSE Transport Error: Endpoint origin does not match connection origin: ${endpoint.origin}` });
}
this.endpoint = endpoint;
this.connected = true;
resolve();
} else if (isMessageEvent$1(event)) try {
const message = await parseJSONRPCMessage(data);
this.onmessage?.(message);
} catch (error) {
const e = new MCPClientError({
message: "MCP SSE Transport Error: Failed to parse message",
cause: error
});
this.onerror?.(e);
}
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") return;
this.onerror?.(error);
reject(error);
}
};
this.sseConnection = { close: () => reader.cancel() };
processEvents();
} catch (error) {
if (error instanceof Error && error.name === "AbortError") return;
this.onerror?.(error);
reject(error);
}
};
establishConnection();
});
}
async close() {
this.connected = false;
this.endpoint = void 0;
this.sseConnection?.close();
this.abortController?.abort();
this.onclose?.();
}
async send(message, options) {
options?.signal?.throwIfAborted();
if (!this.endpoint || !this.connected) throw new MCPClientError({ message: "MCP SSE Transport Error: Not connected" });
const endpoint = this.endpoint;
const transportSignal = this.abortController?.signal;
const requestSignal = options?.signal == null ? transportSignal : transportSignal == null ? options.signal : AbortSignal.any([transportSignal, options.signal]);
const attempt = async (triedAuth = false) => {
try {
const headers = await this.commonHeaders({ "Content-Type": "application/json" });
const init = {
method: "POST",
headers,
body: JSON.stringify(message),
signal: requestSignal,
redirect: this.redirectMode
};
const response = await this.fetchFn(endpoint.href, init);
if (response.status === 401 && this.authProvider && !triedAuth) {
if (await this.accessTokenChanged(new Headers(headers).get("authorization") ?? void 0)) return attempt(true);
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
this.resourceMetadataUrl = resourceMetadataUrl;
if (await this.authorizeOnce(this.resourceMetadataUrl, scope) !== "AUTHORIZED") throw new UnauthorizedError();
return attempt(true);
}
if (!response.ok) {
const text = await response.text().catch(() => null);
throw new MCPClientError({
message: `MCP SSE Transport Error: POSTing to endpoint (HTTP ${response.status}): ${text}`,
statusCode: response.status,
url: endpoint.href,
responseBody: text ?? void 0
});
}
} catch (error) {
if (options?.signal?.aborted) throw error;
this.onerror?.(error);
throw error;
}
};
await attempt();
}
};
//#endregion
//#region src/tool/mcp-http-headers.ts
const HTTP_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
const BASE64_SENTINEL_PATTERN = /^=\?base64\?.*\?=$/;
function encodeMCPHeaderValue(value) {
if ([...value].every((character) => {
const code = character.charCodeAt(0);
return code === 9 || code >= 32 && code <= 126;
}) && value.trim() === value && !BASE64_SENTINEL_PATTERN.test(value)) return value;
return `=?base64?${convertUint8ArrayToBase64(new TextEncoder().encode(value))}?=`;
}
function getMCPToolHeaderBindings(inputSchema) {
if (!isRecord(inputSchema)) return {
success: false,
error: "inputSchema must be a JSON Schema object"
};
const bindings = [];
const headerNames = /* @__PURE__ */ new Set();
let error;
const visit = (value, path, staticallyReachable) => {
if (error != null || !isRecord(value)) return;
if ("x-mcp-header" in value) {
if (!staticallyReachable || path.length === 0) {
error = "x-mcp-header is not on a statically reachable property";
return;
}
const headerName = value["x-mcp-header"];
if (typeof headerName !== "string" || !HTTP_TOKEN_PATTERN.test(headerName)) {
error = "x-mcp-header must be a non-empty HTTP token";
return;
}
const normalizedHeaderName = headerName.toLowerCase();
if (headerNames.has(normalizedHeaderName)) {
error = `x-mcp-header value "${headerName}" is not unique`;
return;
}
const valueType = value.type;
if (valueType !== "boolean" && valueType !== "integer" && valueType !== "string") {
error = "x-mcp-header can only annotate boolean, integer, or string properties";
return;
}
headerNames.add(normalizedHeaderName);
bindings.push({
headerName,
path,
valueType
});
}
for (const [key, child] of Object.entries(value)) {
if (key === "x-mcp-header") continue;
if (key === "properties" && isRecord(child)) for (const [propertyName, propertySchema] of Object.entries(child)) visit(propertySchema, [...path, propertyName], staticallyReachable);
else visit(child, path, false);
}
};
visit(inputSchema, [], true);
return error == null ? {
success: true,
bindings
} : {
success: false,
error
};
}
function getValueAtPath(value, path) {
let current = value;
for (const segment of path) {
if (!isRecord(current)) return;
current = current[segment];
}
return current;
}
function createMCPToolHeaders({ bindings, args }) {
const headers = {};
for (const binding of bindings) {
const value = getValueAtPath(args, binding.path);
if (value == null) continue;
if (binding.valueType === "string" && typeof value !== "string" || binding.valueType === "boolean" && typeof value !== "boolean" || binding.valueType === "integer" && !Number.isSafeInteger(value)) throw new TypeError(`Tool argument "${binding.path.join(".")}" does not match its x-mcp-header type`);
headers[`Mcp-Param-${binding.headerName}`] = encodeMCPHeaderValue(String(value));
}
return headers;
}
//#endregion
//#region src/tool/mcp-http-transport.ts
function isMessageEvent(event) {
return event === void 0 || event === "message";
}
/**
* HTTP MCP transport implementing the Streamable HTTP style.
*
* 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 HttpMCPTransport = class {
constructor({ url, headers, authProvider, redirect = "error", initialSessionId, initialProtocolVersion, onSessionIdChange, onSessionExpired, terminateSessionOnClose = true, fetch: fetchFn }) {
this.supportsProtocolVersionDiscovery = true;
this.waitsForProtocolVersionDiscovery = true;
this.supportsMcpToolParameterHeaders = true;
this.inboundReconnectAttempts = 0;
this.reconnectionOptions = {
initialReconnectionDelay: 1e3,
maxReconnectionDelay: 3e4,
reconnectionDelayGrowFactor: 1.5,
maxRetries: 2
};
this.url = new URL(url);
this.headers = headers;
this.authProvider = authProvider;
this.redirectMode = redirect;
this.sessionId = initialSessionId;
this.protocolVersion = initialProtocolVersion ?? "2025-11-25";
this.onSessionIdChange = onSessionIdChange;
this.onSessionExpired = onSessionExpired;
this.terminateSessionOnClose = terminateSessionOnClose;
this.fetchFn = fetchFn ?? globalThis.fetch.bind(globalThis);
this.customFetch = fetchFn;
}
setProtocolVersion(version) {
this.protocolVersion = version;
if (!this.abortController) return;
if (this.isModernProtocol()) {
this.inboundSseConnection?.close();
this.inboundSseConnection = void 0;
return;
}
if (!this.inboundSseConnection) this.startInboundSse();
}
isModernProtocol() {
return (this.protocolVersion ?? "2026-07-28") === LATEST_PROTOCOL_VERSION;
}
async commonHeaders({ base, includeSessionId = true }) {
const headers = {
...this.headers,
...base,
"mcp-protocol-version": this.protocolVersion ?? "2025-11-25"
};
if (!this.isModernProtocol() && includeSessionId && this.sessionId) headers["mcp-session-id"] = this.sessionId;
if (this.authProvider) {
const tokens = await this.authProvider.tokens();
if (tokens?.access_token) headers["Authorization"] = `Bearer ${tokens.access_token}`;
}
return withUserAgentSuffix(headers, `ai-sdk/${VERSION}`, getRuntimeEnvironmentUserAgent());
}
setSessionId(sessionId) {
if (this.sessionId === sessionId) return;
this.sessionId = sessionId;
this.onSessionIdChange?.(sessionId);
}
applySessionIdFromResponse(response) {
if (this.isModernProtocol()) return;
const sessionId = response.headers.get("mcp-session-id");
if (sessionId) this.setSessionId(sessionId);
}
expireSessionId(sessionId) {
if (this.sessionId === sessionId) this.setSessionId(void 0);
this.onSessionExpired?.(sessionId);
}
/**
* Runs a single OAuth recovery flow for concurrent 401 responses.
*/
authorizeOnce(resourceMetadataUrl, scope) {
if (!this.authProvider) return Promise.resolve("REDIRECT");
if (!this.authPromise) this.authPromise = auth(this.authProvider, {
serverUrl: this.url,
resourceMetadataUrl,
scope,
fetchFn: this.customFetch
}).finally(() => {
this.authPromise = void 0;
});
return this.authPromise;
}
async start() {
if (this.abortController) throw new MCPClientError({ message: "MCP HTTP Transport Error: Transport already started. Note: client.connect() calls start() automatically." });
this.abortController = new AbortController();
if (this.protocolVersion != null && this.protocolVersion !== "2026-07-28") this.startInboundSse();
}
async close(options) {
this.inboundSseConnection?.close();
this.abortController?.abort();
try {
if (!this.isModernProtocol() && this.sessionId && this.terminateSessionOnClose && this.abortController) {
options?.signal?.throwIfAborted();
const headers = await this.commonHeaders({ base: {} });
options?.signal?.throwIfAborted();
await this.fetchFn(this.url.href, {
method: "DELETE",
headers,
signal: options?.signal,
redirect: this.redirectMode
}).catch(() => void 0);
}
} catch {}
this.onclose?.();
}
async send(message, options) {
options?.signal?.throwIfAborted();
const transportSignal = this.abortController?.signal;
const requestSignal = options?.signal == null ? transportSignal : transportSignal == null ? options.signal : AbortSignal.any([transportSignal, options.signal]);
const attempt = async (triedAuth = false) => {
try {
const isInitializeRequest = "method" in message && message.method === "initialize";
const sessionIdForRequest = isInitializeRequest ? void 0 : this.sessionId;
const init = {
method: "POST",
headers: await this.commonHeaders({
base: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
...this.isModernProtocol() ? options?.headers : {},
...this.isModernProtocol() && "method" in message && "id" in message ? this.getStandardRequestHeaders(message) : {}
},
includeSessionId: !isInitializeRequest
}),
body: JSON.stringify(message),
signal: requestSignal,
redirect: this.redirectMode
};
const response = await this.fetchFn(this.url.href, init);
this.applySessionIdFromResponse(response);
if (response.status === 401 && this.authProvider && !triedAuth) {
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
this.resourceMetadataUrl = resourceMetadataUrl;
try {
if (await this.authorizeOnce(this.resourceMetadataUrl, scope) !== "AUTHORIZED") throw new UnauthorizedError();
} catch (error) {
this.onerror?.(error);
throw error;
}
return attempt(true);
}
if (response.status === 202) {
if (!this.isModernProtocol() && !this.inboundSseConnection) this.startInboundSse();
return;
}
if (!response.ok) {
const text = await response.text().catch(() => null);
if ("id" in message && text != null) {
const jsonRpcMessage = await parseJSONRPCMessage(text).catch(() => void 0);
if (jsonRpcMessage != null && "error" in jsonRpcMessage) {
this.onmessage?.(jsonRpcMessage.id == null ? {
...jsonRpcMessage,
id: message.id
} : jsonRpcMessage);
return;
}
}
let errorMessage = `MCP HTTP Transport Error: POSTing to endpoint (HTTP ${response.status}): ${text}`;
if (response.status === 404) {
if (!this.isModernProtocol() && sessionIdForRequest) {
this.expireSessionId(sessionIdForRequest);
errorMessage += ". The MCP session expired. Create a new client without `initialSessionId` to start a fresh session";
} else if (!this.isModernProtocol()) errorMessage += ". This server does not support HTTP transport. Try using `sse` transport instead";
}
const error = new MCPClientError({
message: errorMessage,
statusCode: response.status,
url: this.url.href,
responseBody: text ?? void 0
});
this.onerror?.(error);
throw error;
}
if (!("id" in message)) return;
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
const data = await response.json();
const messages = Array.isArray(data) ? data.map((message) => validateJSONRPCMessage(message)) : [validateJSONRPCMessage(data)];
for (const jsonRpcMessage of messages) this.onmessage?.(jsonRpcMessage);
return;
}
if (contentType.includes("text/event-stream")) {
if (!response.body) {
const error = new MCPClientError({
message: "MCP HTTP Transport Error: text/event-stream response without body",
statusCode: response.status,
url: this.url.href
});
this.onerror?.(error);
throw error;
}
const reader = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream()).getReader();
const processEvents = async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) return;
const { event, data } = value;
if (isMessageEvent(event)) try {
const jsonRpcMessage = await parseJSONRPCMessage(data);
this.onmessage?.(jsonRpcMessage);
} catch (error) {
const e = new MCPClientError({
message: "MCP HTTP Transport Error: Failed to parse message",
cause: error
});
this.onerror?.(e);
}
}
} catch (error) {
if (options?.signal?.aborted || error instanceof Error && error.name === "AbortError") return;
this.onerror?.(error);
}
};
processEvents().catch((error) => {
if (options?.signal?.aborted || error instanceof Error && error.name === "AbortError") return;
this.onerror?.(error);
});
return;
}
const error = new MCPClientError({
message: `MCP HTTP Transport Error: Unexpected content type: ${contentType}`,
statusCode: response.status,
url: this.url.href
});
this.onerror?.(error);
throw error;
} catch (error) {
if (options?.signal?.aborted) throw error;
this.onerror?.(error);
throw error;
}
};
await attempt();
}
getStandardRequestHeaders(message) {
const headers = { "Mcp-Method": message.method };
const params = message.params;
const name = message.method === "resources/read" ? params?.uri : message.method === "tools/call" || message.method === "prompts/get" ? params?.name : void 0;
if (typeof name === "string") headers["Mcp-Name"] = encodeMCPHeaderValue(name);
return headers;
}
getNextReconnectionDelay(attempt) {
const { initialReconnectionDelay, reconnectionDelayGrowFactor, maxReconnectionDelay } = this.reconnectionOptions;
return Math.min(initialReconnectionDelay * Math.pow(reconnectionDelayGrowFactor, attempt), maxReconnectionDelay);
}
scheduleInboundSseReconnection() {
const { maxRetries } = this.reconnectionOptions;
if (maxRetries > 0 && this.inboundReconnectAttempts >= maxRetries) {
this.onerror?.(new MCPClientError({ message: `MCP HTTP Transport Error: Maximum reconnection attempts (${maxRetries}) exceeded.` }));
return;
}
const delay = this.getNextReconnectionDelay(this.inboundReconnectAttempts);
this.inboundReconnectAttempts += 1;
setTimeout(() => {
if (this.abortController?.signal.aborted) return;
this.startInboundSse(false, this.lastInboundEventId);
}, delay);
}
startInboundSse(triedAuth = false, resumeToken) {
if (this.isModernProtocol()) return;
this.openInboundSse(triedAuth, resumeToken).catch((error) => {
if (error instanceof Error && error.name === "AbortError") return;
this.onerror?.(error);
});
}
async openInboundSse(triedAuth = false, resumeToken) {
if (this.isModernProtocol()) return;
try {
const sessionIdForRequest = this.sessionId;
const headers = await this.commonHeaders({ base: { Accept: "text/event-stream" } });
if (resumeToken) headers["last-event-id"] = resumeToken;
const response = await this.fetchFn(this.url.href, {
method: "GET",
headers,
signal: this.abortController?.signal,
redirect: this.redirectMode
});
this.applySessionIdFromResponse(response);
if (response.status === 401 && this.authProvider && !triedAuth) {
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
this.resourceMetadataUrl = resourceMetadataUrl;
try {
if (await this.authorizeOnce(this.resourceMetadataUrl, scope) !== "AUTHORIZED") {
const error = new UnauthorizedError();
this.onerror?.(error);
return;
}
} catch (error) {
this.onerror?.(error);
return;
}
return this.openInboundSse(true, resumeToken);
}
if (response.status === 405) return;
if (!response.ok || !response.body) {
if (response.status === 404 && sessionIdForRequest) this.expireSessionId(sessionIdForRequest);
const error = new MCPClientError({
message: `MCP HTTP Transport Error: GET SSE failed: ${response.status} ${response.statusText}`,
statusCode: response.status,
url: this.url.href
});
this.onerror?.(error);
return;
}
const reader = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream()).getReader();
const processEvents = async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) return;
const { event, data, id } = value;
if (id) this.lastInboundEventId = id;
if (isMessageEvent(event)) try {
const jsonRpcMessage = await parseJSONRPCMessage(data);
this.onmessage?.(jsonRpcMessage);
} catch (error) {
const e = new MCPClientError({
message: "MCP HTTP Transport Error: Failed to parse message",
cause: error
});
this.onerror?.(e);
}
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") return;
this.onerror?.(error);
if (!this.abortController?.signal.aborted) this.scheduleInboundSseReconnection();
}
};
this.inboundSseConnection = { close: () => {
reader.cancel().catch((error) => {
if (error instanceof Error && error.name === "AbortError") return;
this.onerror?.(error);
});
} };
this.inboundReconnectAttempts = 0;
processEvents().catch((error) => {
if (error instanceof Error && error.name === "AbortError") return;
this.onerror?.(error);
if (!this.abortController?.signal.aborted) this.scheduleInboundSseReconnection();
});
} catch (error) {
if (error instanceof Error && error.name === "AbortError") return;
this.onerror?.(error);
if (!this.abortController?.signal.aborted) this.scheduleInboundSseReconnection();
}
}
};
//#endregion
//#region src/tool/mcp-transport.ts
function createMcpTransport(config) {
switch (config.type) {
case "sse": return new SseMCPTransport(config);
case "http": return new HttpMCPTransport({
...config,
initialProtocolVersion: config.initialProtocolVersion ?? "2026-07-28"
});
default: throw new MCPClientError({ message: "Unsupported or invalid transport configuration. If you are using a custom transport, make sure it implements the MCPTransport interface." });
}
}
function isCustomMcpTransport(transport) {
return "start" in transport && typeof transport.start === "function" && "send" in transport && typeof transport.send === "function" && "close" in transport && typeof transport.close === "function";
}
//#endregion
//#region src/tool/mcp-apps.ts
/**
* MCP capability extension name used by hosts that can render MCP Apps.
*/
const MCP_APP_EXTENSION_NAME = "io.modelcontextprotocol/ui";
/**
* MIME type for HTML resources that are meant to be rendered as MCP Apps.
*/
const MCP_APP_MIME_TYPE = "text/html;profile=mcp-app";
/**
* Client capabilities to pass to `createMCPClient` when the host supports MCP Apps.
*/
const mcpAppClientCapabilities = { extensions: { [MCP_APP_EXTENSION_NAME]: { mimeTypes: [MCP_APP_MIME_TYPE] } } };
function getToolUiMeta(meta) {
const uiMeta = meta?.ui;
return isJSONObject(uiMeta) ? uiMeta : void 0;
}
/**
* Filters a string array to its string elements, dropping the field entirely
* (rather than failing the parse) if the value is not an array.
*/
const optionalStringArray = z4.array(z4.unknown()).transform((items) => items.filter((v) => typeof v === "string")).optional().catch(void 0);
const MCPAppResourceCSPSchema = z4.looseObject({
connectDomains: optionalStringArray,
resourceDomains: optionalStringArray,
frameDomains: optionalStringArray,
baseUriDomains: optionalStringArray
});
/**
* Runtime schema for `_meta.ui` on an MCP App resource. Each known field is
* validated; a malformed field is dropped (`.catch(undefined)`) rather than
* failing the whole parse, and unknown keys pass through for forward-compat.
*/
const MCPAppResourceMetaSchema = z4.looseObject({
prefersBorder: z4.boolean().optional().catch(void 0),
csp: MCPAppResourceCSPSchema.optional().catch(void 0),
permissions: z4.record(z4.string(), z4.unknown()).optional().catch(void 0)
});
function getResourceUiMeta(meta) {
const rawUiMeta = (isJSONObject(meta) ? meta : void 0)?.ui;
if (!isJSONObject(rawUiMeta)) return;
const parsed = MCPAppResourceMetaSchema.safeParse(rawUiMeta);
return parsed.success ? parsed.data : void 0;
}
function parseVisibility(value) {
return Array.isArray(value) ? value.filter((v) => v === "model" || v === "app") : void 0;
}
/**
* Reads and validates MCP Apps metadata from a tool definition.
*/
function getMCPAppToolMeta(tool) {
const uiMeta = getToolUiMeta(tool._meta);
const resourceUri = uiMeta?.resourceUri ?? tool._meta?.["ui/resourceUri"];
const visibility = parseVisibility(uiMeta?.visibility);
if (resourceUri !== void 0) {
if (typeof resourceUri !== "string" || !resourceUri.startsWith("ui://")) throw new Error(`Invalid MCP App resource URI: ${JSON.stringify(resourceUri)}`);
} else if (uiMeta == null) return;
return {
...uiMeta,
...resourceUri != null ? { resourceUri } : {},
...visibility != null ? { visibility } : {}
};
}
/**
* Splits tool definitions into model-visible tools and app-visible tools.
*/
function splitMCPAppTools(definitions) {
const modelVisibleTools = [];
const appVisibleTools = [];
for (const tool of definitions.tools) {
const visibility = getMCPAppToolMeta(tool)?.visibility;
if (visibility == null || visibility.includes("model")) modelVisibleTools.push(tool);
if (visibility?.includes("app") === true) appVisibleTools.push(tool);
}
return {
modelVisible: {
...definitions,
tools: modelVisibleTools
},
appVisible: {
...definitions,
tools: appVisibleTools
}
};
}
/**
* Extracts app HTML and rendering metadata from a `resources/read` result.
*/
function getMCPAppResourceFromReadResult({ uri, resource }) {
const content = resource.contents.find((content) => content.uri === uri);
if (content == null) throw new Error(`MCP App resource not found in read result: ${uri}`);
if (content.mimeType !== "text/html;profile=mcp-app") throw new Error(`Unsupported MCP App resource MIME type: ${content.mimeType}`);
const html = "text" in content && typeof content.text === "string" ? content.text : "blob" in content && typeof content.blob === "string" ? new TextDecoder().decode(convertBase64ToUint8Array(content.blob)) : void 0;
if (html == null) throw new Error(`Unsupported MCP App resource content format: ${uri}`);
const meta = getResourceUiMeta(content._meta);
return {
uri,
mimeType: MCP_APP_MIME_TYPE,
html,
meta
};
}
/**
* Reads a `ui://` resource from an MCP server and normalizes it for rendering.
*/
async function readMCPAppResource({ client, uri, options }) {
if (!uri.startsWith("ui://")) throw new Error(`Unsupported MCP App resource URI: ${uri}`);
return getMCPAppResourceFromReadResult({
uri,
resource: await client.readResource({
uri,
options
})
});
}
//#endregion
//#region src/tool/mcp-client.ts
const CLIENT_VERSION = "1.0.0";
const DEFAULT_MAX_TOOL_CALL_RETRIES = 0;
const DEFAULT_PROTOCOL_DISCOVERY_TIMEOUT = 1e3;
const MAX_INPUT_REQUIRED_ROUNDS = 10;
const INPUT_REQUIRED_RETRY_DELAY = 250;
const MODERN_PROTOCOL_ERROR_CODES = [
-32020,
-32021,
-32022
];
const DEFAULT_RETRY_ERROR_CODES = [
"ConnectionRefused",
"ConnectionClosed",
"FailedToOpenSocket",
"ECONNRESET",
"ECONNREFUSED",
"ETIMEDOUT",
"EPIPE"
];
function getErrorStatusCode(error) {
if (error != null && typeof error === "object" && "statusCode" in error && typeof error.statusCode === "number") return error.statusCode;
}
function getStringErrorCode(error) {
if (error != null && typeof error === "object" && "code" in error && typeof error.code === "string") return error.code;
}
function isRetryableMCPToolCallError(error) {
const statusCode = getErrorStatusCode(error);
if (statusCode != null) return statusCode === 408 || statusCode === 409 || statusCode === 429 || statusCode >= 500;
if (MCPClientError.isInstance(error) && error.code != null) return false;
const errorCode = getStringErrorCode(error);
return errorCode != null && DEFAULT_RETRY_ERROR_CODES.includes(errorCode);
}
function prepareMaxRetries(maxRetries) {
if (maxRetries == null) return DEFAULT_MAX_TOOL_CALL_RETRIES;
if (!Number.isInteger(maxRetries)) throw new MCPClientError({ message: "maxRetries must be an integer" });
if (maxRetries < 0) throw new MCPClientError({ message: "maxRetries must be >= 0" });
return maxRetries;
}
function getEffectiveTimeout({ timeout, maxTotalTimeout }) {
if (timeout == null) return maxTotalTimeout;
if (maxTotalTimeout == null) return timeout;
return Math.min(timeout, maxTotalTimeout);
}
function waitForAbort(promise, signal) {
if (signal == null) return promise;
return new Promise((resolve, reject) => {
const cleanup = () => {
signal.removeEventListener("abort", onAbort);
};
const onAbort = () => {
cleanup();
reject(signal.reason);
};
if (signal.aborted) {
onAbort();
return;
}
signal.addEventListener("abort", onAbort, { once: true });
promise.then((value) => {
cleanup();
resolve(value);
}).catch((error) => {
cleanup();
reject(error);
});
});
}
function mcpToModelOutput({ output }) {
if (typeof output === "string") return {
type: "text",
value: output
};
if (output == null || typeof output !== "object" || !("content" in output) || !Array.isArray(output.content)) return {
type: "json",
value: output ?? null
};
return {
type: "content",
value: output.content.map((part) => {
if (part.type === "text" && "text" in part) return {
type: "text",
text: part.text
};
if (part.type === "image" && "data" in part && "mimeType" in part) return {
type: "file",
mediaType: part.mimeType,
data: {
type: "data",
data: part.data
}
};
return {
type: "text",
text: JSON.stringify(part)
};
})
};
}
async function createMCPClient(config) {
const client = new DefaultMCPClient(config);
await client.init();
return client;
}
/**
* A lightweight MCP Client implementation
*
* The primary purpose of this client is tool conversion between MCP<>AI SDK
* but can later be extended to support other MCP features
*
* Tool parameters are automatically inferred from the server's JSON schema
* if not explicitly provided in the tools configuration
*
* This client is meant to be used to communicate with a single server. To communicate and fetch tools across multiple servers, it's recommended to create a new client instance per server.
*
* Not supported:
* - Accepting in-band notifications (webhook events use a separate HTTP handler)
* - Automatic session persistence for Streamable HTTP transport
* - Resumable SSE streams
*/
var DefaultMCPClient = class {
constructor({ experimental_events: events, transport: transportConfig, name, clientName = name ?? "ai-sdk-mcp-client", version = CLIENT_VERSION, onUncaughtError, maxRetries, capabilities, initialInitializeResult, initializationOptions, protocolVersionDiscovery = true }) {
this.requestMessageId = 0;
this.responseHandlers = /* @__PURE__ */ new Map();
this.serverCapabilities = {};
this._serverInfo = {
name: "",
version: ""
};
this._initializeResult = {
protocolVersion: LATEST_LEGACY_PROTOCOL_VERSION,
capabilities: {},
serverInfo: this._serverInfo
};
this.protocolEra = "legacy";
this.protocolVersion = LATEST_LEGACY_PROTOCOL_VERSION;
this.toolHeaderBindings = /* @__PURE__ */ new Map();
this.isClosed = true;
this.closeController = new AbortController();
if (events?.adapter !== void 0 && (events.store !== void 0 || events.validateArguments !== void 0)) throw new MCPClientError({ message: "Configure experimental_events with either an adapter or a store, not both. Managed adapters own argument validation." });
if (events?.adapter !== void 0 && typeof events.adapter?.createAdapter !== "function") throw new MCPClientError({ message: "experimental_events.adapter must implement createAdapter. Wrap bound operations with { createAdapter: () => operations }." });
const operations = events?.adapter?.createAdapter({ transport: isCustomMcpTransport(transportConfig) ? { type: "custom" } : {
type: transportConfig.type,
url: transportConfig.url
} });
if (events?.adapter !== void 0) validateMCPEventOperations(operations);
this.onUncaughtError = onUncaughtError;
this.maxRetries = prepareMaxRetries(maxRetries);
this.clientCapabilities = capabilities ?? {};
this.initialInitializeResult = initialInitializeResult;
this.initializationOptions = initializationOptions;
this.protocolVersionDiscovery = protocolVersionDiscovery;
if (isCustomMcpTransport(transportConfig)) this.transport = transportConfig;
else this.transport = createMcpTransport(transportConfig);
this.transport.onclose = () => this.onClose();
this.transport.onerror = (error) => this.onError(error);
this.transport.onmessage = (message) => {
if ("method" in message) {
if ("id" in message) this.onRequestMessage(message);
else this.onError(new MCPClientError({ message: "Unsupported message type" }));
return;
}
this.onResponse(message);
};
this.clientInfo = {
name: clientName,
version
};
const directEvents = createMCPEvents({
request: (args) => this.request(args),
store: events?.store,
validateArguments: events?.validateArguments
});
this.experimental_events = operations ? createManagedMCPEvents(operations, directEvents.list) : directEvents;
}
get serverInfo() {
return this._serverInfo;
}
get initializeResult() {
return this._initializeResult;
}
get instructions() {
return this._serverInstructions;
}
async init() {
const externalSignal = this.initializationOptions?.signal;
const timeout = this.initializationOptions ? getEffectiveTimeout(this.initializationOptions) : void 0;
const timeoutController = timeout == null ? void 0 : new AbortController();
const signal = externalSignal == null ? timeoutController?.signal : timeoutController == null ? externalSignal : AbortSignal.any([externalSignal, timeoutController.signal]);
let timeoutId;
let timeoutError;
if (timeout != null) timeoutId = setTimeout(() => {
timeoutError = new MCPClientError({ message: `MCP client initialization timed out after ${timeout}ms` });
timeoutController?.abort(timeoutError);
}, timeout);
try {
this.isClosed = false;
signal?.throwIfAborted();
await waitForAbort(this.transport.start(), signal);
if (this.initialInitializeResult) {
const result = InitializeResultSchema.parse(this.initialInitializeResult);
this.applyInitializeResult(result);
return this;
}
if (this.protocolVersionDiscovery && this.transport.supportsProtocolVersionDiscovery) {
if (await this.tryProtocolDiscovery(signal)) return this;
}
this.protocolEra = "legacy";
this.protocolVersion = LATEST_LEGACY_PROTOCOL_VERSION;
this.setTransportProtocolVersion(this.protocolVersion);
const result = await this.request({
request: {
method: "initialize",
params: {
protocolVersion: LATEST_LEGACY_PROTOCOL_VERSION,
capabilities: this.clientCapabilities,
clientInfo: this.clientInfo
}
},
resultSchema: InitializeResultSchema,
options: { signal }
});
if (result === void 0) throw new MCPClientError({ message: "Server sent invalid initialize result" });
this.applyInitializeResult(result);
await this.notification({ method: "notifications/initialized" }, { signal });
return this;
} catch (error) {
try {
await waitForAbort(this.transport.close({ signal }), signal);
} catch {}
this.onClose();
if (timeoutError != null) throw timeoutError;
if (externalSignal?.aborted) throw new MCPClientError({
message: "MCP client initialization was aborted",
cause: externalSignal.reason
});
throw error;
} finally {
if (timeoutId != null) clearTimeout(timeoutId);
}
}
async tryProtocolDiscovery(signal) {
this.protocolEra = "modern";
this.protocolVersion = LATEST_PROTOCOL_VERSION;
this.setTransportProtocolVersion(this.protocolVersion);
try {
const result = await this.request({
request: { method: "server/discover" },
resultSchema: DiscoverResultSchema,
options: {
signal,
timeout: this.transport.waitsForProtocolVersionDiscovery ? void 0 : DEFAULT_PROTOCOL_DISCOVERY_TIMEOUT
}
});
this.applyDiscoverResult(result);
return true;
} catch (error) {
if (MCPClientError.isInstance(error) && error.code != null && MODERN_PROTOCOL_ERROR_CODES.includes(error.code)) {
if (error.code === -32022 && error.data != null && typeof error.data === "object" && "supported" in error.data && Array.isArray(error.data.supported) && error.data.supported.every((version) => typeof version === "string")) {
const supportedVersions = error.data.supported;
if (!supportedVersions.includes(this.protocolVersion) && supportedVersions.some((version) => version <= "2025-11-25" && SUPPORTED_PROTOCOL_VERSIONS.includes(version))) throw new MCPClientError({
message: `MCP server rejected protocol version ${this.protocolVersion} during discovery. Server-supported versions: ${supportedVersions.join(", ")}. The client does not automatically switch to legacy initialization after this error. If this is a known legacy server, set protocolVersionDiscovery: false to use initialize.`,
code: error.code,
data: error.data,
statusCode: error.statusCode,
url: error.url,
responseBody: error.responseBody,
cause: error
});
}
throw error;
}
if (this.transport.waitsForProtocolVersionDiscovery && !MCPClientError.isInstance(error)) throw error;
return false;
}
}
applyDiscoverResult(result) {
if (!result.supportedVersions.includes(this.protocolVersion)) throw new MCPClientError({ message: `Server does not support the requested protocol version: ${this.protocolVersion}` });
const serverInfo = result._meta?.["io.modelcontextprotocol/serverInfo"];
if (serverInfo != null && typeof serverInfo === "object" && "name" in serverInfo && typeof serverInfo.name === "string" && "version" in serverInfo && typeof serverInfo.version === "string") this._serverInfo = serverInfo;
this.serverCapabilities = result.capabilities;
this._serverInstructions = result.instructions;
this._initializeResult = {
protocolVersion: this.protocolVersion,
capabilities: result.capabilities,
serverInfo: this._serverInfo,
instructions: result.instructions
};
}
setTransportProtocolVersion(version) {
if (this.transport.setProtocolVersion) this.transport.setProtocolVersion(version);
else this.transport.protocolVersion = version;
}
applyInitializeResult(result) {
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) throw new MCPClientError({ message: `Server's protocol version is not supported: ${result.protocolVersion}` });
this.serverCapabilities = result.capabilities;
this.protocolEra = "legacy";
this.protocolVersion = result.protocolVersion;
this._serverInfo = result.serverInfo;
this._initializeResult = result;
this.setTransportProtocolVersion(result.protocolVersion);
this._serverInstructions = result.instructions;
}
async close() {
if (this.isClosed) return;
await this.transport?.close();
this.onClose();
}
send(message, options) {
return options == null ? this.transport.send(message) : this.transport.send(message, options);
}
assertCapability(method) {
switch (method) {
case "initialize":
case "server/discover": break;
case "completion/complete":
if (!this.serverCapabilities.completions) throw new MCPClientError({ message: `Server does not support completions` });
break;
case "events/list":
case "events/subscribe":
case "events/unsubscribe":
if (!this.serverCapabilities.events) throw new MCPClientError({ message: "Server does not support events" });
break;
case "tools/list":
case "tools/call":
if (!this.serverCapabilities.tools) throw new MCPClientError({ message: `Server does not support tools` });
break;
case "resources/list":
case "resources/read":
case "resources/templates/list":
if (!this.serverCapabilities.resources) throw new MCPClientError({ message: `Server does not support resources` });
break;
case "prompts/list":
case "prompts/get":
if (!this.serverCapabilities.prompts) throw new MCPClientError({ message: `Server does not support prompts` });
break;
default: throw new MCPClientError({ message: `Unsupported method: ${method}` });
}
}
async request({ request, resultSchema, options }) {
if (this.isClosed || this.protocolEra !== "modern" || ![
"tools/call",
"prompts/get",
"resources/read"
].includes(request.method)) return this.requestOnce({
request,
resultSchema,
options
});
options?.signal?.throwIfAborted();
const startedAt = Date.now();
let nextRequest = request;
for (let round = 0;; round++) {
const timeout = getEffectiveTimeout({
timeout: options?.timeout,
maxTotalTimeout: options?.maxTotalTimeout == null ? void 0 : Math.max(0, options.maxTotalTimeout - (Date.now() - startedAt))
});
const timeoutController = new AbortController();
const signal = AbortSignal.any([
this.closeController.signal,
timeoutController.signal,
...options?.signal == null ? [] : [options.signal]
]);
const timeoutError = new MCPClientError({ message: `Request timed out after ${timeout}ms` });
const timeoutId = timeout == null ? void 0 : setTimeout(() => timeoutController.abort(timeoutError), timeout);
try {
signal.throwIfAborted();
if (timeout === 0) throw timeoutError;
const result = await this.requestOnce({
request: nextRequest,
resultSchema: ResultSchema,
options: { signal },
allowInputRequired: true
});
if (result.resultType !== "input_required") return resultSchema.parse(result);
const { inputRequests, requestState } = InputRequiredResultSchema.parse(result);
if (round === MAX_INPUT_REQUIRED_ROUNDS) throw new MCPClientError({ message: `Request still requires input after ${MAX_INPUT_REQUIRED_ROUNDS} rounds` });
const responses = Object.create(null);
for (const [key, inputRequest] of Object.entries(inputRequests ?? {})) {
signal.throwIfAborted();
if (inputRequest.method !== "elicitation/create" || inputRequest.params?.mode != null && inputRequest.params.mode !== "form") throw new MCPClientError({ message: `Unsupported input request: ${inputRequest.method}. Only form elicitation is supported.` });
if (!this.elicitationRequestHandler) throw new MCPClientError({ message: "Server requested input, but no handler is registered. Use onElicitationRequest to handle elicitation." });
const parsedRequest = InputRequiredElicitationSchema.parse(inputRequest);
const answer = await waitForAbort(Promise.resolve(this.elicitationRequestHandler(parsedRequest)), signal);
responses[key] = ElicitResultSchema.parse(answer);
}
const hasResponses = Object.keys(responses).length > 0;
if (!hasResponses) await delay(INPUT_REQUIRED_RETRY_DELAY, { abortSignal: signal });
signal.throwIfAborted();
nextRequest = {
...request,
params: {
...request.params,
...hasResponses ? { inputResponses: responses } : {},
...requestState !== void 0 ? { requestState } : {}
}
};
} catch (error) {
if (signal.aborted) throw options?.signal?.aborted ? new MCPClientError({
message: "Request was aborted",
cause: options.signal.reason
}) : signal.reason;
throw MCPClientError.isInstance(error) ? error : new MCPClientError({
message: "Failed to process server response",
cause: error
});
} finally {
clearTimeout(timeoutId);
}
}
}
async requestOnce({ request, resultSchema, options, allowInputRequired = false }) {
return new Promise((resolve, reject) => {
if (this.isClosed) return reject(new MCPClientError({ message: "Attempted to send a request from a closed client" }));
this.assertCapability(request.method);
const signal = options?.signal;
signal?.throwIfAborted();
const timeout = options == null ? void 0 : getEffectiveTimeout(options);
const timeoutController = timeout == null ? void 0 : new AbortController();
const transportSignal = signal == null ? timeoutController?.signal : timeoutController == null ? signal : AbortSignal.any([signal, timeoutController.signal]);
let timeoutId;
const messageId = this.requestMessageId++;
const preparedRequest = this.protocolEra === "modern" ? {
...request,
params: {
...request.params,
_meta: {
...request.params?._meta,
"io.modelcontextprotocol/protocolVersion": this.protocolVersion,
"io.modelcontextprotocol/clientCapabilities": this.clientCapabilities,
"io.modelcontextprotocol/clientInfo": this.clientInfo
}
}
} : request;
const jsonrpcRequest = {
...preparedRequest,
jsonrpc: "2.0",
id: messageId
};
const headers = this.getToolRequestHeaders(preparedRequest);
const rejectWithAbortError = () => {
reject(new MCPClientError({
message: "Request was aborted",
cause: signal?.reason
}));
};
const cleanup = () => {
this.responseHandlers.delete(messageId);
signal?.removeEventListener("abort", onAbort);
if (timeoutId != null) clearTimeout(timeoutId);
};
const rejectAndCleanup = (error) => {
cleanup();
reject(error);
};
const onAbort = () => {
cleanup();
rejectWithAbortError();
};
const onTimeout = () => {
const error = new MCPClientError({ message: `Request timed out after ${timeout}ms` });
timeoutController?.abort(error);
rejectAndCleanup(error);
};
this.responseHandlers.set(messageId, (response) => {
if (signal?.aborted) {
cleanup();
return rejectWithAbortError();
}
if (response instanceof Error) return rejectAndCleanup(response);
try {
if (this.protocolEra === "modern" && response.result.resultType == null) throw new MCPClientError({ message: "Modern MCP result is missing resultType" });
if (response.result.resultType === "input_required" && !allowInputRequired) throw new MCPClientError({ message: `Input-required results are not supported for ${request.method} on protocol ${this.protocolVersion}` });
const result = resultSchema.parse(response.result);
cleanup();
resolve(result);
} catch (error) {
const parseError = MCPClientError.isInstance(error) ? error : new MCPClientError({
message: "Failed to parse server response",
cause: error
});
rejectAndCleanup(parseError);
}
});
signal?.addEventListener("abort", onAbort, { once: true });
if (timeout != null) timeoutId = setTimeout(onTimeout, timeout);
const sendOptions = {
...transportSignal == null ? {} : { signal: transportSignal },
...headers == null ? {} : { headers }
};
(Object.keys(sendOptions).length === 0 ? this.send(jsonrpcRequest) : this.send(jsonrpcRequest, sendOptions)).catch((error) => {
rejectAndCleanup(error);
});
});
}
async listTools({ params, options } = {}) {
const result = await this.request({
request: {
method: "tools/list",
params
},
resultSchema: ListToolsResultSchema,
options
});
return this.prepareToolDefinitions(result, params?.cursor == null);
}
prepareToolDefinitions(definitions, resetHeaderBindings = false) {
if (this.protocolEra !== "modern" || !this.transport.supportsMcpToolParameterHeaders) return definitions;
if (resetHeaderBindings) this.toolHeaderBindings.clear();
const tools = definitions.tools.filter((toolDefinition) => {
const result = getMCPToolHeaderBindings(toolDefinition.inputSchema);
if (!result.success) {
this.onError(new MCPClientError({ message: `Ignoring MCP tool "${toolDefinition.name}": ${result.error}` }));
return false;
}
this.toolHeaderBindings.set(toolDefinition.name, result.bindings);
return true;
});
return {
...definitions,
tools
};
}
getToolRequestHeaders(request) {
if (this.protocolEra !== "modern" || request.method !== "tools/call" || typeof request.params?.name !== "string") return;
const bindings = this.toolHeaderBindings.get(request.params.name);
if (bindings == null || bindings.length === 0) return;
const args = request.params.arguments;
if (args == null || typeof args !== "object" || Array.isArray(args)) return;
try {
return createMCPToolHeaders({
bindings,
args
});
} catch (error) {
throw new MCPClientError({
message: `Failed to create MCP headers for tool "${request.params.name}"`,
cause: error
});
}
}
async callToolWithRetry({ options, execute }) {
if (this.maxRetries === 0) return execute();
return retryWithExponentialBackoff({
maxRetries: this.maxRetries,
abortSignal: options?.signal,
shouldRetry: isRetryableMCPToolCallError,
createRetryError: ({ message, errors }) => new MCPClientError({
message,
cause: errors[errors.length - 1]
})
})(execute);
}
async callTool({ name, arguments: args = {}, options }) {
return this.callToolWithRetry({
options,
execute: () => this.request({
request: {
method: "tools/call",
params: {
name,
arguments: args
}
},
resultSchema: CallToolResultSchema,
options
})
});
}
async listResourcesInternal({ params, options } = {}) {
return this.request({
request: {
method: "resources/list",
params
},
resultSchema: ListResourcesResultSchema,
options
});
}
async readResourceInternal({ uri, options }) {
return this.request({
request: {
method: "resources/read",
params: { uri }
},
resultSchema: ReadResourceResultSchema,
options
});
}
async listResourceTemplatesInternal({ options } = {}) {
return this.request({
request: { method: "resources/templates/list" },
resultSchema: ListResourceTemplatesResultSchema,
options
});
}
async listPromptsInternal({ params, options } = {}) {
return this.request({
request: {
method: "prompts/list",
params
},
resultSchema: ListPromptsResultSchema,
options
});
}
async getPromptInternal({ name, args, options }) {
return this.request({
request: {
method: "prompts/get",
params: {
name,
arguments: args
}
},
resultSchema: GetPromptResultSchema,
options
});
}
async completeInternal({ options, ...params }) {
return this.request({
request: {
method: "completion/complete",
params
},
resultSchema: CompleteResultSchema,
options
});
}
async notification(notification, options) {
const jsonrpcNotification = {
...notification,
jsonrpc: "2.0"
};
await waitForAbort(this.send(jsonrpcNotification, options?.signal == null ? void 0 : { signal: options.signal }), options?.signal);
}
/**
* Returns a set of AI SDK tools from the MCP server.
* This fetches tool definitions and wraps them with execute functions.
* @returns A record of tool names to their implementations
*/
async tools({ schemas = "automatic" } = {}) {
let definitions = await this.listTools();
const tools = [...definitions.tools];
while (definitions.nextCursor != null) {
definitions = await this.listTools({ params: { cursor: definitions.nextCursor } });
tools.push(...definitions.tools);
}
return this.toolsFromDefinitions({
...definitions,
tools
}, { schemas });
}
/**
* Creates AI SDK tools from tool definitions without fetching from the server.
*/
toolsFromDefinitions(definitions, { schemas = "automatic" } = {}) {
definitions = this.prepareToolDefinitions(definitions);
const tools = Object.create(null);
for (const { name, title, description, inputSchema, annotations, _meta } of definitions.tools) {
const resolvedTitle = title ?? annotations?.title;
if (schemas !== "automatic" && !Object.prototype.hasOwnProperty.call(schemas, name)) continue;
const self = this;
const outputSchema = schemas !== "automatic" ? schemas[name]?.outputSchema : void 0;
const appMeta = getMCPAppToolMeta({ _meta });
const metadata = {
clientName: this.clientInfo.name,
toolName: name,
...resolvedTitle != null ? { title: resolvedTitle } : {},
...annotations != null ? { annotations: {
...annotations.title != null ? { title: annotations.title } : {},
...annotations.readOnlyHint != null ? { readOnlyHint: annotations.readOnlyHint } : {},
...annotations.destructiveHint != null ? { destructiveHint: annotations.destructiveHint } : {},
...annotations.idempotentHint != null ? { idempotentHint: annotations.idempotentHint } : {},
...annotations.openWorldHint != null ? { openWorldHint: annotations.openWorldHint } : {}
} } : {},
...appMeta?.resourceUri != null ? { app: {
...appMeta,
mimeType: MCP_APP_MIME_TYPE
} } : {}
};
const execute = async (args, options) => {
options?.abortSignal?.throwIfAborted();
const result = await self.callTool({
name,
arguments: args,
options: { signal: options?.abortSignal }
});
if (result.isError) return result;
if (outputSchema != null) return self.extractStructuredContent(result, outputSchema, name);
return result;
};
tools[name] = {
...schemas === "automatic" ? dynamicTool({
description,
title: resolvedTitle,
metadata,
inputSchema: jsonSchema({
...inputSchema,
properties: inputSchema.properties ?? {},
additionalProperties: false
}),
execute,
toModelOutput: mcpToModelOutput
}) : tool({
description,
title: resolvedTitle,
metadata,
inputSchema: schemas[name].inputSchema,
...outputSchema != null ? { outputSchema } : {},
execute,
toModelOutput: mcpToModelOutput
}),
_meta
};
}
return tools;
}
/**
* Extracts and validates structuredContent from a tool result.
*/
async extractStructuredContent(result, outputSchema, toolName) {
if ("structuredContent" in result && result.structuredContent != null) {
const validationResult = await safeValidateTypes({
value: result.structuredContent,
schema: asSchema(outputSchema)
});
if (!validationResult.success) throw new MCPClientError({
message: `Tool "${toolName}" returned structuredContent that does not match the expected outputSchema`,
cause: validationResult.error
});
return validationResult.value;
}
if ("content" in result && Array.isArray(result.content)) {
const textContent = result.content.find((content) => content.type === "text");
if (textContent && "text" in textContent) {
const parseResult = await safeParseJSON({
text: textContent.text,
schema: outputSchema
});
if (!parseResult.success) throw new MCPClientError({
message: `Tool "${toolName}" returned content that does not match the expected outputSchema`,
cause: parseResult.error
});
return parseResult.value;
}
}
throw new MCPClientError({ message: `Tool "${toolName}" did not return structuredContent or parseable text content` });
}
listResources({ params, options } = {}) {
return this.listResourcesInternal({
params,
options
});
}
readResource({ uri, options }) {
return this.readResourceInternal({
uri,
options
});
}
listResourceTemplates({ options } = {}) {
return this.listResourceTemplatesInternal({ options });
}
listPrompts({ params, options } = {}) {
return this.listPromptsInternal({
params,
options
});
}
getPrompt({ name, arguments: args, options }) {
return this.getPromptInternal({
name,
args,
options
});
}
/** @deprecated Use `listPrompts` instead. */
experimental_listPrompts(options) {
return this.listPrompts(options);
}
/** @deprecated Use `getPrompt` instead. */
experimental_getPrompt(args) {
return this.getPrompt(args);
}
complete(args) {
return this.completeInternal(args);
}
onElicitationRequest(schema, handler) {
if (schema !== ElicitationRequestSchema) throw new MCPClientError({ message: "Unsupported request schema. Only ElicitationRequestSchema is supported." });
this.elicitationRequestHandler = handler;
}
async onRequestMessage(request) {
try {
if (request.method === "ping") {
await this.transport.send({
jsonrpc: "2.0",
id: request.id,
result: {}
});
return;
}
if (request.method !== "elicitation/create") {
await this.transport.send({
jsonrpc: "2.0",
id: request.id,
error: {
code: -32601,
message: `Unsupported request method: ${request.method}`
}
});
return;
}
if (!this.elicitationRequestHandler) {
await this.transport.send({
jsonrpc: "2.0",
id: request.id,
error: {
code: -32601,
message: "No elicitation handler registered on client"
}
});
return;
}
const parsedRequest = ElicitationRequestSchema.safeParse({
method: request.method,
params: request.params
});
if (!parsedRequest.success) {
await this.transport.send({
jsonrpc: "2.0",
id: request.id,
error: {
code: -32602,
message: `Invalid elicitation request: ${parsedRequest.error.message}`,
data: parsedRequest.error.issues
}
});
return;
}
try {
const result = await this.elicitationRequestHandler(parsedRequest.data);
const validatedResult = ElicitResultSchema.parse(result);
await this.transport.send({
jsonrpc: "2.0",
id: request.id,
result: validatedResult
});
} catch (error) {
await this.transport.send({
jsonrpc: "2.0",
id: request.id,
error: {
code: -32603,
message: error instanceof Error ? error.message : "Failed to handle elicitation request"
}
});
this.onError(error);
}
} catch (error) {
this.onError(error);
}
}
onClose() {
if (this.isClosed) return;
this.isClosed = true;
const error = new MCPClientError({ message: "Connection closed" });
this.closeController.abort(error);
for (const handler of this.responseHandlers.values()) handler(error);
this.responseHandlers.clear();
}
onError(error) {
if (this.onUncaughtError) this.onUncaughtError(error);
}
onResponse(response) {
if (response.id == null) {
this.onError(new MCPClientError({ message: `Protocol error: Received a response without a message ID: ${JSON.stringify(response)}` }));
return;
}
const messageId = Number(response.id);
const handler = this.responseHandlers.get(messageId);
if (handler === void 0) throw new MCPClientError({ message: `Protocol error: Received a response for an unknown message ID: ${JSON.stringify(response)}` });
this.responseHandlers.delete(messageId);
handler("result" in response ? response : new MCPClientError({
message: response.error.message,
code: response.error.code,
data: response.error.data,
cause: response.error
}));
}
};
//#endregion
//#region src/tool/mcp-app-fingerprint.ts
const encoder = new TextEncoder();
/**
* Deterministic JSON serialization: object keys are sorted so two
* structurally-equal values always produce the same string regardless of key
* insertion order. Used as the stable input to content hashing.
*/
function canonicalJSON(value) {
if (value == null || typeof value !== "object") return JSON.stringify(value ?? null);
if (Array.isArray(value)) return `[${value.map(canonicalJSON).join(",")}]`;
const record = value;
return `{${Object.keys(record).sort().map((k) => `${JSON.stringify(k)}:${canonicalJSON(record[k])}`).join(",")}}`;
}
function toBase64url(bytes) {
return convertUint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
/**
* Stable SHA-256 digest (base64url) over an MCP App resource's `html`, `csp`,
* and `permissions`.
*
* Capture a baseline on first load and compare later reads with
* {@link detectMCPAppResourceDrift} to detect a changed resource. Baseline
* storage and the response are the host's concern, mirroring `fingerprintTools`.
*/
async function fingerprintMCPAppResource(resource) {
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalJSON({
html: resource.html,
csp: resource.meta?.csp ?? null,
permissions: resource.meta?.permissions ?? null
})));
return toBase64url(new Uint8Array(digest));
}
/**
* Compares two fingerprints from {@link fingerprintMCPAppResource}. Returns
* `true` when the current resource differs from its baseline.
*/
function detectMCPAppResourceDrift(current, baseline) {
return current !== baseline;
}
//#endregion
//#region src/tool/mcp-event-webhook.ts
const MAX_BODY_BYTES = 262144;
const VerificationSchema = z.object({
type: z.literal("verification"),
challenge: z.string().min(1)
});
/**
* Create a Web Request/Response handler for MCP webhook deliveries. Mount it
* at the callback URL before subscribing. Verification is enabled automatically
* using the pending delivery secret saved by the client's subscribe method.
*/
function createMCPEventWebhook({ store, validatePayload, onEvent, onGap, onTerminated, onError }) {
return async (request) => {
if (request.method !== "POST") return new Response(null, {
status: 405,
headers: { Allow: "POST" }
});
if (request.headers.get("content-type")?.split(";")[0].trim().toLowerCase() !== "application/json") return new Response(null, { status: 415 });
if (Number(request.headers.get("content-length")) > MAX_BODY_BYTES) return new Response(null, { status: 413 });
try {
const key = new URL(request.url).searchParams.get(MCP_EVENT_CALLBACK_KEY);
const id = request.headers.get("X-MCP-Subscription-Id");
if (!key || !id) return new Response(null, { status: 401 });
const subscription = await store.get(key);
if (!subscription || subscription.id != null && subscription.id !== id) return new Response(null, { status: 401 });
const reader = request.body?.getReader();
if (!reader) return new Response(null, { status: 400 });
const chunks = [];
let length = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
length += value.byteLength;
if (length > MAX_BODY_BYTES) {
await reader.cancel();
return new Response(null, { status: 413 });
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
const body = new Uint8Array(length);
let offset = 0;
for (const chunk of chunks) {
body.set(chunk, offset);
offset += chunk.length;
}
if (!await verifyMCPEventSignature({
headers: request.headers,
body,
secret: subscription.delivery.secret
})) return new Response(null, { status: 401 });
const parsed = await safeParseJSON({ text: new TextDecoder().decode(body) });
if (!parsed.success) return new Response(null, { status: 400 });
const value = parsed.value;
let control;
if (value != null && typeof value === "object" && "type" in value) {
if (value.type === "verification") {
const verification = VerificationSchema.safeParse(value);
if (!verification.success) return new Response(null, { status: 400 });
return Response.json({ challenge: verification.data.challenge });
}
const parsedControl = MCPEventControlSchema.safeParse(value);
if (!parsedControl.success) return new Response(null, { status: 400 });
control = parsedControl.data;
}
if (subscription.id == null || subscription.status !== "active" && control?.type !== "terminated") return new Response(null, { status: 503 });
const subscriptionInfo = {
id: subscription.id,
name: subscription.name,
arguments: subscription.arguments,
delivery: {
mode: "webhook",
url: subscription.delivery.url
},
refreshBefore: subscription.refreshBefore ?? null
};
if (control) {
const messageId = request.headers.get("webhook-id");
if (control.type === "gap") {
await onGap?.({
subscription: subscriptionInfo,
messageId,
gap: control
});
await store.update(subscription.key, {
cursor: control.cursor,
truncated: true
});
} else {
await onTerminated?.({
subscription: subscriptionInfo,
messageId,
termination: control
});
await store.delete(subscription.key);
}
return new Response(null, { status: 204 });
}
if (subscription.refreshBefore != null && Date.parse(subscription.refreshBefore) <= Date.now()) return new Response(null, { status: 410 });
const event = MCPEventSchema.safeParse(value);
if (!event.success || event.data.name !== subscription.name || event.data.eventId !== request.headers.get("webhook-id")) return new Response(null, { status: 400 });
try {
await validatePayload?.({
definition: subscription.definition,
data: event.data.data
});
} catch (error) {
onError?.(error);
return new Response(null, { status: 400 });
}
await onEvent({
subscription: subscriptionInfo,
event: event.data
});
if (event.data.cursor !== void 0) await store.update(subscription.key, { cursor: event.data.cursor });
return new Response(null, { status: 204 });
} catch (error) {
onError?.(error);
return new Response(null, { status: 503 });
}
};
}
//#endregion
export { AuthorizationServerMismatchError, ElicitResultSchema, ElicitationRequestSchema, MCPClientError, MCPClientOAuthError, MCP_APP_MIME_TYPE, UnauthorizedError, auth, createMCPClient, createMCPClient as experimental_createMCPClient, detectMCPAppResourceDrift, createMCPEventWebhook as experimental_createMCPEventWebhook, fingerprintMCPAppResource, mcpAppClientCapabilities, readMCPAppResource, splitMCPAppTools, validateJSONRPCMessage };
//# sourceMappingURL=index.js.map