UNPKG

agents

Version:
1,122 lines (1,117 loc) 39.7 kB
import { t as DurableObjectOAuthClientProvider } from "./do-oauth-client-provider-DDg8QrEA.js"; import { nanoid } from "nanoid"; import escapeHtml from "escape-html"; import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker-provider.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { ElicitRequestSchema, PromptListChangedNotificationSchema, ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js"; //#region src/core/events.ts function toDisposable(fn) { return { dispose: fn }; } var DisposableStore = class { constructor() { this._items = []; } add(d) { this._items.push(d); return d; } dispose() { while (this._items.length) try { this._items.pop().dispose(); } catch {} } }; var Emitter = class { constructor() { this._listeners = /* @__PURE__ */ new Set(); this.event = (listener) => { this._listeners.add(listener); return toDisposable(() => this._listeners.delete(listener)); }; } fire(data) { for (const listener of [...this._listeners]) try { listener(data); } catch (err) { console.error("Emitter listener error:", err); } } dispose() { this._listeners.clear(); } }; //#endregion //#region src/mcp/errors.ts function toErrorMessage(error) { return error instanceof Error ? error.message : String(error); } function getErrorCode(error) { if (error && typeof error === "object" && "code" in error && typeof error.code === "number") return error.code; } function isUnauthorized(error) { if (getErrorCode(error) === 401) return true; const msg = toErrorMessage(error); return msg.includes("Unauthorized") || msg.includes("401"); } function isTransportNotImplemented(error) { const code = getErrorCode(error); if (code === 404 || code === 405) return true; const msg = toErrorMessage(error); return msg.includes("404") || msg.includes("405") || msg.includes("Not Implemented") || msg.includes("not implemented"); } //#endregion //#region src/mcp/client-connection.ts /** * Connection state machine for MCP client connections. * * State transitions: * - Non-OAuth: init() → CONNECTING → DISCOVERING → READY * - OAuth: init() → AUTHENTICATING → (callback) → CONNECTING → DISCOVERING → READY * - Any state can transition to FAILED on error */ const MCPConnectionState = { AUTHENTICATING: "authenticating", CONNECTING: "connecting", CONNECTED: "connected", DISCOVERING: "discovering", READY: "ready", FAILED: "failed" }; var MCPClientConnection = class { constructor(url, info, options = { client: {}, transport: {} }) { this.url = url; this.options = options; this.connectionState = MCPConnectionState.CONNECTING; this.connectionError = null; this.tools = []; this.prompts = []; this.resources = []; this.resourceTemplates = []; this._onObservabilityEvent = new Emitter(); this.onObservabilityEvent = this._onObservabilityEvent.event; this.client = new Client(info, { ...options.client, capabilities: { ...options.client?.capabilities, elicitation: {} } }); } /** * Initialize a client connection, if authentication is required, the connection will be in the AUTHENTICATING state * Sets connection state based on the result and emits observability events * * @returns Error message if connection failed, undefined otherwise */ async init() { const transportType = this.options.transport.type; if (!transportType) throw new Error("Transport type must be specified"); const res = await this.tryConnect(transportType); this.connectionState = res.state; if (res.state === MCPConnectionState.CONNECTED && res.transport) { this.client.setRequestHandler(ElicitRequestSchema, async (request) => { return await this.handleElicitationRequest(request); }); this.lastConnectedTransport = res.transport; this._onObservabilityEvent.fire({ type: "mcp:client:connect", displayMessage: `Connected successfully using ${res.transport} transport for ${this.url.toString()}`, payload: { url: this.url.toString(), transport: res.transport, state: this.connectionState }, timestamp: Date.now(), id: nanoid() }); return; } else if (res.state === MCPConnectionState.FAILED && res.error) { const errorMessage = toErrorMessage(res.error); this._onObservabilityEvent.fire({ type: "mcp:client:connect", displayMessage: `Failed to connect to ${this.url.toString()}: ${errorMessage}`, payload: { url: this.url.toString(), transport: transportType, state: this.connectionState, error: errorMessage }, timestamp: Date.now(), id: nanoid() }); return errorMessage; } } /** * Finish OAuth by probing transports based on configured type. * - Explicit: finish on that transport * - Auto: try streamable-http, then sse on 404/405/Not Implemented */ async finishAuthProbe(code) { if (!this.options.transport.authProvider) throw new Error("No auth provider configured"); const configuredType = this.options.transport.type; if (!configuredType) throw new Error("Transport type must be specified"); const finishAuth = async (base) => { await this.getTransport(base).finishAuth(code); }; if (configuredType === "sse" || configuredType === "streamable-http") { await finishAuth(configuredType); return; } try { await finishAuth("streamable-http"); } catch (e) { if (isTransportNotImplemented(e)) { await finishAuth("sse"); return; } throw e; } } /** * Complete OAuth authorization */ async completeAuthorization(code) { if (this.connectionState !== MCPConnectionState.AUTHENTICATING) throw new Error("Connection must be in authenticating state to complete authorization"); try { await this.finishAuthProbe(code); this.connectionState = MCPConnectionState.CONNECTING; } catch (error) { this.connectionState = MCPConnectionState.FAILED; throw error; } } /** * Discover server capabilities and register tools, resources, prompts, and templates. * This method does the work but does not manage connection state - that's handled by discover(). */ async discoverAndRegister() { this.serverCapabilities = this.client.getServerCapabilities(); if (!this.serverCapabilities) throw new Error("The MCP Server failed to return server capabilities"); const operations = []; const operationNames = []; operations.push(Promise.resolve(this.client.getInstructions())); operationNames.push("instructions"); if (this.serverCapabilities.tools) { operations.push(this.registerTools()); operationNames.push("tools"); } if (this.serverCapabilities.resources) { operations.push(this.registerResources()); operationNames.push("resources"); } if (this.serverCapabilities.prompts) { operations.push(this.registerPrompts()); operationNames.push("prompts"); } if (this.serverCapabilities.resources) { operations.push(this.registerResourceTemplates()); operationNames.push("resource templates"); } try { const results = await Promise.all(operations); for (let i = 0; i < results.length; i++) { const result = results[i]; switch (operationNames[i]) { case "instructions": this.instructions = result; break; case "tools": this.tools = result; break; case "resources": this.resources = result; break; case "prompts": this.prompts = result; break; case "resource templates": this.resourceTemplates = result; break; } } } catch (error) { this._onObservabilityEvent.fire({ type: "mcp:client:discover", displayMessage: `Failed to discover capabilities for ${this.url.toString()}: ${toErrorMessage(error)}`, payload: { url: this.url.toString(), error: toErrorMessage(error) }, timestamp: Date.now(), id: nanoid() }); throw error; } } /** * Discover server capabilities with timeout and cancellation support. * If called while a previous discovery is in-flight, the previous discovery will be aborted. * * @param options Optional configuration * @param options.timeoutMs Timeout in milliseconds (default: 15000) * @returns Result indicating success/failure with optional error message */ async discover(options = {}) { const { timeoutMs = 15e3 } = options; if (this.connectionState !== MCPConnectionState.CONNECTED && this.connectionState !== MCPConnectionState.READY) { this._onObservabilityEvent.fire({ type: "mcp:client:discover", displayMessage: `Discovery skipped for ${this.url.toString()}, state is ${this.connectionState}`, payload: { url: this.url.toString(), state: this.connectionState }, timestamp: Date.now(), id: nanoid() }); return { success: false, error: `Discovery skipped - connection in ${this.connectionState} state` }; } if (this._discoveryAbortController) { this._discoveryAbortController.abort(); this._discoveryAbortController = void 0; } const abortController = new AbortController(); this._discoveryAbortController = abortController; this.connectionState = MCPConnectionState.DISCOVERING; let timeoutId; try { const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Discovery timed out after ${timeoutMs}ms`)), timeoutMs); }); if (abortController.signal.aborted) throw new Error("Discovery was cancelled"); const abortPromise = new Promise((_, reject) => { abortController.signal.addEventListener("abort", () => { reject(/* @__PURE__ */ new Error("Discovery was cancelled")); }); }); await Promise.race([ this.discoverAndRegister(), timeoutPromise, abortPromise ]); if (timeoutId !== void 0) clearTimeout(timeoutId); this.connectionState = MCPConnectionState.READY; this._onObservabilityEvent.fire({ type: "mcp:client:discover", displayMessage: `Discovery completed for ${this.url.toString()}`, payload: { url: this.url.toString() }, timestamp: Date.now(), id: nanoid() }); return { success: true }; } catch (e) { if (timeoutId !== void 0) clearTimeout(timeoutId); this.connectionState = MCPConnectionState.CONNECTED; return { success: false, error: e instanceof Error ? e.message : String(e) }; } finally { this._discoveryAbortController = void 0; } } /** * Cancel any in-flight discovery operation. * Called when closing the connection. */ cancelDiscovery() { if (this._discoveryAbortController) { this._discoveryAbortController.abort(); this._discoveryAbortController = void 0; } } /** * Notification handler registration for tools * Should only be called if serverCapabilities.tools exists */ async registerTools() { if (this.serverCapabilities?.tools?.listChanged) this.client.setNotificationHandler(ToolListChangedNotificationSchema, async (_notification) => { this.tools = await this.fetchTools(); }); return this.fetchTools(); } /** * Notification handler registration for resources * Should only be called if serverCapabilities.resources exists */ async registerResources() { if (this.serverCapabilities?.resources?.listChanged) this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async (_notification) => { this.resources = await this.fetchResources(); }); return this.fetchResources(); } /** * Notification handler registration for prompts * Should only be called if serverCapabilities.prompts exists */ async registerPrompts() { if (this.serverCapabilities?.prompts?.listChanged) this.client.setNotificationHandler(PromptListChangedNotificationSchema, async (_notification) => { this.prompts = await this.fetchPrompts(); }); return this.fetchPrompts(); } async registerResourceTemplates() { return this.fetchResourceTemplates(); } async fetchTools() { let toolsAgg = []; let toolsResult = { tools: [] }; do { toolsResult = await this.client.listTools({ cursor: toolsResult.nextCursor }).catch(this._capabilityErrorHandler({ tools: [] }, "tools/list")); toolsAgg = toolsAgg.concat(toolsResult.tools); } while (toolsResult.nextCursor); return toolsAgg; } async fetchResources() { let resourcesAgg = []; let resourcesResult = { resources: [] }; do { resourcesResult = await this.client.listResources({ cursor: resourcesResult.nextCursor }).catch(this._capabilityErrorHandler({ resources: [] }, "resources/list")); resourcesAgg = resourcesAgg.concat(resourcesResult.resources); } while (resourcesResult.nextCursor); return resourcesAgg; } async fetchPrompts() { let promptsAgg = []; let promptsResult = { prompts: [] }; do { promptsResult = await this.client.listPrompts({ cursor: promptsResult.nextCursor }).catch(this._capabilityErrorHandler({ prompts: [] }, "prompts/list")); promptsAgg = promptsAgg.concat(promptsResult.prompts); } while (promptsResult.nextCursor); return promptsAgg; } async fetchResourceTemplates() { let templatesAgg = []; let templatesResult = { resourceTemplates: [] }; do { templatesResult = await this.client.listResourceTemplates({ cursor: templatesResult.nextCursor }).catch(this._capabilityErrorHandler({ resourceTemplates: [] }, "resources/templates/list")); templatesAgg = templatesAgg.concat(templatesResult.resourceTemplates); } while (templatesResult.nextCursor); return templatesAgg; } /** * Handle elicitation request from server * Automatically uses the Agent's built-in elicitation handling if available */ async handleElicitationRequest(_request) { throw new Error("Elicitation handler must be implemented for your platform. Override handleElicitationRequest method."); } /** * Get the transport for the client * @param transportType - The transport type to get * @returns The transport for the client */ getTransport(transportType) { switch (transportType) { case "streamable-http": return new StreamableHTTPClientTransport(this.url, this.options.transport); case "sse": return new SSEClientTransport(this.url, this.options.transport); default: throw new Error(`Unsupported transport type: ${transportType}`); } } async tryConnect(transportType) { const transports = transportType === "auto" ? ["streamable-http", "sse"] : [transportType]; for (const currentTransportType of transports) { const isLastTransport = currentTransportType === transports[transports.length - 1]; const hasFallback = transportType === "auto" && currentTransportType === "streamable-http" && !isLastTransport; const transport = this.getTransport(currentTransportType); try { await this.client.connect(transport); return { state: MCPConnectionState.CONNECTED, transport: currentTransportType }; } catch (e) { const error = e instanceof Error ? e : new Error(String(e)); if (isUnauthorized(error)) return { state: MCPConnectionState.AUTHENTICATING }; if (isTransportNotImplemented(error) && hasFallback) continue; return { state: MCPConnectionState.FAILED, error }; } } return { state: MCPConnectionState.FAILED, error: /* @__PURE__ */ new Error("No transports available") }; } _capabilityErrorHandler(empty, method) { return (e) => { if (e.code === -32601) { const url = this.url.toString(); this._onObservabilityEvent.fire({ type: "mcp:client:discover", displayMessage: `The server advertised support for the capability ${method.split("/")[0]}, but returned "Method not found" for '${method}' for ${url}`, payload: { url, capability: method.split("/")[0], error: toErrorMessage(e) }, timestamp: Date.now(), id: nanoid() }); return empty; } throw e; }; } }; //#endregion //#region src/mcp/client.ts const defaultClientOptions = { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() }; /** * Utility class that aggregates multiple MCP clients into one */ var MCPClientManager = class { /** * @param _name Name of the MCP client * @param _version Version of the MCP Client * @param options Storage adapter for persisting MCP server state */ constructor(_name, _version, options) { this._name = _name; this._version = _version; this.mcpConnections = {}; this._didWarnAboutUnstableGetAITools = false; this._connectionDisposables = /* @__PURE__ */ new Map(); this._isRestored = false; this._onObservabilityEvent = new Emitter(); this.onObservabilityEvent = this._onObservabilityEvent.event; this._onServerStateChanged = new Emitter(); this.onServerStateChanged = this._onServerStateChanged.event; if (!options.storage) throw new Error("MCPClientManager requires a valid DurableObjectStorage instance"); this._storage = options.storage; } sql(query, ...bindings) { return [...this._storage.sql.exec(query, ...bindings)]; } saveServerToStorage(server) { this.sql(`INSERT OR REPLACE INTO cf_agents_mcp_servers ( id, name, server_url, client_id, auth_url, callback_url, server_options ) VALUES (?, ?, ?, ?, ?, ?, ?)`, server.id, server.name, server.server_url, server.client_id ?? null, server.auth_url ?? null, server.callback_url, server.server_options ?? null); } removeServerFromStorage(serverId) { this.sql("DELETE FROM cf_agents_mcp_servers WHERE id = ?", serverId); } getServersFromStorage() { return this.sql("SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers"); } clearServerAuthUrl(serverId) { this.sql("UPDATE cf_agents_mcp_servers SET auth_url = NULL WHERE id = ?", serverId); } failConnection(serverId, error) { this.clearServerAuthUrl(serverId); if (this.mcpConnections[serverId]) { this.mcpConnections[serverId].connectionState = MCPConnectionState.FAILED; this.mcpConnections[serverId].connectionError = error; } this._onServerStateChanged.fire(); return { serverId, authSuccess: false, authError: error }; } /** * Create an auth provider for a server * @internal */ createAuthProvider(serverId, callbackUrl, clientName, clientId) { if (!this._storage) throw new Error("Cannot create auth provider: storage is not initialized"); const authProvider = new DurableObjectOAuthClientProvider(this._storage, clientName, callbackUrl); authProvider.serverId = serverId; if (clientId) authProvider.clientId = clientId; return authProvider; } /** * Restore MCP server connections from storage * This method is called on Agent initialization to restore previously connected servers * * @param clientName Name to use for OAuth client (typically the agent instance name) */ async restoreConnectionsFromStorage(clientName) { if (this._isRestored) return; const servers = this.getServersFromStorage(); if (!servers || servers.length === 0) { this._isRestored = true; return; } for (const server of servers) { const existingConn = this.mcpConnections[server.id]; if (existingConn) { if (existingConn.connectionState === MCPConnectionState.READY) { console.warn(`[MCPClientManager] Server ${server.id} already has a ready connection. Skipping recreation.`); continue; } if (existingConn.connectionState === MCPConnectionState.AUTHENTICATING || existingConn.connectionState === MCPConnectionState.CONNECTING || existingConn.connectionState === MCPConnectionState.DISCOVERING) continue; if (existingConn.connectionState === MCPConnectionState.FAILED) { try { await existingConn.client.close(); } catch (error) { console.warn(`[MCPClientManager] Error closing failed connection ${server.id}:`, error); } delete this.mcpConnections[server.id]; this._connectionDisposables.get(server.id)?.dispose(); this._connectionDisposables.delete(server.id); } } const parsedOptions = server.server_options ? JSON.parse(server.server_options) : null; const authProvider = this.createAuthProvider(server.id, server.callback_url, clientName, server.client_id ?? void 0); const conn = this.createConnection(server.id, server.server_url, { client: parsedOptions?.client ?? {}, transport: { ...parsedOptions?.transport ?? {}, type: parsedOptions?.transport?.type ?? "auto", authProvider } }); if (server.auth_url) { conn.connectionState = MCPConnectionState.AUTHENTICATING; continue; } this._restoreServer(server.id); } this._isRestored = true; } /** * Internal method to restore a single server connection and discovery */ async _restoreServer(serverId) { if ((await this.connectToServer(serverId).catch((error) => { console.error(`Error connecting to ${serverId}:`, error); return null; }))?.state === MCPConnectionState.CONNECTED) { const discoverResult = await this.discoverIfConnected(serverId); if (discoverResult && !discoverResult.success) console.error(`Error discovering ${serverId}:`, discoverResult.error); } } /** * Connect to and register an MCP server * * @deprecated This method is maintained for backward compatibility. * For new code, use registerServer() and connectToServer() separately. * * @param url Server URL * @param options Connection options * @returns Object with server ID, auth URL (if OAuth), and client ID (if OAuth) */ async connect(url, options = {}) { /** * We need to delay loading ai sdk, because putting it in module scope is * causing issues with startup time. * The only place it's used is in getAITools, which only matters after * .connect() is called on at least one server. * So it's safe to delay loading it until .connect() is called. */ await this.ensureJsonSchema(); const id = options.reconnect?.id ?? nanoid(8); if (options.transport?.authProvider) { options.transport.authProvider.serverId = id; if (options.reconnect?.oauthClientId) options.transport.authProvider.clientId = options.reconnect?.oauthClientId; } if (!options.reconnect?.oauthCode || !this.mcpConnections[id]) { const normalizedTransport = { ...options.transport, type: options.transport?.type ?? "auto" }; this.mcpConnections[id] = new MCPClientConnection(new URL(url), { name: this._name, version: this._version }, { client: options.client ?? {}, transport: normalizedTransport }); const store = new DisposableStore(); const existing = this._connectionDisposables.get(id); if (existing) existing.dispose(); this._connectionDisposables.set(id, store); store.add(this.mcpConnections[id].onObservabilityEvent((event) => { this._onObservabilityEvent.fire(event); })); } await this.mcpConnections[id].init(); if (options.reconnect?.oauthCode) try { await this.mcpConnections[id].completeAuthorization(options.reconnect.oauthCode); await this.mcpConnections[id].init(); } catch (error) { this._onObservabilityEvent.fire({ type: "mcp:client:connect", displayMessage: `Failed to complete OAuth reconnection for ${id} for ${url}`, payload: { url, transport: options.transport?.type ?? "auto", state: this.mcpConnections[id].connectionState, error: toErrorMessage(error) }, timestamp: Date.now(), id }); throw error; } const authUrl = options.transport?.authProvider?.authUrl; if (this.mcpConnections[id].connectionState === MCPConnectionState.AUTHENTICATING && authUrl && options.transport?.authProvider?.redirectUrl) return { authUrl, clientId: options.transport?.authProvider?.clientId, id }; const discoverResult = await this.discoverIfConnected(id); if (discoverResult && !discoverResult.success) throw new Error(`Failed to discover server capabilities: ${discoverResult.error}`); return { id }; } /** * Create an in-memory connection object and set up observability * Does NOT save to storage - use registerServer() for that * @returns The connection object (existing or newly created) */ createConnection(id, url, options) { if (this.mcpConnections[id]) return this.mcpConnections[id]; const normalizedTransport = { ...options.transport, type: options.transport?.type ?? "auto" }; this.mcpConnections[id] = new MCPClientConnection(new URL(url), { name: this._name, version: this._version }, { client: { ...defaultClientOptions, ...options.client }, transport: normalizedTransport }); const store = new DisposableStore(); const existing = this._connectionDisposables.get(id); if (existing) existing.dispose(); this._connectionDisposables.set(id, store); store.add(this.mcpConnections[id].onObservabilityEvent((event) => { this._onObservabilityEvent.fire(event); })); return this.mcpConnections[id]; } /** * Register an MCP server connection without connecting * Creates the connection object, sets up observability, and saves to storage * * @param id Server ID * @param options Registration options including URL, name, callback URL, and connection config * @returns Server ID */ async registerServer(id, options) { this.createConnection(id, options.url, { client: options.client, transport: { ...options.transport, type: options.transport?.type ?? "auto" } }); const { authProvider: _, ...transportWithoutAuth } = options.transport ?? {}; this.saveServerToStorage({ id, name: options.name, server_url: options.url, callback_url: options.callbackUrl, client_id: options.clientId ?? null, auth_url: options.authUrl ?? null, server_options: JSON.stringify({ client: options.client, transport: transportWithoutAuth }) }); this._onServerStateChanged.fire(); return id; } /** * Connect to an already registered MCP server and initialize the connection. * * For OAuth servers, returns `{ state: "authenticating", authUrl, clientId? }`. * The user must complete the OAuth flow via the authUrl, which triggers a * callback handled by `handleCallbackRequest()`. * * For non-OAuth servers, establishes the transport connection and returns * `{ state: "connected" }`. Call `discoverIfConnected()` afterwards to * discover capabilities and transition to "ready" state. * * @param id Server ID (must be registered first via registerServer()) * @returns Connection result with current state and OAuth info (if applicable) */ async connectToServer(id) { const conn = this.mcpConnections[id]; if (!conn) throw new Error(`Server ${id} is not registered. Call registerServer() first.`); const error = await conn.init(); this._onServerStateChanged.fire(); switch (conn.connectionState) { case MCPConnectionState.FAILED: return { state: conn.connectionState, error: error ?? "Unknown connection error" }; case MCPConnectionState.AUTHENTICATING: { const authUrl = conn.options.transport.authProvider?.authUrl; const redirectUrl = conn.options.transport.authProvider?.redirectUrl; if (!authUrl || !redirectUrl) return { state: MCPConnectionState.FAILED, error: `OAuth configuration incomplete: missing ${!authUrl ? "authUrl" : "redirectUrl"}` }; const clientId = conn.options.transport.authProvider?.clientId; const serverRow = this.getServersFromStorage().find((s) => s.id === id); if (serverRow) { this.saveServerToStorage({ ...serverRow, auth_url: authUrl, client_id: clientId ?? null }); this._onServerStateChanged.fire(); } return { state: conn.connectionState, authUrl, clientId }; } case MCPConnectionState.CONNECTED: return { state: conn.connectionState }; default: return { state: MCPConnectionState.FAILED, error: `Unexpected connection state after init: ${conn.connectionState}` }; } } extractServerIdFromState(state) { if (!state) return null; const parts = state.split("."); return parts.length === 2 ? parts[1] : null; } isCallbackRequest(req) { if (req.method !== "GET") return false; if (!req.url.includes("/callback")) return false; const state = new URL(req.url).searchParams.get("state"); const serverId = this.extractServerIdFromState(state); if (!serverId) return false; return this.getServersFromStorage().some((server) => server.id === serverId); } async handleCallbackRequest(req) { const url = new URL(req.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); const error = url.searchParams.get("error"); const errorDescription = url.searchParams.get("error_description"); if (!state) throw new Error("Unauthorized: no state provided"); const serverId = this.extractServerIdFromState(state); if (!serverId) throw new Error("No serverId found in state parameter. Expected format: {nonce}.{serverId}"); if (!this.getServersFromStorage().some((server) => server.id === serverId)) throw new Error(`No server found with id "${serverId}". Was the request matched with \`isCallbackRequest()\`?`); if (this.mcpConnections[serverId] === void 0) throw new Error(`Could not find serverId: ${serverId}`); const conn = this.mcpConnections[serverId]; try { if (!conn.options.transport.authProvider) throw new Error("Trying to finalize authentication for a server connection without an authProvider"); const authProvider = conn.options.transport.authProvider; authProvider.serverId = serverId; const stateValidation = await authProvider.checkState(state); if (!stateValidation.valid) throw new Error(stateValidation.error || "Invalid state"); if (error) throw new Error(escapeHtml(errorDescription || error)); if (!code) throw new Error("Unauthorized: no code provided"); if (conn.connectionState === MCPConnectionState.READY || conn.connectionState === MCPConnectionState.CONNECTED) { this.clearServerAuthUrl(serverId); return { serverId, authSuccess: true }; } if (conn.connectionState !== MCPConnectionState.AUTHENTICATING) throw new Error(`Failed to authenticate: the client is in "${conn.connectionState}" state, expected "authenticating"`); await authProvider.consumeState(state); await conn.completeAuthorization(code); await authProvider.deleteCodeVerifier(); this.clearServerAuthUrl(serverId); conn.connectionError = null; this._onServerStateChanged.fire(); return { serverId, authSuccess: true }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return this.failConnection(serverId, message); } } /** * Discover server capabilities if connection is in CONNECTED or READY state. * Transitions to DISCOVERING then READY (or CONNECTED on error). * Can be called to refresh server capabilities (e.g., from a UI refresh button). * * If called while a previous discovery is in-flight for the same server, * the previous discovery will be aborted. * * @param serverId The server ID to discover * @param options Optional configuration * @param options.timeoutMs Timeout in milliseconds (default: 30000) * @returns Result with current state and optional error, or undefined if connection not found */ async discoverIfConnected(serverId, options = {}) { const conn = this.mcpConnections[serverId]; if (!conn) { this._onObservabilityEvent.fire({ type: "mcp:client:discover", displayMessage: `Connection not found for ${serverId}`, payload: {}, timestamp: Date.now(), id: nanoid() }); return; } const result = await conn.discover(options); this._onServerStateChanged.fire(); return { ...result, state: conn.connectionState }; } /** * Establish connection in the background after OAuth completion * This method connects to the server and discovers its capabilities * @param serverId The server ID to establish connection for */ async establishConnection(serverId) { const conn = this.mcpConnections[serverId]; if (!conn) { this._onObservabilityEvent.fire({ type: "mcp:client:preconnect", displayMessage: `Connection not found for serverId: ${serverId}`, payload: { serverId }, timestamp: Date.now(), id: nanoid() }); return; } if (conn.connectionState === MCPConnectionState.DISCOVERING || conn.connectionState === MCPConnectionState.READY) { this._onObservabilityEvent.fire({ type: "mcp:client:connect", displayMessage: `establishConnection skipped for ${serverId}, already in ${conn.connectionState} state`, payload: { url: conn.url.toString(), transport: conn.options.transport.type || "unknown", state: conn.connectionState }, timestamp: Date.now(), id: nanoid() }); return; } const connectResult = await this.connectToServer(serverId); this._onServerStateChanged.fire(); if (connectResult.state === MCPConnectionState.CONNECTED) await this.discoverIfConnected(serverId); this._onObservabilityEvent.fire({ type: "mcp:client:connect", displayMessage: `establishConnection completed for ${serverId}, final state: ${conn.connectionState}`, payload: { url: conn.url.toString(), transport: conn.options.transport.type || "unknown", state: conn.connectionState }, timestamp: Date.now(), id: nanoid() }); } /** * Configure OAuth callback handling * @param config OAuth callback configuration */ configureOAuthCallback(config) { this._oauthCallbackConfig = config; } /** * Get the current OAuth callback configuration * @returns The current OAuth callback configuration */ getOAuthCallbackConfig() { return this._oauthCallbackConfig; } /** * @returns namespaced list of tools */ listTools() { return getNamespacedData(this.mcpConnections, "tools"); } /** * Lazy-loads the jsonSchema function from the AI SDK. * * This defers importing the "ai" package until it's actually needed, which helps reduce * initial bundle size and startup time. The jsonSchema function is required for converting * MCP tools into AI SDK tool definitions via getAITools(). * * @internal This method is for internal use only. It's automatically called before operations * that need jsonSchema (like getAITools() or OAuth flows). External consumers should not need * to call this directly. */ async ensureJsonSchema() { if (!this.jsonSchema) { const { jsonSchema } = await import("ai"); this.jsonSchema = jsonSchema; } } /** * @returns a set of tools that you can use with the AI SDK */ getAITools() { if (!this.jsonSchema) throw new Error("jsonSchema not initialized."); for (const [id, conn] of Object.entries(this.mcpConnections)) if (conn.connectionState !== MCPConnectionState.READY && conn.connectionState !== MCPConnectionState.AUTHENTICATING) console.warn(`[getAITools] WARNING: Reading tools from connection ${id} in state "${conn.connectionState}". Tools may not be loaded yet.`); return Object.fromEntries(getNamespacedData(this.mcpConnections, "tools").map((tool) => { return [`tool_${tool.serverId.replace(/-/g, "")}_${tool.name}`, { description: tool.description, execute: async (args) => { const result = await this.callTool({ arguments: args, name: tool.name, serverId: tool.serverId }); if (result.isError) { const textContent = result.content?.[0]; const message = textContent?.type === "text" && textContent.text ? textContent.text : "Tool call failed"; throw new Error(message); } return result; }, inputSchema: this.jsonSchema(tool.inputSchema), outputSchema: tool.outputSchema ? this.jsonSchema(tool.outputSchema) : void 0 }]; })); } /** * @deprecated this has been renamed to getAITools(), and unstable_getAITools will be removed in the next major version * @returns a set of tools that you can use with the AI SDK */ unstable_getAITools() { if (!this._didWarnAboutUnstableGetAITools) { this._didWarnAboutUnstableGetAITools = true; console.warn("unstable_getAITools is deprecated, use getAITools instead. unstable_getAITools will be removed in the next major version."); } return this.getAITools(); } /** * Closes all active in-memory connections to MCP servers. * * Note: This only closes the transport connections - it does NOT remove * servers from storage. Servers will still be listed and their callback * URLs will still match incoming OAuth requests. * * Use removeServer() instead if you want to fully clean up a server * (closes connection AND removes from storage). */ async closeAllConnections() { const ids = Object.keys(this.mcpConnections); for (const id of ids) this.mcpConnections[id].cancelDiscovery(); await Promise.all(ids.map(async (id) => { await this.mcpConnections[id].client.close(); })); for (const id of ids) { const store = this._connectionDisposables.get(id); if (store) store.dispose(); this._connectionDisposables.delete(id); delete this.mcpConnections[id]; } } /** * Closes a connection to an MCP server * @param id The id of the connection to close */ async closeConnection(id) { if (!this.mcpConnections[id]) throw new Error(`Connection with id "${id}" does not exist.`); this.mcpConnections[id].cancelDiscovery(); await this.mcpConnections[id].client.close(); delete this.mcpConnections[id]; const store = this._connectionDisposables.get(id); if (store) store.dispose(); this._connectionDisposables.delete(id); } /** * Remove an MCP server - closes connection if active and removes from storage. */ async removeServer(serverId) { if (this.mcpConnections[serverId]) try { await this.closeConnection(serverId); } catch (_e) {} this.removeServerFromStorage(serverId); this._onServerStateChanged.fire(); } /** * List all MCP servers from storage */ listServers() { return this.getServersFromStorage(); } /** * Dispose the manager and all resources. */ async dispose() { try { await this.closeAllConnections(); } finally { this._onServerStateChanged.dispose(); this._onObservabilityEvent.dispose(); } } /** * @returns namespaced list of prompts */ listPrompts() { return getNamespacedData(this.mcpConnections, "prompts"); } /** * @returns namespaced list of tools */ listResources() { return getNamespacedData(this.mcpConnections, "resources"); } /** * @returns namespaced list of resource templates */ listResourceTemplates() { return getNamespacedData(this.mcpConnections, "resourceTemplates"); } /** * Namespaced version of callTool */ async callTool(params, resultSchema, options) { const unqualifiedName = params.name.replace(`${params.serverId}.`, ""); return this.mcpConnections[params.serverId].client.callTool({ ...params, name: unqualifiedName }, resultSchema, options); } /** * Namespaced version of readResource */ readResource(params, options) { return this.mcpConnections[params.serverId].client.readResource(params, options); } /** * Namespaced version of getPrompt */ getPrompt(params, options) { return this.mcpConnections[params.serverId].client.getPrompt(params, options); } }; function getNamespacedData(mcpClients, type) { return Object.entries(mcpClients).map(([name, conn]) => { return { data: conn[type], name }; }).flatMap(({ name: serverId, data }) => { return data.map((item) => { return { ...item, serverId }; }); }); } //#endregion export { DisposableStore as i, getNamespacedData as n, MCPConnectionState as r, MCPClientManager as t }; //# sourceMappingURL=client-CtC9E06G.js.map