UNPKG

mcp-use

Version:

Opinionated MCP Framework for TypeScript (@modelcontextprotocol/sdk compatible) - Build MCP Agents, Clients and Servers with support for ChatGPT Apps, Code Mode, OAuth, Notifications, Sampling, Observability and more.

1,392 lines (1,382 loc) 43 kB
import { Tel, Telemetry } from "./chunk-CN263ZGG.js"; import { logger } from "./chunk-FRUZDWXH.js"; import { __name } from "./chunk-3GQAWCBQ.js"; // src/session.ts var MCPSession = class { static { __name(this, "MCPSession"); } connector; autoConnect; constructor(connector, autoConnect = true) { this.connector = connector; this.autoConnect = autoConnect; } async connect() { await this.connector.connect(); } async disconnect() { await this.connector.disconnect(); } async initialize() { if (!this.isConnected && this.autoConnect) { await this.connect(); } await this.connector.initialize(); } get isConnected() { return this.connector && this.connector.isClientConnected; } /** * Register an event handler for session events * * @param event - The event type to listen for * @param handler - The handler function to call when the event occurs * * @example * ```typescript * session.on("notification", async (notification) => { * console.log(`Received: ${notification.method}`, notification.params); * * if (notification.method === "notifications/tools/list_changed") { * // Refresh tools list * } * }); * ``` */ on(event, handler) { if (event === "notification") { this.connector.onNotification(handler); } } /** * Set roots and notify the server. * Roots represent directories or files that the client has access to. * * @param roots - Array of Root objects with `uri` (must start with "file://") and optional `name` * * @example * ```typescript * await session.setRoots([ * { uri: "file:///home/user/project", name: "My Project" }, * { uri: "file:///home/user/data" } * ]); * ``` */ async setRoots(roots) { return this.connector.setRoots(roots); } /** * Get the current roots. */ getRoots() { return this.connector.getRoots(); } /** * Get the cached list of tools from the server. * * @returns Array of available tools * * @example * ```typescript * const tools = session.tools; * console.log(`Available tools: ${tools.map(t => t.name).join(", ")}`); * ``` */ get tools() { return this.connector.tools; } /** * List all available tools from the MCP server. * This method fetches fresh tools from the server, unlike the `tools` getter which returns cached tools. * * @param options - Optional request options * @returns Array of available tools * * @example * ```typescript * const tools = await session.listTools(); * console.log(`Available tools: ${tools.map(t => t.name).join(", ")}`); * ``` */ async listTools(options) { return this.connector.listTools(options); } /** * Get the server capabilities advertised during initialization. * * @returns Server capabilities object */ get serverCapabilities() { return this.connector.serverCapabilities; } /** * Get the server information (name and version). * * @returns Server info object or null if not available */ get serverInfo() { return this.connector.serverInfo; } /** * Call a tool on the server. * * @param name - Name of the tool to call * @param args - Arguments to pass to the tool (defaults to empty object) * @param options - Optional request options (timeout, progress handlers, etc.) * @returns Result from the tool execution * * @example * ```typescript * const result = await session.callTool("add", { a: 5, b: 3 }); * console.log(`Result: ${result.content[0].text}`); * ``` */ async callTool(name, args = {}, options) { return this.connector.callTool(name, args, options); } /** * List resources from the server with optional pagination. * * @param cursor - Optional cursor for pagination * @param options - Request options * @returns Resource list with optional nextCursor for pagination * * @example * ```typescript * const result = await session.listResources(); * console.log(`Found ${result.resources.length} resources`); * ``` */ async listResources(cursor, options) { return this.connector.listResources(cursor, options); } /** * List all resources from the server, automatically handling pagination. * * @param options - Request options * @returns Complete list of all resources * * @example * ```typescript * const result = await session.listAllResources(); * console.log(`Total resources: ${result.resources.length}`); * ``` */ async listAllResources(options) { return this.connector.listAllResources(options); } /** * List resource templates from the server. * * @param options - Request options * @returns List of available resource templates * * @example * ```typescript * const result = await session.listResourceTemplates(); * console.log(`Available templates: ${result.resourceTemplates.length}`); * ``` */ async listResourceTemplates(options) { return this.connector.listResourceTemplates(options); } /** * Read a resource by URI. * * @param uri - URI of the resource to read * @param options - Request options * @returns Resource content * * @example * ```typescript * const resource = await session.readResource("file:///path/to/file.txt"); * console.log(resource.contents); * ``` */ async readResource(uri, options) { return this.connector.readResource(uri, options); } /** * Subscribe to resource updates. * * @param uri - URI of the resource to subscribe to * @param options - Request options * * @example * ```typescript * await session.subscribeToResource("file:///path/to/file.txt"); * // Now you'll receive notifications when this resource changes * ``` */ async subscribeToResource(uri, options) { return this.connector.subscribeToResource(uri, options); } /** * Unsubscribe from resource updates. * * @param uri - URI of the resource to unsubscribe from * @param options - Request options * * @example * ```typescript * await session.unsubscribeFromResource("file:///path/to/file.txt"); * ``` */ async unsubscribeFromResource(uri, options) { return this.connector.unsubscribeFromResource(uri, options); } /** * List available prompts from the server. * * @returns List of available prompts * * @example * ```typescript * const result = await session.listPrompts(); * console.log(`Available prompts: ${result.prompts.length}`); * ``` */ async listPrompts() { return this.connector.listPrompts(); } /** * Get a specific prompt with arguments. * * @param name - Name of the prompt to get * @param args - Arguments for the prompt * @returns Prompt result * * @example * ```typescript * const prompt = await session.getPrompt("greeting", { name: "Alice" }); * console.log(prompt.messages); * ``` */ async getPrompt(name, args) { return this.connector.getPrompt(name, args); } /** * Send a raw request through the client. * * @param method - MCP method name * @param params - Request parameters * @param options - Request options * @returns Response from the server * * @example * ```typescript * const result = await session.request("custom/method", { key: "value" }); * ``` */ async request(method, params = null, options) { return this.connector.request(method, params, options); } }; // src/client/base.ts var BaseMCPClient = class { static { __name(this, "BaseMCPClient"); } config = {}; sessions = {}; activeSessions = []; constructor(config) { if (config) { this.config = config; } } static fromDict(_cfg) { throw new Error("fromDict must be implemented by concrete class"); } addServer(name, serverConfig) { this.config.mcpServers = this.config.mcpServers || {}; this.config.mcpServers[name] = serverConfig; Tel.getInstance().trackClientAddServer(name, serverConfig); } removeServer(name) { if (this.config.mcpServers?.[name]) { delete this.config.mcpServers[name]; this.activeSessions = this.activeSessions.filter((n) => n !== name); Tel.getInstance().trackClientRemoveServer(name); } } getServerNames() { return Object.keys(this.config.mcpServers ?? {}); } getServerConfig(name) { return this.config.mcpServers?.[name]; } getConfig() { return this.config ?? {}; } async createSession(serverName, autoInitialize = true) { const servers = this.config.mcpServers ?? {}; if (Object.keys(servers).length === 0) { logger.warn("No MCP servers defined in config"); } if (!servers[serverName]) { throw new Error(`Server '${serverName}' not found in config`); } const connector = this.createConnectorFromConfig(servers[serverName]); const session = new MCPSession(connector); if (autoInitialize) { await session.initialize(); } this.sessions[serverName] = session; if (!this.activeSessions.includes(serverName)) { this.activeSessions.push(serverName); } return session; } async createAllSessions(autoInitialize = true) { const servers = this.config.mcpServers ?? {}; if (Object.keys(servers).length === 0) { logger.warn("No MCP servers defined in config"); } for (const name of Object.keys(servers)) { await this.createSession(name, autoInitialize); } return this.sessions; } getSession(serverName) { const session = this.sessions[serverName]; if (!session) { return null; } return session; } requireSession(serverName) { const session = this.sessions[serverName]; if (!session) { throw new Error( `Session '${serverName}' not found. Available sessions: ${this.activeSessions.join(", ") || "none"}` ); } return session; } getAllActiveSessions() { return Object.fromEntries( this.activeSessions.map((n) => [n, this.sessions[n]]) ); } async closeSession(serverName) { const session = this.sessions[serverName]; if (!session) { logger.warn( `No session exists for server ${serverName}, nothing to close` ); return; } try { logger.debug(`Closing session for server ${serverName}`); await session.disconnect(); } catch (e) { logger.error(`Error closing session for server '${serverName}': ${e}`); } finally { delete this.sessions[serverName]; this.activeSessions = this.activeSessions.filter((n) => n !== serverName); } } async closeAllSessions() { const serverNames = Object.keys(this.sessions); const errors = []; for (const serverName of serverNames) { try { logger.debug(`Closing session for server ${serverName}`); await this.closeSession(serverName); } catch (e) { const errorMsg = `Failed to close session for server '${serverName}': ${e}`; logger.error(errorMsg); errors.push(errorMsg); } } if (errors.length) { logger.error( `Encountered ${errors.length} errors while closing sessions` ); } else { logger.debug("All sessions closed successfully"); } } }; // src/connectors/base.ts import { ListRootsRequestSchema, CreateMessageRequestSchema, ElicitRequestSchema } from "@mcp-use/modelcontextprotocol-sdk/types.js"; var BaseConnector = class { static { __name(this, "BaseConnector"); } client = null; connectionManager = null; toolsCache = null; capabilitiesCache = null; serverInfoCache = null; connected = false; opts; notificationHandlers = []; rootsCache = []; constructor(opts = {}) { this.opts = opts; if (opts.roots) { this.rootsCache = [...opts.roots]; } } /** * Track connector initialization event * Should be called by subclasses after successful connection */ trackConnectorInit(data) { const connectorType = this.constructor.name; Telemetry.getInstance().trackConnectorInit({ connectorType, ...data }).catch((e) => logger.debug(`Failed to track connector init: ${e}`)); } /** * Register a handler for server notifications * * @param handler - Function to call when a notification is received * * @example * ```typescript * connector.onNotification((notification) => { * console.log(`Received: ${notification.method}`, notification.params); * }); * ``` */ onNotification(handler) { this.notificationHandlers.push(handler); if (this.client) { this.setupNotificationHandler(); } } /** * Internal: wire notification handlers to the SDK client * Includes automatic handling for list_changed notifications per MCP spec */ setupNotificationHandler() { if (!this.client) return; this.client.fallbackNotificationHandler = async (notification) => { switch (notification.method) { case "notifications/tools/list_changed": await this.refreshToolsCache(); break; case "notifications/resources/list_changed": await this.onResourcesListChanged(); break; case "notifications/prompts/list_changed": await this.onPromptsListChanged(); break; default: break; } for (const handler of this.notificationHandlers) { try { await handler(notification); } catch (err) { logger.error("Error in notification handler:", err); } } }; } /** * Auto-refresh tools cache when server sends tools/list_changed notification */ async refreshToolsCache() { if (!this.client) return; try { logger.debug( "[Auto] Refreshing tools cache due to list_changed notification" ); const result = await this.client.listTools(); this.toolsCache = result.tools ?? []; logger.debug( `[Auto] Refreshed tools cache: ${this.toolsCache.length} tools` ); } catch (err) { logger.warn("[Auto] Failed to refresh tools cache:", err); } } /** * Called when server sends resources/list_changed notification * Resources aren't cached by default, but we log for user awareness */ async onResourcesListChanged() { logger.debug( "[Auto] Resources list changed - clients should re-fetch if needed" ); } /** * Called when server sends prompts/list_changed notification * Prompts aren't cached by default, but we log for user awareness */ async onPromptsListChanged() { logger.debug( "[Auto] Prompts list changed - clients should re-fetch if needed" ); } /** * Set roots and notify the server. * Roots represent directories or files that the client has access to. * * @param roots - Array of Root objects with `uri` (must start with "file://") and optional `name` * * @example * ```typescript * await connector.setRoots([ * { uri: "file:///home/user/project", name: "My Project" }, * { uri: "file:///home/user/data" } * ]); * ``` */ async setRoots(roots) { this.rootsCache = [...roots]; if (this.client) { logger.debug( `Sending roots/list_changed notification with ${roots.length} root(s)` ); await this.client.sendRootsListChanged(); } } /** * Get the current roots. */ getRoots() { return [...this.rootsCache]; } /** * Internal: set up roots/list request handler. * This is called after the client connects to register the handler for server requests. */ setupRootsHandler() { if (!this.client) return; this.client.setRequestHandler( ListRootsRequestSchema, async (_request, _extra) => { logger.debug( `Server requested roots list, returning ${this.rootsCache.length} root(s)` ); return { roots: this.rootsCache }; } ); } /** * Internal: set up sampling/createMessage request handler. * This is called after the client connects to register the handler for sampling requests. */ setupSamplingHandler() { if (!this.client) { logger.debug("setupSamplingHandler: No client available"); return; } if (!this.opts.samplingCallback) { logger.debug("setupSamplingHandler: No sampling callback provided"); return; } logger.debug("setupSamplingHandler: Setting up sampling request handler"); this.client.setRequestHandler( CreateMessageRequestSchema, async (request, _extra) => { logger.debug("Server requested sampling, forwarding to callback"); return await this.opts.samplingCallback(request.params); } ); logger.debug( "setupSamplingHandler: Sampling handler registered successfully" ); } /** * Internal: set up elicitation/create request handler. * This is called after the client connects to register the handler for elicitation requests. */ setupElicitationHandler() { if (!this.client) { logger.debug("setupElicitationHandler: No client available"); return; } if (!this.opts.elicitationCallback) { logger.debug("setupElicitationHandler: No elicitation callback provided"); return; } logger.debug( "setupElicitationHandler: Setting up elicitation request handler" ); this.client.setRequestHandler( ElicitRequestSchema, async (request, _extra) => { logger.debug("Server requested elicitation, forwarding to callback"); return await this.opts.elicitationCallback(request.params); } ); logger.debug( "setupElicitationHandler: Elicitation handler registered successfully" ); } /** Disconnect and release resources. */ async disconnect() { if (!this.connected) { logger.debug("Not connected to MCP implementation"); return; } logger.debug("Disconnecting from MCP implementation"); await this.cleanupResources(); this.connected = false; logger.debug("Disconnected from MCP implementation"); } /** Check if the client is connected */ get isClientConnected() { return this.client != null; } /** * Initialise the MCP session **after** `connect()` has succeeded. * * In the SDK, `Client.connect(transport)` automatically performs the * protocol‑level `initialize` handshake, so we only need to cache the list of * tools and expose some server info. */ async initialize(defaultRequestOptions = this.opts.defaultRequestOptions ?? {}) { if (!this.client) { throw new Error("MCP client is not connected"); } logger.debug("Caching server capabilities & tools"); const capabilities = this.client.getServerCapabilities(); this.capabilitiesCache = capabilities || null; const serverInfo = this.client.getServerVersion(); this.serverInfoCache = serverInfo || null; const listToolsRes = await this.client.listTools( void 0, defaultRequestOptions ); this.toolsCache = listToolsRes.tools ?? []; logger.debug(`Fetched ${this.toolsCache.length} tools from server`); logger.debug("Server capabilities:", capabilities); logger.debug("Server info:", serverInfo); return capabilities; } /** Lazily expose the cached tools list. */ get tools() { if (!this.toolsCache) { throw new Error("MCP client is not initialized; call initialize() first"); } return this.toolsCache; } /** Expose cached server capabilities. */ get serverCapabilities() { return this.capabilitiesCache || {}; } /** Expose cached server info. */ get serverInfo() { return this.serverInfoCache; } /** Call a tool on the server. */ async callTool(name, args, options) { if (!this.client) { throw new Error("MCP client is not connected"); } const enhancedOptions = options ? { ...options } : void 0; if (enhancedOptions?.resetTimeoutOnProgress && !enhancedOptions.onprogress) { enhancedOptions.onprogress = () => { }; logger.debug( `[BaseConnector] Added onprogress callback for tool '${name}' to enable progressToken` ); } logger.debug(`Calling tool '${name}' with args`, args); const res = await this.client.callTool( { name, arguments: args }, void 0, enhancedOptions ); logger.debug(`Tool '${name}' returned`, res); return res; } /** * List all available tools from the MCP server. * This method fetches fresh tools from the server, unlike the `tools` getter which returns cached tools. * * @param options - Optional request options * @returns Array of available tools */ async listTools(options) { if (!this.client) { throw new Error("MCP client is not connected"); } const result = await this.client.listTools(void 0, options); return result.tools ?? []; } /** * List resources from the server with optional pagination * * @param cursor - Optional cursor for pagination * @param options - Request options * @returns Resource list with optional nextCursor for pagination */ async listResources(cursor, options) { if (!this.client) { throw new Error("MCP client is not connected"); } logger.debug("Listing resources", cursor ? `with cursor: ${cursor}` : ""); return await this.client.listResources({ cursor }, options); } /** * List all resources from the server, automatically handling pagination * * @param options - Request options * @returns Complete list of all resources */ async listAllResources(options) { if (!this.client) { throw new Error("MCP client is not connected"); } if (!this.capabilitiesCache?.resources) { logger.debug("Server does not advertise resources capability, skipping"); return { resources: [] }; } try { logger.debug("Listing all resources (with auto-pagination)"); const allResources = []; let cursor = void 0; do { const result = await this.client.listResources({ cursor }, options); allResources.push(...result.resources || []); cursor = result.nextCursor; } while (cursor); return { resources: allResources }; } catch (err) { const error = err; if (error.code === -32601) { logger.debug("Server advertised resources but method not found"); return { resources: [] }; } throw err; } } /** * List resource templates from the server * * @param options - Request options * @returns List of available resource templates */ async listResourceTemplates(options) { if (!this.client) { throw new Error("MCP client is not connected"); } logger.debug("Listing resource templates"); return await this.client.listResourceTemplates(void 0, options); } /** Read a resource by URI. */ async readResource(uri, options) { if (!this.client) { throw new Error("MCP client is not connected"); } logger.debug(`Reading resource ${uri}`); const res = await this.client.readResource({ uri }, options); return res; } /** * Subscribe to resource updates * * @param uri - URI of the resource to subscribe to * @param options - Request options */ async subscribeToResource(uri, options) { if (!this.client) { throw new Error("MCP client is not connected"); } logger.debug(`Subscribing to resource: ${uri}`); return await this.client.subscribeResource({ uri }, options); } /** * Unsubscribe from resource updates * * @param uri - URI of the resource to unsubscribe from * @param options - Request options */ async unsubscribeFromResource(uri, options) { if (!this.client) { throw new Error("MCP client is not connected"); } logger.debug(`Unsubscribing from resource: ${uri}`); return await this.client.unsubscribeResource({ uri }, options); } async listPrompts() { if (!this.client) { throw new Error("MCP client is not connected"); } if (!this.capabilitiesCache?.prompts) { logger.debug("Server does not advertise prompts capability, skipping"); return { prompts: [] }; } try { logger.debug("Listing prompts"); return await this.client.listPrompts(); } catch (err) { const error = err; if (error.code === -32601) { logger.debug("Server advertised prompts but method not found"); return { prompts: [] }; } throw err; } } async getPrompt(name, args) { if (!this.client) { throw new Error("MCP client is not connected"); } logger.debug(`Getting prompt ${name}`); return await this.client.getPrompt({ name, arguments: args }); } /** Send a raw request through the client. */ async request(method, params = null, options) { if (!this.client) { throw new Error("MCP client is not connected"); } logger.debug(`Sending raw request '${method}' with params`, params); return await this.client.request( { method, params: params ?? {} }, void 0, options ); } /** * Helper to tear down the client & connection manager safely. */ async cleanupResources() { const issues = []; if (this.client) { try { if (typeof this.client.close === "function") { await this.client.close(); } } catch (e) { const msg = `Error closing client: ${e}`; logger.warn(msg); issues.push(msg); } finally { this.client = null; } } if (this.connectionManager) { try { await this.connectionManager.stop(); } catch (e) { const msg = `Error stopping connection manager: ${e}`; logger.warn(msg); issues.push(msg); } finally { this.connectionManager = null; } } this.toolsCache = null; if (issues.length) { logger.warn(`Resource cleanup finished with ${issues.length} issue(s)`); } } }; // src/connectors/http.ts import { Client } from "@mcp-use/modelcontextprotocol-sdk/client/index.js"; import { StreamableHTTPClientTransport, StreamableHTTPError } from "@mcp-use/modelcontextprotocol-sdk/client/streamableHttp.js"; // src/task_managers/sse.ts import { SSEClientTransport } from "@mcp-use/modelcontextprotocol-sdk/client/sse.js"; // src/task_managers/base.ts var ConnectionManager = class { static { __name(this, "ConnectionManager"); } _readyPromise; _readyResolver; _donePromise; _doneResolver; _exception = null; _connection = null; _task = null; _abortController = null; constructor() { this.reset(); } /** * Start the connection manager and establish a connection. * * @returns The established connection. * @throws If the connection cannot be established. */ async start() { this.reset(); logger.debug(`Starting ${this.constructor.name}`); this._task = this.connectionTask(); await this._readyPromise; if (this._exception) { throw this._exception; } if (this._connection === null) { throw new Error("Connection was not established"); } return this._connection; } /** * Stop the connection manager and close the connection. */ async stop() { if (this._task && this._abortController) { logger.debug(`Cancelling ${this.constructor.name} task`); this._abortController.abort(); try { await this._task; } catch (e) { if (e instanceof Error && e.name === "AbortError") { logger.debug(`${this.constructor.name} task aborted successfully`); } else { logger.warn(`Error stopping ${this.constructor.name} task: ${e}`); } } } await this._donePromise; logger.debug(`${this.constructor.name} task completed`); } /** * Reset all internal state. */ reset() { this._readyPromise = new Promise((res) => this._readyResolver = res); this._donePromise = new Promise((res) => this._doneResolver = res); this._exception = null; this._connection = null; this._task = null; this._abortController = new AbortController(); } /** * The background task responsible for establishing and maintaining the * connection until it is cancelled. */ async connectionTask() { logger.debug(`Running ${this.constructor.name} task`); try { this._connection = await this.establishConnection(); logger.debug(`${this.constructor.name} connected successfully`); this._readyResolver(); await this.waitForAbort(); } catch (err) { this._exception = err; logger.error(`Error in ${this.constructor.name} task: ${err}`); this._readyResolver(); } finally { if (this._connection !== null) { try { await this.closeConnection(this._connection); } catch (closeErr) { logger.warn( `Error closing connection in ${this.constructor.name}: ${closeErr}` ); } this._connection = null; } this._doneResolver(); } } /** * Helper that returns a promise which resolves when the abort signal fires. */ async waitForAbort() { return new Promise((_resolve, _reject) => { if (!this._abortController) { return; } const signal = this._abortController.signal; if (signal.aborted) { _resolve(); return; } const onAbort = /* @__PURE__ */ __name(() => { signal.removeEventListener("abort", onAbort); _resolve(); }, "onAbort"); signal.addEventListener("abort", onAbort); }); } }; // src/task_managers/sse.ts var SseConnectionManager = class extends ConnectionManager { static { __name(this, "SseConnectionManager"); } url; opts; _transport = null; reinitializing = false; /** * Create an SSE connection manager. * * @param url The SSE endpoint URL. * @param opts Optional transport options (auth, headers, etc.). */ constructor(url, opts) { super(); this.url = typeof url === "string" ? new URL(url) : url; this.opts = opts; } /** * Spawn a new `SSEClientTransport` and wrap it with 404 handling. * Per MCP spec, clients MUST re-initialize when receiving 404 for stale sessions. */ async establishConnection() { const transport = new SSEClientTransport(this.url, this.opts); const originalSend = transport.send.bind(transport); transport.send = async (message) => { const sendMessage = /* @__PURE__ */ __name(async (msg) => { if (Array.isArray(msg)) { for (const singleMsg of msg) { await originalSend(singleMsg); } } else { await originalSend(msg); } }, "sendMessage"); try { await sendMessage(message); } catch (error) { if (error?.code === 404 && transport.sessionId && !this.reinitializing) { logger.warn( `[SSE] Session not found (404), re-initializing per MCP spec...` ); this.reinitializing = true; try { transport.sessionId = void 0; await this.reinitialize(transport); logger.info(`[SSE] Re-initialization successful, retrying request`); await sendMessage(message); } finally { this.reinitializing = false; } } else { throw error; } } }; this._transport = transport; logger.debug(`${this.constructor.name} connected successfully`); return transport; } /** * Re-initialize the transport with a new session * This is called when the server returns 404 for a stale session */ async reinitialize(transport) { logger.debug(`[SSE] Re-initialization triggered`); } /** * Close the underlying transport and clean up resources. */ async closeConnection(_connection) { if (this._transport) { try { await this._transport.close(); } catch (e) { logger.warn(`Error closing SSE transport: ${e}`); } finally { this._transport = null; } } } }; // src/connectors/http.ts var HttpConnector = class extends BaseConnector { static { __name(this, "HttpConnector"); } baseUrl; headers; timeout; sseReadTimeout; clientInfo; preferSse; disableSseFallback; transportType = null; streamableTransport = null; constructor(baseUrl, opts = {}) { super(opts); this.baseUrl = baseUrl.replace(/\/$/, ""); this.headers = { ...opts.headers ?? {} }; if (opts.authToken) { this.headers.Authorization = `Bearer ${opts.authToken}`; } this.timeout = opts.timeout ?? 3e4; this.sseReadTimeout = opts.sseReadTimeout ?? 3e5; this.clientInfo = opts.clientInfo ?? { name: "http-connector", version: "1.0.0" }; this.preferSse = opts.preferSse ?? false; this.disableSseFallback = opts.disableSseFallback ?? false; } /** Establish connection to the MCP implementation via HTTP (streamable or SSE). */ async connect() { if (this.connected) { logger.debug("Already connected to MCP implementation"); return; } const baseUrl = this.baseUrl; if (this.preferSse) { logger.debug(`Connecting to MCP implementation via HTTP/SSE: ${baseUrl}`); await this.connectWithSse(baseUrl); return; } logger.debug(`Connecting to MCP implementation via HTTP: ${baseUrl}`); try { logger.info("\u{1F504} Attempting streamable HTTP transport..."); await this.connectWithStreamableHttp(baseUrl); logger.info("\u2705 Successfully connected via streamable HTTP"); } catch (err) { let fallbackReason = "Unknown error"; let is401Error = false; if (err instanceof StreamableHTTPError) { const streamableErr = err; is401Error = streamableErr.code === 401; if (streamableErr.code === 400 && streamableErr.message.includes("Missing session ID")) { fallbackReason = "Server requires session ID (FastMCP compatibility) - using SSE transport"; logger.warn(`\u26A0\uFE0F ${fallbackReason}`); } else if (streamableErr.code === 404 || streamableErr.code === 405) { fallbackReason = `Server returned ${streamableErr.code} - server likely doesn't support streamable HTTP`; logger.debug(fallbackReason); } else { fallbackReason = `Server returned ${streamableErr.code}: ${streamableErr.message}`; logger.debug(fallbackReason); } } else if (err instanceof Error) { const errorStr = err.toString(); const errorMsg = err.message || ""; is401Error = errorStr.includes("401") || errorMsg.includes("Unauthorized"); if (errorStr.includes("Missing session ID") || errorStr.includes("Bad Request: Missing session ID") || errorMsg.includes("FastMCP session ID error")) { fallbackReason = "Server requires session ID (FastMCP compatibility) - using SSE transport"; logger.warn(`\u26A0\uFE0F ${fallbackReason}`); } else if (errorStr.includes("405 Method Not Allowed") || errorStr.includes("404 Not Found")) { fallbackReason = "Server doesn't support streamable HTTP (405/404)"; logger.debug(fallbackReason); } else { fallbackReason = `Streamable HTTP failed: ${err.message}`; logger.debug(fallbackReason); } } if (is401Error) { logger.info("Authentication required - skipping SSE fallback"); await this.cleanupResources(); const authError = new Error("Authentication required"); authError.code = 401; throw authError; } if (this.disableSseFallback) { logger.info("SSE fallback disabled - failing connection"); await this.cleanupResources(); throw new Error( `Streamable HTTP connection failed: ${fallbackReason}. SSE fallback is disabled.` ); } logger.info("\u{1F504} Falling back to SSE transport..."); try { await this.connectWithSse(baseUrl); } catch (sseErr) { logger.error(`Failed to connect with both transports:`); logger.error(` Streamable HTTP: ${fallbackReason}`); logger.error(` SSE: ${sseErr}`); await this.cleanupResources(); const sseIs401 = sseErr?.message?.includes("401") || sseErr?.message?.includes("Unauthorized"); if (sseIs401) { const authError = new Error("Authentication required"); authError.code = 401; throw authError; } throw new Error( "Could not connect to server with any available transport" ); } } } async connectWithStreamableHttp(baseUrl) { try { const streamableTransport = new StreamableHTTPClientTransport( new URL(baseUrl), { authProvider: this.opts.authProvider, // ← Pass OAuth provider to SDK requestInit: { headers: this.headers }, // Pass through reconnection options reconnectionOptions: { maxReconnectionDelay: 3e4, initialReconnectionDelay: 1e3, reconnectionDelayGrowFactor: 1.5, maxRetries: 2 } // Don't pass sessionId - let the SDK generate it automatically during connect() } ); let transport = streamableTransport; if (this.opts.wrapTransport) { const serverId = this.baseUrl; transport = this.opts.wrapTransport( transport, serverId ); } const clientOptions = { ...this.opts.clientOptions || {}, capabilities: { ...this.opts.clientOptions?.capabilities || {}, roots: { listChanged: true }, // Always advertise roots capability // Add sampling capability if callback is provided ...this.opts.samplingCallback ? { sampling: {} } : {}, // Add elicitation capability if callback is provided ...this.opts.elicitationCallback ? { elicitation: { form: {}, url: {} } } : {} } }; logger.debug( `Creating Client with capabilities:`, JSON.stringify(clientOptions.capabilities, null, 2) ); this.client = new Client(this.clientInfo, clientOptions); this.setupRootsHandler(); logger.debug("Roots handler registered before connect"); try { await this.client.connect(transport, { timeout: Math.min(this.timeout, 3e3) }); const sessionId = streamableTransport.sessionId; if (sessionId) { logger.debug(`Session ID obtained: ${sessionId}`); } else { logger.warn( "Session ID not available after connect - this may cause issues with SSE stream" ); } } catch (connectErr) { if (connectErr instanceof Error) { const errMsg = connectErr.message || connectErr.toString(); if (errMsg.includes("Missing session ID") || errMsg.includes("Bad Request: Missing session ID") || errMsg.includes("Mcp-Session-Id header is required")) { const wrappedError = new Error( `Session ID error: ${errMsg}. The SDK should automatically extract session ID from initialize response.` ); wrappedError.cause = connectErr; throw wrappedError; } } throw connectErr; } this.streamableTransport = streamableTransport; this.connectionManager = { stop: /* @__PURE__ */ __name(async () => { if (this.streamableTransport) { try { await this.streamableTransport.terminateSession(); await this.streamableTransport.close(); } catch (e) { logger.warn(`Error closing Streamable HTTP transport: ${e}`); } finally { this.streamableTransport = null; } } }, "stop") }; this.connected = true; this.transportType = "streamable-http"; this.setupNotificationHandler(); this.setupSamplingHandler(); this.setupElicitationHandler(); logger.debug( `Successfully connected to MCP implementation via streamable HTTP: ${baseUrl}` ); this.trackConnectorInit({ serverUrl: this.baseUrl, publicIdentifier: `${this.baseUrl} (streamable-http)` }); } catch (err) { await this.cleanupResources(); throw err; } } async connectWithSse(baseUrl) { try { this.connectionManager = new SseConnectionManager(baseUrl, { authProvider: this.opts.authProvider, // ← Pass OAuth provider to SDK (same as streamable HTTP) requestInit: { headers: this.headers } }); let transport = await this.connectionManager.start(); if (this.opts.wrapTransport) { const serverId = this.baseUrl; transport = this.opts.wrapTransport(transport, serverId); } const clientOptions = { ...this.opts.clientOptions || {}, capabilities: { ...this.opts.clientOptions?.capabilities || {}, roots: { listChanged: true }, // Always advertise roots capability // Add sampling capability if callback is provided ...this.opts.samplingCallback ? { sampling: {} } : {}, // Add elicitation capability if callback is provided ...this.opts.elicitationCallback ? { elicitation: { form: {}, url: {} } } : {} } }; logger.debug( `Creating Client with capabilities (SSE):`, JSON.stringify(clientOptions.capabilities, null, 2) ); this.client = new Client(this.clientInfo, clientOptions); this.setupRootsHandler(); logger.debug("Roots handler registered before connect (SSE)"); await this.client.connect(transport); this.connected = true; this.transportType = "sse"; this.setupNotificationHandler(); this.setupSamplingHandler(); this.setupElicitationHandler(); logger.debug( `Successfully connected to MCP implementation via HTTP/SSE: ${baseUrl}` ); this.trackConnectorInit({ serverUrl: this.baseUrl, publicIdentifier: `${this.baseUrl} (sse)` }); } catch (err) { await this.cleanupResources(); throw err; } } get publicIdentifier() { return { type: "http", url: this.baseUrl, transport: this.transportType || "unknown" }; } /** * Get the transport type being used (streamable-http or sse) */ getTransportType() { return this.transportType; } }; export { MCPSession, BaseMCPClient, BaseConnector, ConnectionManager, HttpConnector };