import { t as DurableObjectOAuthClientProvider } from "./do-oauth-client-provider-B1fVIshX.js";
import { nanoid } from "nanoid";
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 isUnauthorized(error) {
const msg = toErrorMessage(error);
return msg.includes("Unauthorized") || msg.includes("401");
}
function isTransportNotImplemented(error) {
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.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);
}
/**
* 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];
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(`Invalid state: ${stateValidation.error}`);
if (error) return {
serverId,
authSuccess: false,
authError: errorDescription || error
};
if (!code) throw new Error("Unauthorized: no code provided");
if (this.mcpConnections[serverId].connectionState === MCPConnectionState.READY || this.mcpConnections[serverId].connectionState === MCPConnectionState.CONNECTED) {
this.clearServerAuthUrl(serverId);
return {
serverId,
authSuccess: true
};
}
if (this.mcpConnections[serverId].connectionState !== MCPConnectionState.AUTHENTICATING) throw new Error(`Failed to authenticate: the client is in "${this.mcpConnections[serverId].connectionState}" state, expected "authenticating"`);
try {
await authProvider.consumeState(state);
await conn.completeAuthorization(code);
await authProvider.deleteCodeVerifier();
this.clearServerAuthUrl(serverId);
this._onServerStateChanged.fire();
return {
serverId,
authSuccess: true
};
} catch (authError) {
const errorMessage = authError instanceof Error ? authError.message : String(authError);
this._onServerStateChanged.fire();
return {
serverId,
authSuccess: false,
authError: errorMessage
};
}
}
/**
* 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) throw new Error(result.content[0].text);
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-QZa2Rq0l.js.map