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.

952 lines (943 loc) 29.7 kB
import { BaseConnector } from "./chunk-IL42COOI.js"; import { Tel } from "./chunk-43HAMRQH.js"; import { logger } from "./chunk-U7F22OTV.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/http.ts import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; // src/task_managers/sse.ts import { SSEClientTransport } from "@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; gatewayUrl; serverId; transportType = null; streamableTransport = null; constructor(baseUrl, opts = {}) { super(opts); const originalUrl = baseUrl.replace(/\/$/, ""); this.gatewayUrl = opts.gatewayUrl; this.serverId = opts.serverId; if (this.gatewayUrl) { this.baseUrl = this.gatewayUrl.replace(/\/$/, ""); this.headers = { ...opts.headers ?? {} }; this.headers["X-Target-URL"] = originalUrl; if (this.serverId) { this.headers["X-Server-Id"] = this.serverId; } } else { this.baseUrl = originalUrl; this.headers = { ...opts.headers ?? {} }; } if (opts.authToken) { this.headers.Authorization = `Bearer ${opts.authToken}`; } this.timeout = opts.timeout ?? 1e4; 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) { console.log("error in http connector connect", err); let fallbackReason = "Unknown error"; let is401Error = false; let httpStatusCode; let streamableErr = null; if (err instanceof StreamableHTTPError) { streamableErr = err; } else if (err instanceof Error && err.cause instanceof StreamableHTTPError) { streamableErr = err.cause; } if (streamableErr) { is401Error = streamableErr.code === 401; httpStatusCode = streamableErr.code; console.log("Captured HTTP status code:", httpStatusCode); 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; } logger.info("\u{1F504} Falling back to SSE transport..."); try { await this.connectWithSse(baseUrl); } catch (sseErr) { console.error(`Failed to connect with both transports:`); console.error(` Streamable HTTP: ${fallbackReason}`); console.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; } const finalError = new Error( `Could not connect to server with any available transport. Streamable HTTP: ${fallbackReason}` ); if (httpStatusCode !== void 0) { Object.defineProperty(finalError, "code", { value: httpStatusCode, writable: false, enumerable: true, configurable: true }); logger.debug( `Preserving HTTP status code ${httpStatusCode} in error for proxy fallback detection` ); } throw finalError; } } } async connectWithStreamableHttp(baseUrl) { try { console.log(`[HttpConnector] Connecting with Streamable HTTP:`); console.log(` Base URL: ${baseUrl}`); console.log(` Original URL: ${this.baseUrl}`); console.log(` Gateway URL: ${this.gatewayUrl || "none"}`); console.log( ` Auth Provider URL: ${this.opts.authProvider?.serverUrl || "none"}` ); console.log(` Headers: ${JSON.stringify(this.headers)}`); 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 // Disable automatic reconnection - let higher-level logic handle it } // 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: this.timeout }); 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, ConnectionManager, HttpConnector };