agents
Version:
A home for your AI agents
2,385 lines • 85.9 kB
JavaScript
import { tryN } from "./retries.js";
import { n as callV2Tool } from "./client-invoker-VNZ7X0nn.js";
import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider.js";
import { nanoid } from "nanoid";
import { getServerByName } from "partyserver";
import { z } from "zod";
import { Client, SSEClientTransport, SdkHttpError, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/client/validators/cf-worker";
import { JSONRPCMessageSchema, isJSONRPCErrorResponse, isJSONRPCResultResponse } 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/abort.ts
function abortError(signal) {
return signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason ?? "Aborted"));
}
/**
* Stop awaiting an operation when its owner aborts. The underlying operation
* remains responsible for observing the same signal and cancelling its work.
*/
async function raceWithSignal(promise, signal) {
if (!signal) return promise;
if (signal.aborted) throw abortError(signal);
return new Promise((resolve, reject) => {
const onAbort = () => reject(abortError(signal));
signal.addEventListener("abort", onAbort, { once: true });
promise.then(resolve, reject).finally(() => {
signal.removeEventListener("abort", onAbort);
});
});
}
//#endregion
//#region src/mcp/client-catalog.ts
async function fetchMcpTools(client, options) {
let aggregate = [];
let page = { tools: [] };
do {
const params = { cursor: page.nextCursor };
page = await (options.probing ? client.request({
method: "tools/list",
params
}) : client.listTools(params)).catch(options.onCapabilityError({ tools: [] }, "tools/list"));
aggregate = aggregate.concat(page.tools);
} while (page.nextCursor);
return aggregate;
}
async function fetchMcpResources(client, options) {
let aggregate = [];
let page = { resources: [] };
do {
const params = { cursor: page.nextCursor };
page = await (options.probing ? client.request({
method: "resources/list",
params
}) : client.listResources(params)).catch(options.onCapabilityError({ resources: [] }, "resources/list"));
aggregate = aggregate.concat(page.resources);
} while (page.nextCursor);
return aggregate;
}
async function fetchMcpPrompts(client, options) {
let aggregate = [];
let page = { prompts: [] };
do {
const params = { cursor: page.nextCursor };
page = await (options.probing ? client.request({
method: "prompts/list",
params
}) : client.listPrompts(params)).catch(options.onCapabilityError({ prompts: [] }, "prompts/list"));
aggregate = aggregate.concat(page.prompts);
} while (page.nextCursor);
return aggregate;
}
async function fetchMcpResourceTemplates(client, options) {
let aggregate = [];
let page = { resourceTemplates: [] };
do {
const params = { cursor: page.nextCursor };
page = await (options.probing ? client.request({
method: "resources/templates/list",
params
}) : client.listResourceTemplates(params)).catch(options.onCapabilityError({ resourceTemplates: [] }, "resources/templates/list"));
aggregate = aggregate.concat(page.resourceTemplates);
} while (page.nextCursor);
return aggregate;
}
//#endregion
//#region src/mcp/client-runtime.ts
var CompatibleWorkerJsonSchemaValidator = class extends CfWorkerJsonSchemaValidator {
constructor(..._args) {
super(..._args);
this.legacy = new CfWorkerJsonSchemaValidator({ draft: "7" });
}
getValidator(schema) {
const dialect = schema.$schema;
return typeof dialect === "string" && /draft-0?7/i.test(dialect) ? this.legacy.getValidator(schema) : super.getValidator(schema);
}
};
const DEFAULT_CLIENT_OPTIONS = {
jsonSchemaValidator: new CompatibleWorkerJsonSchemaValidator(),
versionNegotiation: { mode: "auto" },
inputRequired: { autoFulfill: true }
};
function normalizeMcpClientOptions(options) {
return {
...DEFAULT_CLIENT_OPTIONS,
...options,
versionNegotiation: {
...DEFAULT_CLIENT_OPTIONS.versionNegotiation,
...options?.versionNegotiation
},
inputRequired: {
...DEFAULT_CLIENT_OPTIONS.inputRequired,
...options?.inputRequired
}
};
}
function elicitationCapabilitiesFromHandlers(handlers) {
if (!handlers) return void 0;
const elicitation = {};
if (handlers.form) elicitation.form = {};
if (handlers.url) elicitation.url = {};
return elicitation.form || elicitation.url ? elicitation : void 0;
}
function listChangedHandlers(configured, callbacks) {
return {
tools: {
...configured?.tools,
onChanged: (error, tools) => {
callbacks.tools(error, tools);
configured?.tools?.onChanged(error, tools);
}
},
prompts: {
...configured?.prompts,
onChanged: (error, prompts) => {
callbacks.prompts(error, prompts);
configured?.prompts?.onChanged(error, prompts);
}
},
resources: {
...configured?.resources,
onChanged: (error, resources) => {
callbacks.resources(error, resources);
configured?.resources?.onChanged(error, resources);
}
}
};
}
function createMcpSdkClient(info, options, capabilitySeed, handlerModes, callbacks) {
const elicitation = options.capabilities?.elicitation ?? elicitationCapabilitiesFromHandlers(handlerModes) ?? capabilitySeed?.elicitation;
return {
client: new Client(info, {
...options,
capabilities: {
...capabilitySeed,
...options.capabilities,
...elicitation ? { elicitation } : {}
},
listChanged: listChangedHandlers(options.listChanged, callbacks)
}),
elicitationEnabled: elicitation !== void 0
};
}
//#endregion
//#region src/mcp/errors.ts
function toErrorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
function getErrorStatus(error) {
if (!error || typeof error !== "object") return void 0;
const record = error;
if (typeof record.code === "number") return record.code;
if (typeof record.status === "number") return record.status;
if (typeof record.data?.status === "number") return record.data.status;
}
function getErrorCause(error) {
if (!error || typeof error !== "object") return void 0;
return error.cause ?? error.data?.cause;
}
function isUnauthorized(error) {
if (getErrorStatus(error) === 401) return true;
const cause = getErrorCause(error);
if (cause && cause !== error && isUnauthorized(cause)) return true;
const msg = toErrorMessage(error);
return msg.includes("Unauthorized") || msg.includes("401");
}
function isTransportNotImplemented(error) {
const status = getErrorStatus(error);
if (status === 404 || status === 405) return true;
const cause = getErrorCause(error);
if (cause && cause !== error && isTransportNotImplemented(cause)) return true;
const msg = toErrorMessage(error);
return msg.includes("404") || msg.includes("405") || msg.includes("Error POSTing to endpoint: Not Found") || msg.includes("Not Implemented") || msg.includes("not implemented");
}
//#endregion
//#region src/mcp/rpc.ts
const RPC_DO_PREFIX = "rpc:";
function makeInvalidRequestError(id) {
return {
jsonrpc: "2.0",
id: id ?? null,
error: {
code: -32600,
message: "Invalid Request"
}
};
}
function validateBatch(batch) {
if (batch.length === 0) throw new Error("Invalid JSON-RPC batch: array must not be empty");
}
var RPCClientTransport = class {
constructor(options) {
this._started = false;
this._namespace = options.namespace;
this._name = options.name;
this._props = options.props;
}
setProtocolVersion(version) {
this._protocolVersion = version;
}
getProtocolVersion() {
return this._protocolVersion;
}
async start() {
if (this._started) throw new Error("Transport already started");
const doName = `${RPC_DO_PREFIX}${this._name}`;
this._stub = await getServerByName(this._namespace, doName, { props: this._props });
this._started = true;
}
async close() {
this._started = false;
this._stub = void 0;
this.onclose?.();
}
async send(message, options) {
if (!this._started || !this._stub) throw new Error("Transport not started");
try {
const result = await raceWithSignal(this._stub.handleMcpMessage(message), options?.requestSignal);
if (!result || options?.requestSignal?.aborted) return;
const extra = void 0;
const messages = Array.isArray(result) ? result : [result];
for (const msg of messages) this.onmessage?.(msg, extra);
} catch (error) {
this.onerror?.(error);
throw error;
}
}
};
var RPCServerTransport = class {
constructor(options) {
this._started = false;
this._pendingRequests = /* @__PURE__ */ new Map();
this._pendingContinuations = [];
this._timeout = options?.timeout ?? 6e4;
}
setProtocolVersion(version) {
this._protocolVersion = version;
}
getProtocolVersion() {
return this._protocolVersion;
}
async start() {
if (this._started) throw new Error("Transport already started");
this._started = true;
}
async close() {
this._started = false;
this.onclose?.();
const error = /* @__PURE__ */ new Error("Transport closed");
for (const pending of this._pendingRequests.values()) {
clearTimeout(pending.timeoutId);
pending.reject(error);
}
this._pendingRequests.clear();
for (const pending of this._pendingContinuations) {
clearTimeout(pending.timeoutId);
pending.reject(error);
}
this._pendingContinuations = [];
}
_makeTimeout(onTimeout) {
return setTimeout(onTimeout, this._timeout);
}
_appendPending(pending, message) {
pending.messages.push(message);
}
_completePending(pending, message) {
pending.messages.push(message);
clearTimeout(pending.timeoutId);
const messages = pending.messages;
queueMicrotask(() => {
pending.resolve(messages.length === 1 ? messages[0] : messages);
});
}
_completeRequest(key, message) {
const pending = this._pendingRequests.get(key);
if (!pending) return false;
this._pendingRequests.delete(key);
this._completePending(pending, message);
return true;
}
_appendRequest(key, message) {
const pending = this._pendingRequests.get(key);
if (!pending) return false;
this._appendPending(pending, message);
return true;
}
_completeContinuation(message) {
const pending = this._pendingContinuations.shift();
if (!pending) return false;
this._completePending(pending, message);
return true;
}
_appendContinuation(message) {
const pending = this._pendingContinuations[0];
if (!pending) return false;
this._appendPending(pending, message);
return true;
}
async send(message, options) {
if (!this._started) throw new Error("Transport not started");
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
const id = message.id;
if (id === void 0) {
this.onerror?.(/* @__PURE__ */ new Error(`RPC response missing id: ${JSON.stringify(message)}`));
return;
}
if (this._completeRequest(id.toString(), message)) return;
if (this._completeContinuation(message)) return;
this.onerror?.(/* @__PURE__ */ new Error(`No pending RPC request found for response: ${JSON.stringify(message)}`));
return;
}
const relatedRequestId = options?.relatedRequestId?.toString();
const expectsResponse = "id" in message;
if (relatedRequestId) {
if (expectsResponse) {
if (this._completeRequest(relatedRequestId, message)) return;
} else if (this._appendRequest(relatedRequestId, message)) return;
}
if (expectsResponse) {
if (this._completeContinuation(message)) return;
} else if (this._appendContinuation(message)) return;
this.onerror?.(/* @__PURE__ */ new Error(`No pending RPC request found for message: ${JSON.stringify(message)}`));
}
/**
* @internal Called by McpAgent.handleMcpMessage() — not for external use.
*
* Wait for the next unmatched send() call that expects a client response or
* completes a resumed tool call.
*
* Used after resolving an elicitation response: the original tool call has
* already returned the elicitation request to the RPC client, and the resumed
* tool handler will eventually send the final tool result. That final response
* has the original tool request id, so there is no active handle() waiter left
* for id-based routing; this continuation waiter receives it instead.
*/
async _awaitPendingResponse() {
if (!this._started) throw new Error("Transport not started");
return await new Promise((resolve, reject) => {
const pending = {
messages: [],
resolve,
reject,
timeoutId: this._makeTimeout(() => {
const index = this._pendingContinuations.indexOf(pending);
if (index !== -1) this._pendingContinuations.splice(index, 1);
reject(/* @__PURE__ */ new Error(`Request timeout: No response received within ${this._timeout}ms`));
})
};
this._pendingContinuations.push(pending);
});
}
async handle(message) {
if (!this._started) throw new Error("Transport not started");
if (Array.isArray(message)) {
validateBatch(message);
const flattened = (await Promise.all(message.map((msg) => this.handle(msg)))).flatMap((response) => {
if (response === void 0) return [];
return Array.isArray(response) ? response : [response];
});
return flattened.length === 0 ? void 0 : flattened;
}
try {
JSONRPCMessageSchema.parse(message);
} catch {
return makeInvalidRequestError(typeof message === "object" && message !== null && "id" in message ? message.id : null);
}
if (!("id" in message)) {
this.onmessage?.(message);
return;
}
const id = message.id?.toString();
if (!id) return makeInvalidRequestError(message.id);
if (this._pendingRequests.has(id)) throw new Error(`Duplicate pending RPC request id: ${id}`);
const responsePromise = new Promise((resolve, reject) => {
const pending = {
messages: [],
resolve,
reject,
timeoutId: this._makeTimeout(() => {
this._pendingRequests.delete(id);
reject(/* @__PURE__ */ new Error(`Request timeout: No response received within ${this._timeout}ms`));
})
};
this._pendingRequests.set(id, pending);
});
this.onmessage?.(message);
return await responsePromise;
}
};
//#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 = {
/** Waiting for OAuth authorization to complete */
AUTHENTICATING: "authenticating",
/** Establishing transport connection to MCP server */
CONNECTING: "connecting",
/** Transport connection established */
CONNECTED: "connected",
/** Discovering server capabilities (tools, resources, prompts) */
DISCOVERING: "discovering",
/** Fully connected and ready to use */
READY: "ready",
/** Connection failed at some point */
FAILED: "failed"
};
var MCPClientConnection = class {
constructor(url, _info, options = {
client: {},
transport: {}
}) {
this.url = url;
this._info = _info;
this.options = options;
this.connectionState = MCPConnectionState.CONNECTING;
this.connectionError = null;
this.tools = [];
this.prompts = [];
this.resources = [];
this.resourceTemplates = [];
this._probingCapabilities = false;
this._onObservabilityEvent = new Emitter();
this.onObservabilityEvent = this._onObservabilityEvent.event;
this._onListChanged = new Emitter();
this.onListChanged = this._onListChanged.event;
this._elicitationEnabled = false;
this.options = {
...options,
client: normalizeMcpClientOptions(options.client)
};
this.client = this.createClient();
}
createClient() {
const created = createMcpSdkClient(this._info, this.options.client, this.options.capabilitySeed, this.options.elicitationHandlers, {
tools: (error, tools) => {
if (!error && tools) this.tools = tools;
this._onListChanged.fire();
},
prompts: (error, prompts) => {
if (!error && prompts) this.prompts = prompts;
this._onListChanged.fire();
},
resources: (error, resources) => {
if (!error && resources) this.resources = resources;
this._onListChanged.fire();
}
});
this._elicitationEnabled = created.elicitationEnabled;
return created.client;
}
/**
* Configure the handler used for server-initiated elicitation requests.
*
* If the connection has not been initialized yet, rebuild the SDK client so
* handler-driven elicitation capabilities are reflected in the initial
* handshake. A rebuild (rather than `Client.registerCapabilities`) is
* required because SDK capability registration is merge-only — it cannot
* un-advertise a mode when handlers are cleared before connecting. Active
* connections keep their negotiated capabilities until they reconnect.
*/
configureElicitationHandlers(handlers) {
this.options.elicitationHandlers = handlers;
this.options.capabilitySeed = void 0;
if (!this._transport) this.client = this.createClient();
}
/**
* 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");
if (this._transport) {
this._transport = void 0;
try {
await this.client.close();
} catch {}
this.client = this.createClient();
}
const res = await this.tryConnect(transportType);
this.connectionState = res.state;
if (res.state === MCPConnectionState.CONNECTED && res.transport) {
if (this._elicitationEnabled) this.client.setRequestHandler("elicitation/create", async (request, context) => await this.handleElicitationRequest(request, context.mcpReq.signal));
this.lastConnectedTransport = res.transport;
this._onObservabilityEvent.fire({
type: "mcp:client:connect",
payload: {
url: this.url.toString(),
transport: res.transport,
state: this.connectionState
},
timestamp: Date.now()
});
return;
} else if (res.state === MCPConnectionState.FAILED && res.error) {
const errorMessage = toErrorMessage(res.error);
this._onObservabilityEvent.fire({
type: "mcp:client:connect",
payload: {
url: this.url.toString(),
transport: transportType,
state: this.connectionState,
error: errorMessage
},
timestamp: Date.now()
});
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(callbackParams) {
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) => {
const transport = this.getTransport(base);
let completed = false;
try {
if ("finishAuth" in transport && typeof transport.finishAuth === "function") {
await transport.finishAuth(callbackParams);
completed = true;
}
} finally {
if (typeof transport.close === "function") await transport.close().catch(() => {});
}
if (completed) this.client = this.createClient();
};
if (configuredType === "rpc") throw new Error("RPC transport does not support authentication");
const authTransport = this._pendingAuthTransport ?? this._transport;
this._pendingAuthTransport = void 0;
if (authTransport && "finishAuth" in authTransport && typeof authTransport.finishAuth === "function") {
let completed = false;
try {
await authTransport.finishAuth(callbackParams);
completed = true;
} finally {
if (typeof authTransport.close === "function") await authTransport.close().catch(() => {});
if (this._transport === authTransport) this._transport = void 0;
}
if (completed) this.client = this.createClient();
return;
}
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(callback, options = {}) {
const expectedState = options.alreadyAccepted ? MCPConnectionState.CONNECTING : MCPConnectionState.AUTHENTICATING;
if (this.connectionState !== expectedState) throw new Error(`Connection must be in ${expectedState} state to complete authorization`);
if (!options.alreadyAccepted) this.connectionState = MCPConnectionState.CONNECTING;
try {
const callbackParams = typeof callback === "string" ? new URLSearchParams({ code: callback }) : callback;
await this.finishAuthProbe(callbackParams);
} 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() {
const discoveredCapabilities = this.client.getServerCapabilities();
const shouldProbeCapabilities = !discoveredCapabilities && this.isResumedStreamableHttpSession();
this.serverCapabilities = discoveredCapabilities;
this._probingCapabilities = shouldProbeCapabilities;
if (!discoveredCapabilities && !shouldProbeCapabilities) 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 (discoveredCapabilities?.tools || shouldProbeCapabilities) {
operations.push(this.registerTools());
operationNames.push("tools");
}
if (discoveredCapabilities?.resources || shouldProbeCapabilities) {
operations.push(this.registerResources());
operationNames.push("resources");
}
if (discoveredCapabilities?.prompts || shouldProbeCapabilities) {
operations.push(this.registerPrompts());
operationNames.push("prompts");
}
if (discoveredCapabilities?.resources || shouldProbeCapabilities) {
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",
payload: {
url: this.url.toString(),
error: toErrorMessage(error)
},
timestamp: Date.now()
});
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",
payload: {
url: this.url.toString(),
state: this.connectionState
},
timestamp: Date.now()
});
return {
success: false,
reason: "error",
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",
payload: { url: this.url.toString() },
timestamp: Date.now()
});
return { success: true };
} catch (e) {
if (timeoutId !== void 0) clearTimeout(timeoutId);
this.connectionState = isUnauthorized(e) ? MCPConnectionState.AUTHENTICATING : MCPConnectionState.CONNECTED;
const error = e instanceof Error ? e.message : String(e);
return {
success: false,
reason: this._probingCapabilities && e instanceof SdkHttpError && e.status === 404 ? "stale-session" : "error",
error
};
} 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._probingCapabilities) this.client.setNotificationHandler("notifications/tools/list_changed", async () => {
this.tools = await this.fetchTools();
this._onListChanged.fire();
});
return this.fetchTools();
}
/**
* Notification handler registration for resources
* Should only be called if serverCapabilities.resources exists
*/
async registerResources() {
if (this._probingCapabilities) this.client.setNotificationHandler("notifications/resources/list_changed", async () => {
this.resources = await this.fetchResources();
this._onListChanged.fire();
});
return this.fetchResources();
}
/**
* Notification handler registration for prompts
* Should only be called if serverCapabilities.prompts exists
*/
async registerPrompts() {
if (this._probingCapabilities) this.client.setNotificationHandler("notifications/prompts/list_changed", async () => {
this.prompts = await this.fetchPrompts();
this._onListChanged.fire();
});
return this.fetchPrompts();
}
async registerResourceTemplates() {
return this.fetchResourceTemplates();
}
catalogFetchOptions() {
return {
probing: this._probingCapabilities,
onCapabilityError: this._capabilityErrorHandler.bind(this)
};
}
async fetchTools() {
return fetchMcpTools(this.client, this.catalogFetchOptions());
}
async fetchResources() {
return fetchMcpResources(this.client, this.catalogFetchOptions());
}
async fetchPrompts() {
return fetchMcpPrompts(this.client, this.catalogFetchOptions());
}
async fetchResourceTemplates() {
return fetchMcpResourceTemplates(this.client, this.catalogFetchOptions());
}
/**
* Handle elicitation request from server.
*
* Delegates to the `elicitationHandlers` connection option when provided.
*
* @deprecated Overriding or instance-patching this method directly is
* deprecated — pass the `elicitationHandlers` connection option instead.
*/
async handleElicitationRequest(request, signal) {
const mode = request.params.mode === "url" ? "url" : "form";
const handler = this.options.elicitationHandlers?.[mode];
if (handler) return raceWithSignal(signal ? handler(request, signal) : handler(request), signal);
if (this.options.elicitationHandlers) throw new Error(`No MCP ${mode}-mode elicitation handler configured for this connection.`);
throw new Error("Elicitation handler must be implemented for your platform. Provide the MCPClientConnection elicitationHandlers option, or register handlers through the MCP client manager before connecting.");
}
isResumedStreamableHttpSession() {
return this._transport instanceof StreamableHTTPClientTransport && typeof this._transport.sessionId === "string";
}
get sessionId() {
if (this._transport instanceof StreamableHTTPClientTransport) return this._transport.sessionId;
}
/** @internal Clear a restored session before reconnecting. */
clearResumedSession() {
if ("sessionId" in this.options.transport) delete this.options.transport.sessionId;
}
get protocolVersion() {
if (this._transport instanceof StreamableHTTPClientTransport) return this._transport.protocolVersion;
}
get discoverResult() {
return this.client.getDiscoverResult();
}
async openRestoredListSubscription() {
const capabilities = this.client.getServerCapabilities();
const filter = {
...capabilities?.tools?.listChanged && { toolsListChanged: true },
...capabilities?.prompts?.listChanged && { promptsListChanged: true },
...capabilities?.resources?.listChanged && { resourcesListChanged: true }
};
if (Object.keys(filter).length === 0) return;
try {
this._restoredListSubscription = await this.client.listen(filter);
} catch (error) {
this.client.onerror?.(error instanceof Error ? error : new Error(String(error)));
}
}
getTransportName(transport) {
if (transport instanceof StreamableHTTPClientTransport) return "streamable-http";
if (transport instanceof SSEClientTransport) return "sse";
if (transport instanceof RPCClientTransport) return "rpc";
return this.lastConnectedTransport;
}
async close() {
const transport = this._transport;
this._transport = void 0;
await this._restoredListSubscription?.close().catch(() => {});
this._restoredListSubscription = void 0;
const url = this.url.toString();
const transportName = this.getTransportName(transport);
if (transport instanceof StreamableHTTPClientTransport && transport.sessionId) try {
await transport.terminateSession();
} catch (error) {
this._onObservabilityEvent.fire({
type: "mcp:client:close",
payload: {
url,
transport: transportName,
state: "error",
error: toErrorMessage(error),
phase: "terminate-session"
},
timestamp: Date.now()
});
}
try {
await this.client.close();
} catch (error) {
this._onObservabilityEvent.fire({
type: "mcp:client:close",
payload: {
url,
transport: transportName,
state: "error",
error: toErrorMessage(error),
phase: "client-close"
},
timestamp: Date.now()
});
throw error;
}
this._onObservabilityEvent.fire({
type: "mcp:client:close",
payload: {
url,
transport: transportName,
state: "closed"
},
timestamp: Date.now()
});
}
/**
* 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);
case "rpc": return new RPCClientTransport(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 {
const prior = transport instanceof StreamableHTTPClientTransport && transport.sessionId && this.options.discoverResult ? {
kind: "modern",
discover: this.options.discoverResult
} : void 0;
await this.client.connect(transport, prior ? { prior } : void 0);
this._transport = transport;
this._pendingAuthTransport = void 0;
if (prior) await this.openRestoredListSubscription();
return {
state: MCPConnectionState.CONNECTED,
transport: currentTransportType
};
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
if (isUnauthorized(error)) {
this._pendingAuthTransport = transport;
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",
payload: {
url,
capability: method.split("/")[0],
error: toErrorMessage(e)
},
timestamp: Date.now()
});
return empty;
}
throw e;
};
}
};
//#endregion
//#region src/mcp/client-storage.ts
function persistClientOptions(client) {
if (!client) return void 0;
return {
capabilities: client.capabilities,
supportedProtocolVersions: client.supportedProtocolVersions,
enforceStrictCapabilities: client.enforceStrictCapabilities,
debouncedNotificationMethods: client.debouncedNotificationMethods,
versionNegotiation: client.versionNegotiation,
inputRequired: client.inputRequired,
listMaxPages: client.listMaxPages,
cachePartition: client.cachePartition,
defaultCacheTtlMs: client.defaultCacheTtlMs
};
}
function persistTransportOptions(value) {
if (!value) return void 0;
return {
type: value.type,
headers: value.headers,
requestInit: value.requestInit,
reconnectionOptions: value.reconnectionOptions,
skipIssuerMetadataValidation: value.skipIssuerMetadataValidation,
onInsufficientScope: value.onInsufficientScope,
maxStepUpRetries: value.maxStepUpRetries,
sessionId: value.sessionId,
protocolVersion: value.protocolVersion
};
}
function encodeMcpServerOptions(options) {
return JSON.stringify({
client: persistClientOptions(options.client),
transport: persistTransportOptions(options.transport),
discoverResult: options.discoverResult,
retry: options.retry,
bindingName: options.bindingName,
props: options.props,
capabilities: options.capabilities
});
}
function decodeMcpServerOptions(value) {
if (!value) return {};
const parsed = JSON.parse(value);
const transport = persistTransportOptions(parsed.transport);
const statelessWithoutPrior = transport?.protocolVersion === "2026-07-28" && !parsed.discoverResult;
if (transport?.sessionId && (!transport.protocolVersion || statelessWithoutPrior)) {
delete transport.sessionId;
delete transport.protocolVersion;
delete parsed.discoverResult;
}
return {
client: persistClientOptions(parsed.client),
transport,
discoverResult: parsed.discoverResult,
retry: parsed.retry,
...parsed.bindingName !== void 0 && { bindingName: parsed.bindingName },
...parsed.props !== void 0 && { props: parsed.props },
capabilities: parsed.capabilities
};
}
function withMcpSession(options, session) {
const transport = { ...options.transport ?? {} };
if (!session) {
delete transport.sessionId;
delete transport.protocolVersion;
const next = {
...options,
transport
};
delete next.discoverResult;
return next;
}
transport.sessionId = session.id;
transport.protocolVersion = session.protocolVersion;
return {
...options,
transport,
...session.discoverResult ? { discoverResult: session.discoverResult } : { discoverResult: void 0 }
};
}
//#endregion
//#region src/mcp/client.ts
/** Maximum length of a normalized MCP server id. */
const MCP_SERVER_ID_MAX_LENGTH = 64;
/**
* Normalize a caller-supplied MCP server id into a stable, storage- and
* tool-name-safe form.
*
* The id is surfaced in several places where the character set matters:
* - as the primary key in the `cf_agents_mcp_servers` SQLite table
* - embedded in AI SDK tool names as `` `tool_${id.replace(/-/g, "")}_${tool}` ``
* (tool names must match `/^[A-Za-z0-9_]+$/`)
* - as a key on the `mcpConnections` map and OAuth provider storage
*
* Rules:
* 1. Lowercase.
* 2. Replace any run of disallowed characters with a single `-`.
* 3. Collapse repeated `-` and trim leading/trailing `-`/`_`.
* 4. Prefix with `id-` if the result is empty or doesn't start with a letter.
* 5. Truncate to {@link MCP_SERVER_ID_MAX_LENGTH} characters.
*
* @example
* normalizeServerId("my-supplied-id"); // "my-supplied-id"
* normalizeServerId("GitHub MCP!"); // "github-mcp"
* normalizeServerId("42-things"); // "id-42-things"
*/
function normalizeServerId(input) {
if (typeof input !== "string") throw new TypeError(`normalizeServerId: expected string, got ${typeof input}`);
let id = input.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
if (id.length === 0 || !/^[a-z]/.test(id)) id = `id-${id}`.replace(/-+$/g, "");
if (id.length > 64) id = id.slice(0, 64).replace(/-+$/g, "");
return id;
}
/**
* Blocked hostname patterns for SSRF protection.
* Prevents MCP client from connecting to internal/private network addresses
* while allowing loopback hosts for local development.
*/
const BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
"0.0.0.0",
"[::]",
"metadata.google.internal"
]);
/**
* Check whether four IPv4 octets belong to a private/reserved range.
* Blocks RFC 1918, link-local, cloud metadata, and unspecified addresses.
*/
function isPrivateIPv4(octets) {
const [a, b] = octets;
if (a === 10) return true;
if (a === 172 && b >= 16 && b <= 31) return true;
if (a === 192 && b === 168) return true;
if (a === 169 && b === 254) return true;
if (a === 0) return true;
return false;
}
/**
* fe80::/10 — IPv6 link-local (RFC 4291 §2.5.6).
*
* The /10 boundary fixes the first 10 bits (1111111010), which means valid
* first hextets range from fe80 through febf. Only hex digits 8, 9, a, b
* have high two bits "10" — anything else (e.g. fe7f, fec0) is out of range.
* The fourth hex digit is unconstrained by the /10 boundary.
*
* Historical bug: `startsWith("fe80")` only matched the narrower fe80::/16
* prefix and let fe81::/feab::/febf:: slip through. See issue #1325.
*/
const IPV6_LINK_LOCAL = /^fe[89ab][0-9a-f]/;
/**
* Check whether a bracket-stripped, lowercased IPv6 address belongs to a
* private/reserved range. Also unwraps IPv4-mapped IPv6 (::ffff:...) and
* delegates to isPrivateIPv4 for those.
*
* Loopback (::1) and unspecified (::) are NOT blocked here:
* - ::1 is intentionally allowed (parallel to 127.x.x.x for local dev)
* - :: (== [::]) is blocked via BLOCKED_HOSTNAMES at the hostname level
*/
function isPrivateIPv6(addr) {
if (addr.startsWith("fc") || addr.startsWith("fd")) return true;
if (IPV6_LINK_LOCAL.test(addr)) return true;
if (addr.startsWith("::ffff:")) {
const mapped = addr.slice(7);
const dotParts = mapped.split(".");
if (dotParts.length === 4 && dotParts.every((p) => /^\d{1,3}$/.test(p))) {
if (isPrivateIPv4(dotParts.map(Number))) return true;
} else {
const hexParts = mapped.split(":");
if (hexParts.length === 2) {
const hi = parseInt(hexParts[0], 16);
const lo = parseInt(hexParts[1], 16);
if (isPrivateIPv4([
hi >> 8 & 255,
hi & 255,
lo >> 8 & 255,
lo & 255
])) return true;
}
}
}
return false;
}
/**
* Check whether a hostname looks like a private/internal IP address.
* Blocks RFC 1918, link-local, unique-local, unspecified,
* and cloud metadata endpoints. Also detects IPv4-mapped IPv6 addresses.
*/
function isBlockedUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
return true;
}
const hostname = parsed.hostname;
if (BLOCKED_HOSTNAMES.has(hostname)) return true;
const ipv4Parts = hostname.split(".");
if (ipv4Parts.length === 4 && ipv4Parts.every((p) => /^\d{1,3}$/.test(p))) {
if (isPrivateIPv4(ipv4Parts.map(Number))) return true;
}
if (hostname.startsWith("[") && hostname.endsWith("]")) {
if (isPrivateIPv6(hostname.slice(1, -1).toLowerCase())) return true;
}
return false;
}
/** Converts a resolved connection failure into a retry signal. */
var ConnectRetryError = class extends Error {
constructor(result) {
super("MCP connection attempt failed");
this.result = result;
}
};
/**
* 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._aiToolSchemas = /* @__PURE__ */ new WeakMap();
this._didWarnAboutUnstableGetAITools = false;
this._connectionDisposables = /* @__PURE__ */ new Map();
this._isRestored = false;
this._pendingConnections = /* @__PURE__ */ new Map();
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;
this._createAuthProviderFn = options.createAuthProvider;
}
/**
* Scope the manager-level elicitation handler to a single connection.
* Returns undefined when no handler is configured so the connection keeps
* its default throwing behavior.
*/
scopedElicitationHandlers(serverId) {
const handlers = this._elicitationHandlers;
if (!handlers) return void 0;
const form = handlers.form;
const url = handlers.url;
return {
form: form ? (request, signal) => signal ? form(request, serverId, signal) : form(request, serverId) : void 0,
url: url ? (request, signal) => signal ? url(request, serverId, signal) : url(request, serverId) : void 0
};
}
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);
}
/**
* Rename a server's id, in-place, across every place the id is used as a
* key. Used to JIT-migrate servers that were originally registered under an
* auto-generated nanoid to a caller-supplied stable id (see
* `Agent.addMcpServer`'s `{ id }` option).
*
* Migrates:
* - the `cf_agents_mcp_servers` row (primary key)
* - the in-memory `mcpConnections` map key
* - the connection disposables map key
* - the attached `authProvider.serverId`, if any
* - OAuth-related storage keys under `/{clientName}/{oldId}/...`
*
* Safe to call when no OAuth keys exist (RPC / bearer-token HTTP servers).
* If `oldId === newId` this is a no-op. If a row already exists under
* `newId`, throws — the caller is expected to have verified uniqueness.
*
* @internal Exposed for `Agent.addMcpServer` JIT-migration.
*/
async migrateServerId(oldId, newId, clientName) {
if (oldId === newId) return;
while (true) {
const pending = this._pendingConnections.get(oldId);
if (!pending) break;
await pending.catch(() => {});
}
if (this.sql("SELECT id FROM cf_agents_mcp_servers WHERE id = ?", oldId).length === 0) {
this._renameInMemoryConnection(oldId, newId);
return;
}
if (this.sql("SELECT id FROM cf_agents_mcp_servers WHERE id = ?", newId).length > 0) throw new Error(`Cannot migrate MCP server id "${oldId}" → "${newId}": new id is already in use.`);
this.sql("UPDATE cf_agents_mcp_servers SET id = ? WHERE id = ?", newId, oldId);
const oldPrefix = `/${clientName}/${oldId}/`;
const newPrefix = `/${clientName}/${newId}/`;
try {
const keys = await this._storage.list({ prefix: oldPrefix });
if (keys.size > 0) {
const writes = {};
const deletes = [];
for (const [oldKey, value] of keys) {
const newKey = newPrefix + oldKey.slice(oldPrefix.length);
writes[newKey] = value;
deletes.push(oldKey);
}
await this._storage.put(writes);
await this._storage.delete(deletes);
}
} catch (error) {
console.warn(`[MCPClientManager] OAuth key migration ${oldPrefix} → ${newPrefix} failed:`, error);
}
this._renameInMemoryConnection(oldId, newId);
this._onServerStateChanged.fire();
}
_renameInMemoryConnection(oldId, newId) {
if (oldId === newId) return;
const conn = this.mcpConnections[oldId];
if (conn) {
this.mcpConnections[newId] = conn;
delete this.mcpConnections[oldId];
const authProvider = conn.options.transport.authProvider;
if (authProvider) authProvider.serverId = newId;
const scoped = this.scopedElicitationHandlers(newId);
if (scoped) conn.configureElicitationHandlers(scoped);
}
const disposables = this._connectionDisposables.get(oldId);
if (disposables) {
this._connectionDisposables.set(newId, disposables);
this._connectionDisposables.delete(oldId);
}
}
getServersFromStorage() {
return this.sql("SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers");
}
filterConnections(filter) {
if (!filter) return this.mcpConnections;
const serverIds = filter.serverId ? Array.isArray(filter.serverId) ? filter.serverId : [filter.serverId] : void 0;
const serverNames = filter.serverName ? Array.isArray(filter.serverName) ? filter.serverName : [filter.serverName] : void 0;
const states = filter.state ? Array.isArray(filter.state) ? filter.state : [filter.state] : void 0;
let nameMatchedIds;
if (serverNames) {
const servers = this.getServersFromStorage();
nameMatchedIds = new Set(servers.filter((s) => serverNames.includes(s.name)).map((s) => s.id));
}
return Object.fromEntries(Object.entries(this.mcpConnections).filter(([id, conn]) => {
if (serverIds && !serverIds.includes(id)) return false;
if (nameMatchedIds && !nameMatchedIds.has(id)) return false;
if (states && !states.includes(conn.connectionState)) return false;
return true;
}));
}
/**
* Get the parsed server_options for a stored server, if any.
*/
getStoredServerOptions(serverId) {
const rows = this.sql("SELECT server_options FROM cf_agents_mcp_servers WHERE id = ?", serverId);
if (!rows.length || !rows[0].server_options) return void 0;
return decodeMcpServerOptions(rows[0].server_options);
}
/**
* Clear the capabilities persisted on a stored server row. Called once a
* seeded connection's handshake completes (see `createConnection`): the
* stamp is valid for one successful restore — sessions that configure
* handlers re-stamp every row, so a deploy that stops configuring them
* stops advertising stale modes after its first connected wake instead of
* forever, while wakes that never handshake don't burn the stamp.
*/
clearStoredCapabilities(serverId) {
const row = this.sql("SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers WHERE id = ?", serverId)[0];
if (!row?.server_options) return;
const options = decodeMcpServerOptions(row.server_options);
if (!options.capabilities) return;
options.capabilities = void 0;
this.saveServerToStorage({
...row,
server_options: encodeMcpServerOptions(options)
});
}
/**
* Get the retry options for a server from stored server_options
*/
getServerRetryOptions(serverId) {
return this.getStoredServerOptions(serverId)?.retry;
}
clearServerAuthUrl(serverId) {
this.sql("UPDATE cf_agents_mcp_servers SET auth_url = NULL WHERE id = ?", serverId);
}
updateStoredSession(id, sessionId, protocolVersion, discoverResult) {
const serverRow = this.getServersFromStorage().find((server) => server.id === id);
if (!serverRow) return;
const options = decodeMcpServerOptions(serverRow.server_options);
const next = sessionId && protocolVersion ? withMcpSession(options, {
id: sessionId,
protocolVersion,
discoverResult
}) : withMcpSession(options);
this.saveServerToStorage({
...serverRow,
server_options: encodeMcpServerOptions(next)
});
}
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
};
}
isAuthAcceptedConnection(conn) {
return conn.connectionState === MCPConnectionState.READY || conn.connectionState === MCPConnectionState.CONNECTED || conn.connectionState === MCPConnectionState.CONNECTING || conn.connectionState === MCPConnectionState.DISCOVERING;
}
oauthCallbackSuccess(serverId, conn) {
this.clearServerAuthUrl(serverId);
conn.connectionError = null;
return {
serverId,
authSuccess: true
};
}
async runWithCodeVerifierState(authProvider, state, callback) {
if (authProvider.runWithCodeVerifierState) return authProvider.runWithCodeVerifierState(state, callback);
return callback();
}
async hasRedeemableOAuthState(serverId, authProvider, state) {
authProvider.serverId = serverId;
try {
return (await authProvider.checkState(state)).valid;
} catch {
return false;
}
}
ignoreUnverifiedCallback(serverId, error) {
console.warn(`[MCPClientManager] Ignoring OAuth callback with unverified state for server "${serverId}": ${error}`);
return {
serverId,
authSuccess: false,
authError: error
};
}
async consumeStaleOAuthState(serverId, authProvider, state) {
try {
const stateValidation = await authProvider.checkState(state);
if (!stateValidation.valid) {
console.warn(`[MCPClientManager] Ignoring stale OAuth callback with invalid state for server "${serverId}": ${stateValidation.error ?? "Invalid state"}`);
return;
}
await authProvider.consumeState(state);
} catch (cleanupError) {
console.warn(`[MCPClientManager] Failed to clean up stale OAuth callback state for server "${serverId}":`, cleanupError);
}
}
async completeAuthorizationAndCleanupVerifier(serverId, conn, authProvider, state, callbackParams) {
await this.runWithCodeVerifierState(authProvider, state, async () => {
let completeError;
let cleanupError;
try {
await conn.completeAuthorization(callbackParams, { alreadyAccepted: true });
} catch (error) {
completeError = error;
}
try {
await authProvider.deleteCodeVerifier();
} catch (deleteError) {
cleanupError = deleteError;
}
if (completeError) {
if (cleanupError) console.warn(`[MCPClientManager] Failed to clean up OAuth code verifier for server "${serverId}":`, cleanupError);
throw completeError;
}
if (cleanupError) throw cleanupError;
});
}
/**
* 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;
}
/**
* Get saved RPC servers from storage (servers with rpc:// URLs).
* These are restored separately by the Agent class since they need env bindings.
*/
getRpcServersFromStorage() {
return this.getServersFromStorage().filter((s) => s.server_url.startsWith(RPC_DO_PREFIX));
}
/**
* Save an RPC server to storage for hibernation recovery.
* The bindingName is stored in server_options so the Agent can look up
* the namespace from env during restore.
*/
saveRpcServerToStorage(id, name, normalizedName, bindingName, props) {
this.saveServerToStorage({
id,
name,
server_url: `${RPC_DO_PREFIX}${normalizedName}`,
client_id: null,
auth_url: null,
callback_url: "",
server_options: encodeMcpServerOptions({
bindingName,
props,
capabilities: this.advertisedHandlerCapabilities()
})
});
}
/**
* Restore MCP server connections from storage
* This method is called on Agent initialization to restore previously connected servers.
* RPC servers (rpc:// URLs) are skipped here -- they are restored by the Agent class
* which has access to env bindings.
*
* @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) {
if (server.server_url.startsWith("rpc:")) continue;
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.close();
} catch (error) {
console.warn(`[MCPClientManager] Error closing failed connection ${server.id}:`, error);
} finally {
this.cleanupClosedConnection(server.id);
}
}
const parsedOptions = decodeMcpServerOptions(server.server_options);
let authProvider;
if (server.callback_url) {
authProvider = this._createAuthProviderFn ? this._createAuthProviderFn(server.callback_url) : this.createAuthProvider(server.id, server.callback_url, clientName, server.client_id ?? void 0);
authProvider.serverId = server.id;
if (server.client_id) authProvider.clientId = server.client_id;
}
const conn = this.createConnection(server.id, server.server_url, {
client: parsedOptions?.client ?? {},
transport: {
...parsedOptions?.transport ?? {},
type: parsedOptions?.transport?.type ?? "auto",
authProvider
},
discoverResult: parsedOptions?.discoverResult
});
if (server.auth_url) {
conn.connectionState = MCPConnectionState.AUTHENTICATING;
continue;
}
this._trackConnection(server.id, this._restoreServer(server.id, parsedOptions?.retry));
}
this._isRestored = true;
}
/**
* Track a pending connection promise for a server.
* The promise is removed from the map when it settles.
*/
_trackConnection(serverId, promise) {
const tracked = promise.finally(() => {
if (this._pendingConnections.get(serverId) === tracked) this._pendingConnections.delete(serverId);
});
this._pendingConnections.set(serverId, tracked);
}
/**
* Wait for all in-flight connection and discovery operations to settle.
* This is useful when you need MCP tools to be available before proceeding,
* e.g. before calling getAITools() after the agent wakes from hibernation.
*
* Returns once every pending connection has either connected and discovered,
* failed, or timed out. Never rejects.
*
* @param options.timeout - Maximum time in milliseconds to wait.
* `0` returns immediately without waiting.
* `undefined` (default) waits indefinitely.
*/
async waitForConnections(options) {
if (this._pendingConnections.size === 0) return;
if (options?.timeout != null && options.timeout <= 0) return;
const settled = Promise.allSettled(this._pendingConnections.values());
if (options?.timeout != null && options.timeout > 0) {
let timerId;
const timer = new Promise((resolve) => {
timerId = setTimeout(resolve, options.timeout);
});
await Promise.race([settled, timer]);
clearTimeout(timerId);
} else await settled;
}
async _connectWithRetry(serverId, retry) {
const maxAttempts = retry?.maxAttempts ?? 3;
const baseDelayMs = retry?.baseDelayMs ?? 500;
const maxDelayMs = retry?.maxDelayMs ?? 5e3;
try {
return await tryN(maxAttempts, async () => {
const result = await this.connectToServer(serverId);
if (result.state === MCPConnectionState.FAILED) throw new ConnectRetryError(result);
return result;
}, {
baseDelayMs,
maxDelayMs
});
} catch (error) {
if (error instanceof ConnectRetryError) return error.result;
throw error;
}
}
/**
* Internal method to restore a single server connection and discovery
*/
async _restoreServer(serverId, retry) {
if ((await this._connectWithRetry(serverId, retry).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 = {}) {
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 (isBlockedUrl(url)) throw new Error(`Blocked URL: ${url} — MCP client connections to private/internal addresses are not allowed`);
if (!options.reconnect?.oauthCode || !this.mcpConnections[id]) {
const replaced = this.mcpConnections[id];
if (replaced) {
delete this.mcpConnections[id];
await replaced.close().catch(() => {});
this.updateStoredSession(id, void 0);
}
this.createConnection(id, url, {
client: options.client,
transport: options.transport ?? {}
});
}
await this.mcpConnections[id].init();
if (options.reconnect?.oauthCode) try {
const authProvider = this.mcpConnections[id].options.transport.authProvider;
let completeError;
try {
await this.mcpConnections[id].completeAuthorization(options.reconnect.oauthCode);
} catch (error) {
completeError = error;
}
try {
await authProvider?.deleteCodeVerifier();
} catch (cleanupError) {
console.warn(`[MCPClientManager] Failed to clean up OAuth code verifier for server "${id}":`, cleanupError);
}
if (completeError) throw completeError;
await this.mcpConnections[id].init();
} catch (error) {
this._onObservabilityEvent.fire({
type: "mcp:client:connect",
payload: {
url,
transport: options.transport?.type ?? "auto",
state: this.mcpConnections[id].connectionState,
error: toErrorMessage(error)
},
timestamp: Date.now()
});
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"
};
const capabilitySeed = this.getStoredServerOptions(id)?.capabilities;
this.mcpConnections[id] = new MCPClientConnection(new URL(url), {
name: this._name,
version: this._version
}, {
client: options.client ?? {},
transport: normalizedTransport,
elicitationHandlers: this.scopedElicitationHandlers(id),
capabilitySeed,
discoverResult: options.discoverResult
});
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);
}));
store.add(this.mcpConnections[id].onListChanged(() => {
this._onServerStateChanged.fire();
}));
if (capabilitySeed) {
const conn = this.mcpConnections[id];
const seedClear = conn.onObservabilityEvent((event) => {
if (event.type !== "mcp:client:connect" || event.payload.state !== MCPConnectionState.CONNECTED) return;
seedClear.dispose();
if (this._elicitationHandlers || !conn.options.capabilitySeed) return;
const currentId = Object.keys(this.mcpConnections).find((key) => this.mcpConnections[key] === conn);
if (currentId) this.clearStoredCapabilities(currentId);
});
store.add(seedClear);
}
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) {
if (isBlockedUrl(options.url)) throw new Error(`Blocked URL: ${options.url} — MCP client connections to private/internal addresses are not allowed`);
this.createConnection(id, options.url, {
client: options.client,
transport: {
...options.transport,
type: options.transport?.type ?? "auto"
}
});
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: encodeMcpServerOptions({
client: options.client,
transport: options.transport,
retry: options.retry,
capabilities: this.advertisedHandlerCapabilities()
})
});
this._onServerStateChanged.fire();
return id;
}
/** Persist and emit an OAuth continuation produced by connect or discovery. */
persistAuthContinuation(id, conn) {
const authProvider = conn.options.transport.authProvider;
const authUrl = authProvider?.authUrl;
if (!authUrl || !authProvider.redirectUrl) return void 0;
const clientId = authProvider.clientId;
const serverRow = this.getServersFromStorage().find((s) => s.id === id);
if (serverRow) this.saveServerToStorage({
...serverRow,
auth_url: authUrl,
client_id: clientId ?? null
});
this._onObservabilityEvent.fire({
type: "mcp:client:authorize",
payload: {
serverId: id,
authUrl,
clientId
},
timestamp: Date.now()
});
return {
authUrl,
clientId
};
}
/**
* 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.updateStoredSession(id, conn.sessionId, conn.protocolVersion, conn.discoverResult);
this._onServerStateChanged.fire();
switch (conn.connectionState) {
case MCPConnectionState.FAILED: return {
state: conn.connectionState,
error: error ?? "Unknown connection error"
};
case MCPConnectionState.AUTHENTICATING: {
const auth = this.persistAuthContinuation(id, conn);
if (!auth) {
const provider = conn.options.transport.authProvider;
return {
state: MCPConnectionState.FAILED,
error: `OAuth configuration incomplete: missing ${!provider?.authUrl ? "authUrl" : "redirectUrl"}`
};
}
this._onServerStateChanged.fire();
return {
state: conn.connectionState,
...auth
};
}
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;
const url = new URL(req.url);
const state = url.searchParams.get("state");
const serverId = this.extractServerIdFromState(state);
if (!serverId) return false;
return this.getServersFromStorage().some((server) => {
if (server.id !== serverId) return false;
try {
const storedUrl = new URL(server.callback_url);
return storedUrl.origin === url.origin && storedUrl.pathname === url.pathname;
} catch {
return false;
}
});
}
validateCallbackRequest(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");
if (!state) return {
valid: false,
error: "Unauthorized: no state provided"
};
const serverId = this.extractServerIdFromState(state);
if (!serverId) return {
valid: false,
error: "No serverId found in state parameter. Expected format: {nonce}.{serverId}"
};
if (!code && !error) return {
serverId,
state,
valid: false,
error: "Unauthorized: no code provided"
};
if (!this.getServersFromStorage().some((server) => server.id === serverId)) return {
serverId,
valid: false,
error: `No server found with id "${serverId}". Was the request matched with \`isCallbackRequest()\`?`
};
if (this.mcpConnections[serverId] === void 0) return {
serverId,
valid: false,
error: `No connection found for serverId "${serverId}".`
};
return {
valid: true,
serverId,
state,
callbackParams: url.searchParams
};
}
async handleCallbackRequest(req) {
const validation = this.validateCallbackRequest(req);
if (!validation.valid) {
const conn = validation.serverId ? this.mcpConnections[validation.serverId] : void 0;
if (validation.serverId && conn) {
if (this.isAuthAcceptedConnection(conn)) {
const authProvider = conn.options.transport.authProvider;
if (validation.state && authProvider) {
authProvider.serverId = validation.serverId;
await this.consumeStaleOAuthState(validation.serverId, authProvider, validation.state);
}
return this.oauthCallbackSuccess(validation.serverId, conn);
}
const authProvider = conn.options.transport.authProvider;
if (validation.state && authProvider && !await this.hasRedeemableOAuthState(validation.serverId, authProvider, validation.state)) return this.ignoreUnverifiedCallback(validation.serverId, validation.error);
return this.failConnection(validation.serverId, validation.error);
}
return {
serverId: validation.serverId,
authSuccess: false,
authError: validation.error
};
}
const { serverId, state, callbackParams } = validation;
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) {
if (this.isAuthAcceptedConnection(conn)) {
await this.consumeStaleOAuthState(serverId, authProvider, state);
return this.oauthCallbackSuccess(serverId, conn);
}
return this.ignoreUnverifiedCallback(serverId, callbackParams.get("error_description") ?? callbackParams.get("error") ?? stateValidation.error ?? "Invalid state");
}
if (this.isAuthAcceptedConnection(conn)) {
await this.consumeStaleOAuthState(serverId, authProvider, state);
return this.oauthCallbackSuccess(serverId, conn);
}
if (conn.connectionState !== MCPConnectionState.AUTHENTICATING && conn.connectionState !== MCPConnectionState.FAILED) throw new Error(`Failed to authenticate from "${conn.connectionState}" state`);
conn.connectionState = MCPConnectionState.CONNECTING;
await authProvider.consumeState(state);
await this.completeAuthorizationAndCleanupVerifier(serverId, conn, authProvider, state, callbackParams);
this.updateStoredSession(serverId, conn.sessionId, conn.protocolVersion, conn.discoverResult);
const result = this.oauthCallbackSuccess(serverId, conn);
this._onServerStateChanged.fire();
return result;
} 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",
payload: {},
timestamp: Date.now()
});
return;
}
const result = await conn.discover(options);
if (!result.success && result.reason === "stale-session") return this._recoverStaleSession(conn, serverId, options);
if (conn.connectionState === MCPConnectionState.AUTHENTICATING) this.persistAuthContinuation(serverId, conn);
this._onServerStateChanged.fire();
return this._toDiscoverResult(conn, result);
}
_toDiscoverResult(conn, result) {
return result.success ? {
success: true,
state: conn.connectionState
} : {
success: false,
error: result.error,
state: conn.connectionState
};
}
async _recoverStaleSession(conn, serverId, options) {
conn.clearResumedSession();
this.updateStoredSession(serverId, void 0);
let connectResult;
try {
connectResult = await this.connectToServer(serverId);
} catch (error) {
return {
success: false,
error: toErrorMessage(error),
state: conn.connectionState
};
}
if (connectResult.state !== MCPConnectionState.CONNECTED) return {
success: false,
error: connectResult.state === MCPConnectionState.FAILED ? connectResult.error : `Connection in ${connectResult.state} state after session re-initialization`,
state: conn.connectionState
};
const result = await conn.discover(options);
this._onServerStateChanged.fire();
return this._toDiscoverResult(conn, result);
}
/**
* Establish connection in the background after OAuth completion.
* This method connects to the server and discovers its capabilities.
* The connection is automatically tracked so that `waitForConnections()`
* will include it.
* @param serverId The server ID to establish connection for
*/
async establishConnection(serverId) {
const promise = this._doEstablishConnection(serverId);
this._trackConnection(serverId, promise);
return promise;
}
async _doEstablishConnection(serverId) {
const conn = this.mcpConnections[serverId];
if (!conn) {
this._onObservabilityEvent.fire({
type: "mcp:client:preconnect",
payload: { serverId },
timestamp: Date.now()
});
return;
}
if (conn.connectionState === MCPConnectionState.DISCOVERING || conn.connectionState === MCPConnectionState.READY) {
this._onObservabilityEvent.fire({
type: "mcp:client:connect",
payload: {
url: conn.url.toString(),
transport: conn.options.transport.type || "unknown",
state: conn.connectionState
},
timestamp: Date.now()
});
return;
}
const retry = this.getServerRetryOptions(serverId);
const connectResult = await this._connectWithRetry(serverId, retry);
this._onServerStateChanged.fire();
if (connectResult.state === MCPConnectionState.CONNECTED) await this.discoverIfConnected(serverId);
this._onObservabilityEvent.fire({
type: "mcp:client:connect",
payload: {
url: conn.url.toString(),
transport: conn.options.transport.type || "unknown",
state: conn.connectionState
},
timestamp: Date.now()
});
}
/**
* Configure OAuth callback handling
* @param config OAuth callback configuration
*/
configureOAuthCallback(config) {
this._oauthCallbackConfig = config;
}
/**
* Configure handling for server-initiated `elicitation/create` requests.
*
* The handler is held in memory only and applied to every MCP connection
* created or restored by this manager. Call this before registering
* connections when you want the initial MCP handshake to advertise
* handler-driven form- and url-mode elicitation. Existing active connections
* keep their negotiated capabilities until they reconnect.
*
* The advertised modes are persisted with each stored server, so
* connections restored after hibernation re-advertise them at the handshake
* even when this is called later in the wake-up (e.g. from onStart) — the
* handlers attach to the live connections as soon as this runs.
*
* Pass undefined to clear the handler.
*
* @param handlers Elicitation handlers keyed by mode, each scoped with the server id that sent the request
*/
configureElicitationHandlers(handlers) {
this._elicitationHandlers = handlers && (handlers.form || handlers.url) ? handlers : void 0;
this.persistAdvertisedCapabilities();
for (const [id, connection] of Object.entries(this.mcpConnections)) connection.configureElicitationHandlers(this.scopedElicitationHandlers(id));
}
/** Client capabilities advertised from the currently configured handlers. */
advertisedHandlerCapabilities() {
const elicitation = elicitationCapabilitiesFromHandlers(this._elicitationHandlers);
return elicitation ? { elicitation } : void 0;
}
/**
* Record the handler-derived capabilities on every stored server row so a
* restore after hibernation re-advertises them before the handlers
* themselves are reconfigured.
*/
persistAdvertisedCapabilities() {
const capabilities = this.advertisedHandlerCapabilities();
for (const server of this.getServersFromStorage()) {
const options = decodeMcpServerOptions(server.server_options);
if (JSON.stringify(options.capabilities) === JSON.stringify(capabilities)) continue;
options.capabilities = capabilities;
this.saveServerToStorage({
...server,
server_options: encodeMcpServerOptions(options)
});
}
}
/**
* Get the current OAuth callback configuration
* @returns The current OAuth callback configuration
*/
getOAuthCallbackConfig() {
return this._oauthCallbackConfig;
}
/**
* @param filter - Optional filter to scope results to specific servers
* @returns namespaced list of tools
*/
listTools(filter) {
return getNamespacedData(this.filterConnections(filter), "tools");
}
/**
* Convert connected MCP tools for the AI SDK. Converted schemas are reused
* while a live connection retains the same catalog array and schema-source
* identities; tool records and execute closures are rebuilt on every call.
*
* @param filter - Optional filter to scope results to specific servers
* @returns a set of tools that you can use with the AI SDK
*/
getAITools(filter) {
const connections = this.filterConnections(filter);
const entries = [];
for (const [serverId, conn] of Object.entries(connections)) {
if (conn.connectionState !== MCPConnectionState.READY && conn.connectionState !== MCPConnectionState.AUTHENTICATING) console.warn(`[getAITools] WARNING: Reading tools from connection ${serverId} in state "${conn.connectionState}". Tools may not be loaded yet.`);
const catalog = conn.tools;
let cache = this._aiToolSchemas.get(conn);
if (!cache || cache.catalog !== catalog) {
cache = {
catalog,
converted: []
};
this._aiToolSchemas.set(conn, cache);
}
for (const [index, tool] of catalog.entries()) {
const toolName = tool.name;
try {
const sourceInputSchema = tool.inputSchema;
const sourceOutputSchema = tool.outputSchema;
let slot = cache.converted[index];
if (!slot || slot.tool !== tool || slot.inputSchema !== sourceInputSchema || slot.outputSchema !== sourceOutputSchema) {
try {
slot = {
status: "converted",
tool,
inputSchema: sourceInputSchema,
outputSchema: sourceOutputSchema,
converted: {
inputSchema: sourceInputSchema ? z.fromJSONSchema(sourceInputSchema) : z.fromJSONSchema({ type: "object" }),
outputSchema: sourceOutputSchema ? z.fromJSONSchema(sourceOutputSchema) : void 0
}
};
} catch (error) {
const errorText = String(error);
cache.converted[index] = {
status: "failed",
tool,
inputSchema: sourceInputSchema,
outputSchema: sourceOutputSchema,
error: errorText
};
console.warn(`[getAITools] Skipping tool "${toolName}" from "${serverId}": ${errorText}`);
continue;
}
cache.converted[index] = slot;
}
if (slot.status === "failed") continue;
const toolKey = `tool_${serverId.replace(/-/g, "")}_${toolName}`;
const title = tool.title ?? tool.annotations?.title;
const description = tool.description;
entries.push([toolKey, {
description,
title,
execute: async (args) => {
const result = await this.callTool({
arguments: args,
name: toolName,
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: slot.converted.inputSchema,
outputSchema: slot.converted.outputSchema
}]);
} catch (error) {
console.warn(`[getAITools] Skipping tool "${toolName}" from "${serverId}": ${error}`);
}
}
cache.converted.length = catalog.length;
}
return Object.fromEntries(entries);
}
/**
* @deprecated this has been renamed to getAITools(), and unstable_getAITools will be removed in the next major version
* @param filter - Optional filter to scope results to specific servers
* @returns a set of tools that you can use with the AI SDK
*/
unstable_getAITools(filter) {
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(filter);
}
/**
* 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).
*/
cleanupClosedConnection(id) {
this.updateStoredSession(id, void 0);
const store = this._connectionDisposables.get(id);
if (store) store.dispose();
this._connectionDisposables.delete(id);
delete this.mcpConnections[id];
}
async closeAllConnections() {
const ids = Object.keys(this.mcpConnections);
this._pendingConnections.clear();
for (const id of ids) this.mcpConnections[id].cancelDiscovery();
const errors = (await Promise.allSettled(ids.map(async (id) => {
try {
await this.mcpConnections[id].close();
} finally {
this.cleanupClosedConnection(id);
}
}))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, "Failed to close one or more MCP connections");
}
/**
* Closes a connection to an MCP server
* @param id The id of the connection to close
*/
async closeConnection(id) {
const connection = this.mcpConnections[id];
if (!connection) throw new Error(`Connection with id "${id}" does not exist.`);
connection.cancelDiscovery();
this._pendingConnections.delete(id);
try {
await connection.close();
} finally {
this.cleanupClosedConnection(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();
}
}
/**
* @param filter - Optional filter to scope results to specific servers
* @returns namespaced list of prompts
*/
listPrompts(filter) {
return getNamespacedData(this.filterConnections(filter), "prompts");
}
/**
* @param filter - Optional filter to scope results to specific servers
* @returns namespaced list of resources
*/
listResources(filter) {
return getNamespacedData(this.filterConnections(filter), "resources");
}
/**
* @param filter - Optional filter to scope results to specific servers
* @returns namespaced list of resource templates
*/
listResourceTemplates(filter) {
return getNamespacedData(this.filterConnections(filter), "resourceTemplates");
}
async callTool(params, schemaOrOptions, options) {
const { serverId, ...mcpParams } = params;
const unqualifiedName = mcpParams.name.replace(`${serverId}.`, "");
return callV2Tool(this.mcpConnections[serverId].client, {
...mcpParams,
name: unqualifiedName
}, schemaOrOptions, options);
}
/**
* Namespaced version of readResource
*/
readResource(params, options) {
const { serverId, ...mcpParams } = params;
return this.mcpConnections[serverId].client.readResource(mcpParams, options);
}
/**
* Namespaced version of getPrompt
*/
getPrompt(params, options) {
const { serverId, ...mcpParams } = params;
return this.mcpConnections[serverId].client.getPrompt(mcpParams, 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 { MCPConnectionState as a, RPC_DO_PREFIX as c, normalizeServerId as i, DisposableStore as l, MCP_SERVER_ID_MAX_LENGTH as n, RPCClientTransport as o, getNamespacedData as r, RPCServerTransport as s, MCPClientManager as t };
//# sourceMappingURL=client-zqKcsyFa.js.map