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.

502 lines (499 loc) • 15.3 kB
import { Telemetry } from "./chunk-43HAMRQH.js"; import { logger } from "./chunk-U7F22OTV.js"; import { __name } from "./chunk-3GQAWCBQ.js"; // src/connectors/base.ts import { CreateMessageRequestSchema, ElicitRequestSchema, ListRootsRequestSchema } from "@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 = {}) { const finalOpts = { ...opts, onSampling: opts.onSampling ?? opts.samplingCallback }; if (opts.samplingCallback && !opts.onSampling) { console.warn( '[BaseConnector] The "samplingCallback" option is deprecated. Use "onSampling" instead.' ); } this.opts = finalOpts; if (finalOpts.roots) { this.rootsCache = [...finalOpts.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; } const samplingCallback = this.opts.onSampling ?? this.opts.samplingCallback; if (!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 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)`); } } }; export { BaseConnector };