UNPKG

agents

Version:

A home for your AI agents

2,385 lines 85.9 kB
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