UNPKG

mcp-use

Version:

Opinionated MCP Framework for TypeScript (@modelcontextprotocol/sdk compatible) - Build MCP Agents and Clients + MCP Servers with support for MCP-UI.

1,562 lines (1,546 loc) 160 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/logging.ts async function getNodeModules() { if (typeof process !== "undefined" && process.platform) { try { const fs3 = await import("fs"); const path3 = await import("path"); return { fs: fs3.default, path: path3.default }; } catch { return { fs: null, path: null }; } } return { fs: null, path: null }; } function loadWinstonSync() { if (typeof require !== "undefined") { try { winston = require("winston"); } catch { } } } async function getWinston() { if (!winston) { winston = await import("winston"); } return winston; } function isNodeJSEnvironment() { try { if (typeof navigator !== "undefined" && navigator.userAgent?.includes("Cloudflare-Workers")) { return false; } if (typeof globalThis.EdgeRuntime !== "undefined" || typeof globalThis.Deno !== "undefined") { return false; } const hasNodeGlobals = typeof process !== "undefined" && typeof process.platform !== "undefined" && typeof __dirname !== "undefined"; return hasNodeGlobals; } catch { return false; } } function resolveLevel(env) { const envValue = typeof process !== "undefined" && process.env ? env : void 0; switch (envValue?.trim()) { case "2": return "debug"; case "1": return "info"; default: return "info"; } } var winston, DEFAULT_LOGGER_NAME, SimpleConsoleLogger, Logger, logger; var init_logging = __esm({ "src/logging.ts"() { "use strict"; __name(getNodeModules, "getNodeModules"); winston = null; __name(loadWinstonSync, "loadWinstonSync"); __name(getWinston, "getWinston"); DEFAULT_LOGGER_NAME = "mcp-use"; __name(isNodeJSEnvironment, "isNodeJSEnvironment"); SimpleConsoleLogger = class { static { __name(this, "SimpleConsoleLogger"); } _level; name; constructor(name = DEFAULT_LOGGER_NAME, level = "info") { this.name = name; this._level = level; } shouldLog(level) { const levels = [ "error", "warn", "info", "http", "verbose", "debug", "silly" ]; const currentIndex = levels.indexOf(this._level); const messageIndex = levels.indexOf(level); return messageIndex <= currentIndex; } formatMessage(level, message) { const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }); return `${timestamp} [${this.name}] ${level}: ${message}`; } error(message) { if (this.shouldLog("error")) { console.error(this.formatMessage("error", message)); } } warn(message) { if (this.shouldLog("warn")) { console.warn(this.formatMessage("warn", message)); } } info(message) { if (this.shouldLog("info")) { console.info(this.formatMessage("info", message)); } } debug(message) { if (this.shouldLog("debug")) { console.debug(this.formatMessage("debug", message)); } } http(message) { if (this.shouldLog("http")) { console.log(this.formatMessage("http", message)); } } verbose(message) { if (this.shouldLog("verbose")) { console.log(this.formatMessage("verbose", message)); } } silly(message) { if (this.shouldLog("silly")) { console.log(this.formatMessage("silly", message)); } } // Make it compatible with Winston interface get level() { return this._level; } set level(newLevel) { this._level = newLevel; } }; __name(resolveLevel, "resolveLevel"); Logger = class { static { __name(this, "Logger"); } static instances = {}; static simpleInstances = {}; static currentFormat = "minimal"; static get(name = DEFAULT_LOGGER_NAME) { if (!isNodeJSEnvironment()) { if (!this.simpleInstances[name]) { const debugEnv = typeof process !== "undefined" && process.env?.DEBUG || void 0; this.simpleInstances[name] = new SimpleConsoleLogger( name, resolveLevel(debugEnv) ); } return this.simpleInstances[name]; } if (!this.instances[name]) { if (!winston) { throw new Error("Winston not loaded - call Logger.configure() first"); } const { createLogger, format } = winston; const { combine, timestamp, label, colorize, splat } = format; this.instances[name] = createLogger({ level: resolveLevel(process.env.DEBUG), format: combine( colorize(), splat(), label({ label: name }), timestamp({ format: "HH:mm:ss" }), this.getFormatter() ), transports: [] }); } return this.instances[name]; } static getFormatter() { if (!winston) { throw new Error("Winston not loaded"); } const { format } = winston; const { printf } = format; const minimalFormatter = printf(({ level, message, label, timestamp }) => { return `${timestamp} [${label}] ${level}: ${message}`; }); const detailedFormatter = printf(({ level, message, label, timestamp }) => { return `${timestamp} [${label}] ${level.toUpperCase()}: ${message}`; }); const emojiFormatter = printf(({ level, message, label, timestamp }) => { return `${timestamp} [${label}] ${level.toUpperCase()}: ${message}`; }); switch (this.currentFormat) { case "minimal": return minimalFormatter; case "detailed": return detailedFormatter; case "emoji": return emojiFormatter; default: return minimalFormatter; } } static async configure(options = {}) { const { level, console: console2 = true, file, format = "minimal" } = options; const debugEnv = typeof process !== "undefined" && process.env?.DEBUG || void 0; const resolvedLevel = level ?? resolveLevel(debugEnv); this.currentFormat = format; if (!isNodeJSEnvironment()) { Object.values(this.simpleInstances).forEach((logger2) => { logger2.level = resolvedLevel; }); return; } await getWinston(); if (!winston) { throw new Error("Failed to load winston"); } const root = this.get(); root.level = resolvedLevel; const winstonRoot = root; winstonRoot.clear(); if (console2) { winstonRoot.add(new winston.transports.Console()); } if (file) { const { fs: nodeFs, path: nodePath } = await getNodeModules(); if (nodeFs && nodePath) { const dir = nodePath.dirname(nodePath.resolve(file)); if (!nodeFs.existsSync(dir)) { nodeFs.mkdirSync(dir, { recursive: true }); } winstonRoot.add(new winston.transports.File({ filename: file })); } } const { format: winstonFormat } = winston; const { combine, timestamp, label, colorize, splat } = winstonFormat; Object.values(this.instances).forEach((logger2) => { if (logger2 && "format" in logger2) { logger2.level = resolvedLevel; logger2.format = combine( colorize(), splat(), label({ label: DEFAULT_LOGGER_NAME }), timestamp({ format: "HH:mm:ss" }), this.getFormatter() ); } }); } static setDebug(enabled) { let level; if (enabled === 2 || enabled === true) level = "debug"; else if (enabled === 1) level = "info"; else level = "info"; Object.values(this.simpleInstances).forEach((logger2) => { logger2.level = level; }); Object.values(this.instances).forEach((logger2) => { if (logger2) { logger2.level = level; } }); if (typeof process !== "undefined" && process.env) { process.env.DEBUG = enabled ? enabled === true ? "2" : String(enabled) : "0"; } } static setFormat(format) { this.currentFormat = format; this.configure({ format }); } }; if (isNodeJSEnvironment()) { loadWinstonSync(); if (winston) { Logger.configure(); } } logger = Logger.get(); } }); // src/observability/langfuse.ts var langfuse_exports = {}; __export(langfuse_exports, { initializeLangfuse: () => initializeLangfuse, langfuseClient: () => langfuseClient, langfuseHandler: () => langfuseHandler, langfuseInitPromise: () => langfuseInitPromise }); async function initializeLangfuse(agentId, metadata, metadataProvider, tagsProvider) { try { const langfuseModule = await import("langfuse-langchain").catch(() => null); if (!langfuseModule) { logger.debug( "Langfuse package not installed - tracing disabled. Install with: npm install @langfuse/langchain" ); return; } const { CallbackHandler } = langfuseModule; class LoggingCallbackHandler extends CallbackHandler { static { __name(this, "LoggingCallbackHandler"); } agentId; metadata; metadataProvider; tagsProvider; verbose; constructor(config3, agentId2, metadata2, metadataProvider2, tagsProvider2) { super(config3); this.agentId = agentId2; this.metadata = metadata2; this.metadataProvider = metadataProvider2; this.tagsProvider = tagsProvider2; this.verbose = config3?.verbose ?? false; } // Override to add custom metadata to traces async handleChainStart(chain, inputs, runId, parentRunId, tags, metadata2, name, kwargs) { logger.debug("Langfuse: Chain start intercepted"); const customTags = this.getCustomTags(); const metadataToAdd = this.getMetadata(); const enhancedTags = [...tags || [], ...customTags]; const enhancedMetadata = { ...metadata2 || {}, ...metadataToAdd }; if (this.verbose) { logger.debug( `Langfuse: Chain start with custom tags: ${JSON.stringify(enhancedTags)}` ); logger.debug( `Langfuse: Chain start with metadata: ${JSON.stringify(enhancedMetadata)}` ); } return super.handleChainStart( chain, inputs, runId, parentRunId, enhancedTags, enhancedMetadata, name, kwargs ); } // Get custom tags based on environment and agent configuration getCustomTags() { const tags = []; const env = this.getEnvironmentTag(); if (env) { tags.push(`env:${env}`); } if (this.agentId) { tags.push(`agent_id:${this.agentId}`); } if (this.tagsProvider) { const providerTags = this.tagsProvider(); if (providerTags && providerTags.length > 0) { tags.push(...providerTags); } } return tags; } // Get metadata getMetadata() { const metadata2 = {}; const env = this.getEnvironmentTag(); if (env) { metadata2.env = env; } if (this.agentId) { metadata2.agent_id = this.agentId; } if (this.metadata) { Object.assign(metadata2, this.metadata); } if (this.metadataProvider) { const dynamicMetadata = this.metadataProvider(); if (dynamicMetadata) { Object.assign(metadata2, dynamicMetadata); } } return metadata2; } // Determine environment tag based on MCP_USE_AGENT_ENV getEnvironmentTag() { const agentEnv = process.env.MCP_USE_AGENT_ENV; if (!agentEnv) { return "unknown"; } const envLower = agentEnv.toLowerCase(); if (envLower === "local" || envLower === "development") { return "local"; } else if (envLower === "production" || envLower === "prod") { return "production"; } else if (envLower === "staging" || envLower === "stage") { return "staging"; } else if (envLower === "hosted" || envLower === "cloud") { return "hosted"; } return envLower.replace(/[^a-z0-9_-]/g, "_"); } async handleLLMStart(...args) { logger.debug("Langfuse: LLM start intercepted"); if (this.verbose) { logger.debug(`Langfuse: LLM start args: ${JSON.stringify(args)}`); } return super.handleLLMStart(...args); } async handleToolStart(...args) { logger.debug("Langfuse: Tool start intercepted"); if (this.verbose) { logger.debug(`Langfuse: Tool start args: ${JSON.stringify(args)}`); } return super.handleToolStart(...args); } async handleRetrieverStart(...args) { logger.debug("Langfuse: Retriever start intercepted"); if (this.verbose) { logger.debug( `Langfuse: Retriever start args: ${JSON.stringify(args)}` ); } return super.handleRetrieverStart(...args); } async handleAgentAction(...args) { logger.debug("Langfuse: Agent action intercepted"); if (this.verbose) { logger.debug(`Langfuse: Agent action args: ${JSON.stringify(args)}`); } return super.handleAgentAction(...args); } async handleAgentEnd(...args) { logger.debug("Langfuse: Agent end intercepted"); if (this.verbose) { logger.debug(`Langfuse: Agent end args: ${JSON.stringify(args)}`); } return super.handleAgentEnd(...args); } } const initialMetadata = metadata || (metadataProvider ? metadataProvider() : {}); const initialTags = tagsProvider ? tagsProvider() : []; const config2 = { publicKey: process.env.LANGFUSE_PUBLIC_KEY, secretKey: process.env.LANGFUSE_SECRET_KEY, baseUrl: process.env.LANGFUSE_HOST || process.env.LANGFUSE_BASEURL || "https://cloud.langfuse.com", flushAt: Number.parseInt(process.env.LANGFUSE_FLUSH_AT || "15"), flushInterval: Number.parseInt( process.env.LANGFUSE_FLUSH_INTERVAL || "10000" ), release: process.env.LANGFUSE_RELEASE, requestTimeout: Number.parseInt( process.env.LANGFUSE_REQUEST_TIMEOUT || "10000" ), enabled: process.env.LANGFUSE_ENABLED !== "false", // Set trace name - can be customized via metadata.trace_name or defaults to 'mcp-use-agent' traceName: initialMetadata.trace_name || process.env.LANGFUSE_TRACE_NAME || "mcp-use-agent", // Pass sessionId, userId, and tags to the handler sessionId: initialMetadata.session_id || void 0, userId: initialMetadata.user_id || void 0, tags: initialTags.length > 0 ? initialTags : void 0, metadata: initialMetadata || void 0 }; logger.debug( "Langfuse handler config:", JSON.stringify( { traceName: config2.traceName, sessionId: config2.sessionId, userId: config2.userId, tags: config2.tags }, null, 2 ) ); langfuseState.handler = new LoggingCallbackHandler( config2, agentId, metadata, metadataProvider, tagsProvider ); logger.debug( "Langfuse observability initialized successfully with logging enabled" ); try { const langfuseCore = await import("langfuse").catch(() => null); if (langfuseCore) { const { Langfuse } = langfuseCore; langfuseState.client = new Langfuse({ publicKey: process.env.LANGFUSE_PUBLIC_KEY, secretKey: process.env.LANGFUSE_SECRET_KEY, baseUrl: process.env.LANGFUSE_HOST || "https://cloud.langfuse.com" }); logger.debug("Langfuse client initialized"); } } catch (error) { logger.debug(`Langfuse client initialization failed: ${error}`); } } catch (error) { logger.debug(`Langfuse initialization error: ${error}`); } } var import_dotenv, langfuseDisabled, langfuseState, langfuseHandler, langfuseClient, langfuseInitPromise; var init_langfuse = __esm({ "src/observability/langfuse.ts"() { "use strict"; import_dotenv = require("dotenv"); init_logging(); (0, import_dotenv.config)(); langfuseDisabled = process.env.MCP_USE_LANGFUSE?.toLowerCase() === "false"; langfuseState = { handler: null, client: null, initPromise: null }; __name(initializeLangfuse, "initializeLangfuse"); if (langfuseDisabled) { logger.debug( "Langfuse tracing disabled via MCP_USE_LANGFUSE environment variable" ); } else if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) { logger.debug( "Langfuse API keys not found - tracing disabled. Set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY to enable" ); } else { langfuseState.initPromise = initializeLangfuse(); } langfuseHandler = /* @__PURE__ */ __name(() => langfuseState.handler, "langfuseHandler"); langfuseClient = /* @__PURE__ */ __name(() => langfuseState.client, "langfuseClient"); langfuseInitPromise = /* @__PURE__ */ __name(() => langfuseState.initPromise, "langfuseInitPromise"); } }); // src/browser.ts var browser_exports = {}; __export(browser_exports, { BaseAdapter: () => BaseAdapter, BaseConnector: () => BaseConnector, BrowserOAuthClientProvider: () => BrowserOAuthClientProvider, HttpConnector: () => HttpConnector, LangChainAdapter: () => LangChainAdapter, Logger: () => Logger, MCPAgent: () => MCPAgent, MCPClient: () => BrowserMCPClient, MCPSession: () => MCPSession, ObservabilityManager: () => ObservabilityManager, RemoteAgent: () => RemoteAgent, WebSocketConnector: () => WebSocketConnector, createReadableStreamFromGenerator: () => createReadableStreamFromGenerator, logger: () => logger, onMcpAuthorization: () => onMcpAuthorization, streamEventsToAISDK: () => streamEventsToAISDK, streamEventsToAISDKWithTools: () => streamEventsToAISDKWithTools }); module.exports = __toCommonJS(browser_exports); // src/connectors/http.ts var import_client = require("@modelcontextprotocol/sdk/client/index.js"); var import_streamableHttp2 = require("@modelcontextprotocol/sdk/client/streamableHttp.js"); init_logging(); // src/task_managers/sse.ts var import_sse = require("@modelcontextprotocol/sdk/client/sse.js"); init_logging(); // src/task_managers/base.ts init_logging(); 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; /** * 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 start the connection. */ async establishConnection() { this._transport = new import_sse.SSEClientTransport(this.url, this.opts); logger.debug(`${this.constructor.name} connected successfully`); return this._transport; } /** * 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/task_managers/streamable_http.ts var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js"); init_logging(); var StreamableHttpConnectionManager = class extends ConnectionManager { static { __name(this, "StreamableHttpConnectionManager"); } url; opts; _transport = null; /** * Create a Streamable HTTP connection manager. * * @param url The HTTP 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 `StreamableHTTPClientTransport` and return it. * The Client.connect() method will handle starting the transport. */ async establishConnection() { this._transport = new import_streamableHttp.StreamableHTTPClientTransport(this.url, this.opts); logger.debug(`${this.constructor.name} created successfully`); return this._transport; } /** * 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 Streamable HTTP transport: ${e}`); } finally { this._transport = null; } } } /** * Get the session ID from the transport if available. */ get sessionId() { return this._transport?.sessionId; } }; // src/connectors/base.ts init_logging(); var BaseConnector = class { static { __name(this, "BaseConnector"); } client = null; connectionManager = null; toolsCache = null; capabilitiesCache = null; connected = false; opts; constructor(opts = {}) { this.opts = opts; } /** 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; 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); 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; } /** Call a tool on the server. */ async callTool(name, args, options) { if (!this.client) { throw new Error("MCP client is not connected"); } logger.debug(`Calling tool '${name}' with args`, args); const res = await this.client.callTool( { name, arguments: args }, void 0, options ); logger.debug(`Tool '${name}' returned`, res); return res; } /** * 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) { if (err.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) { if (err.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 var HttpConnector = class extends BaseConnector { static { __name(this, "HttpConnector"); } baseUrl; headers; timeout; sseReadTimeout; clientInfo; preferSse; transportType = 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; } /** 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 import_streamableHttp2.StreamableHTTPError) { is401Error = err.code === 401; if (err.code === 400 && err.message.includes("Missing session ID")) { fallbackReason = "Server requires session ID (FastMCP compatibility) - using SSE transport"; logger.warn(`\u26A0\uFE0F ${fallbackReason}`); } else if (err.code === 404 || err.code === 405) { fallbackReason = `Server returned ${err.code} - server likely doesn't support streamable HTTP`; logger.debug(fallbackReason); } else { fallbackReason = `Server returned ${err.code}: ${err.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) { 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 { this.connectionManager = new StreamableHttpConnectionManager(baseUrl, { authProvider: this.opts.authProvider, // ← Pass OAuth provider to SDK requestInit: { headers: this.headers }, // Pass through timeout and other options reconnectionOptions: { maxReconnectionDelay: 3e4, initialReconnectionDelay: 1e3, reconnectionDelayGrowFactor: 1.5, maxRetries: 2 } }); const transport = await this.connectionManager.start(); this.client = new import_client.Client(this.clientInfo, this.opts.clientOptions); try { await this.client.connect(transport); } 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")) { const wrappedError = new Error( `FastMCP session ID error: ${errMsg}` ); wrappedError.cause = connectErr; throw wrappedError; } } throw connectErr; } this.connected = true; this.transportType = "streamable-http"; logger.debug( `Successfully connected to MCP implementation via streamable HTTP: ${baseUrl}` ); } catch (err) { await this.cleanupResources(); throw err; } } async connectWithSse(baseUrl) { try { this.connectionManager = new SseConnectionManager(baseUrl, { requestInit: { headers: this.headers } }); const transport = await this.connectionManager.start(); this.client = new import_client.Client(this.clientInfo, this.opts.clientOptions); await this.client.connect(transport); this.connected = true; this.transportType = "sse"; logger.debug( `Successfully connected to MCP implementation via HTTP/SSE: ${baseUrl}` ); } 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; } }; // src/connectors/websocket.ts var import_uuid = require("uuid"); init_logging(); // src/task_managers/websocket.ts var import_ws = __toESM(require("ws"), 1); init_logging(); var WebSocketConnectionManager = class extends ConnectionManager { static { __name(this, "WebSocketConnectionManager"); } url; headers; _ws = null; /** * @param url The WebSocket URL to connect to. * @param headers Optional headers to include in the connection handshake. */ constructor(url, headers = {}) { super(); this.url = url; this.headers = headers; } /** Establish a WebSocket connection and wait until it is open. */ async establishConnection() { logger.debug(`Connecting to WebSocket: ${this.url}`); return new Promise((resolve, reject) => { const ws = new import_ws.default(this.url, { headers: this.headers }); this._ws = ws; const onOpen = /* @__PURE__ */ __name(() => { cleanup(); logger.debug("WebSocket connected successfully"); resolve(ws); }, "onOpen"); const onError = /* @__PURE__ */ __name((err) => { cleanup(); logger.error(`Failed to connect to WebSocket: ${err}`); reject(err); }, "onError"); const cleanup = /* @__PURE__ */ __name(() => { ws.off("open", onOpen); ws.off("error", onError); }, "cleanup"); ws.on("open", onOpen); ws.on("error", onError); }); } /** Cleanly close the WebSocket connection. */ async closeConnection(connection) { logger.debug("Closing WebSocket connection"); return new Promise((resolve) => { const onClose = /* @__PURE__ */ __name(() => { connection.off("close", onClose); this._ws = null; resolve(); }, "onClose"); if (connection.readyState === import_ws.default.CLOSED) { onClose(); return; } connection.on("close", onClose); try { connection.close(); } catch (e) { logger.warn(`Error closing WebSocket connection: ${e}`); onClose(); } }); } }; // src/connectors/websocket.ts var WebSocketConnector = class extends BaseConnector { static { __name(this, "WebSocketConnector"); } url; headers; connectionManager = null; ws = null; receiverTask = null; pending = /* @__PURE__ */ new Map(); toolsCache = null; constructor(url, opts = {}) { super(); this.url = url; this.headers = { ...opts.headers ?? {} }; if (opts.authToken) this.headers.Authorization = `Bearer ${opts.authToken}`; } async connect() { if (this.connected) { logger.debug("Already connected to MCP implementation"); return; } logger.debug(`Connecting via WebSocket: ${this.url}`); try { this.connectionManager = new WebSocketConnectionManager( this.url, this.headers ); this.ws = await this.connectionManager.start(); this.receiverTask = this.receiveLoop(); this.connected = true; logger.debug("WebSocket connected successfully"); } catch (e) { logger.error(`Failed to connect: ${e}`); await this.cleanupResources(); throw e; } } async disconnect() { if (!this.connected) { logger.debug("Not connected to MCP implementation"); return; } logger.debug("Disconnecting \u2026"); await this.cleanupResources(); this.connected = false; } sendRequest(method, params = null) { if (!this.ws) throw new Error("WebSocket is not connected"); const id = (0, import_uuid.v4)(); const payload = JSON.stringify({ id, method, params: params ?? {} }); return new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); this.ws.send(payload, (err) => { if (err) { this.pending.delete(id); reject(err); } }); }); } async receiveLoop() { if (!this.ws) return; const socket = this.ws; const onMessage = /* @__PURE__ */ __name((msg) => { let data; try { data = JSON.parse(msg.data ?? msg); } catch (e) { logger.warn("Received non\u2011JSON frame", e); return; } const id = data.id; if (id && this.pending.has(id)) { const { resolve, reject } = this.pending.get(id); this.pending.delete(id); if ("result" in data) resolve(data.result); else if ("error" in data) reject(data.error); } else { logger.debug("Received unsolicited message", data); } }, "onMessage"); if (socket.addEventListener) { socket.addEventListener("message", onMessage); } else { socket.on("message", onMessage); } return new Promise((resolve) => { const onClose = /* @__PURE__ */ __name(() => { if (socket.removeEventListener) { socket.removeEventListener("message", onMessage); } else { socket.off("message", onMessage); } this.rejectAll(new Error("WebSocket closed")); resolve(); }, "onClose"); if (socket.addEventListener) { socket.addEventListener("close", onClose); } else { socket.on("close", onClose); } }); } rejectAll(err) { for (const { reject } of this.pending.values()) reject(err); this.pending.clear(); } async initialize() { logger.debug("Initializing MCP session over WebSocket"); const result = await this.sendRequest("initialize"); const toolsList = await this.listTools(); this.toolsCache = toolsList.map((t) => t); logger.debug(`Initialized with ${this.toolsCache.length} tools`); return result; } async listTools() { const res = await this.sendRequest("tools/list"); return res.tools ?? []; } async callTool(name, args) { return await this.sendRequest("tools/call", { name, arguments: args }); } async listResources() { const resources = await this.sendRequest("resources/list"); return { resources: Array.isArray(resources) ? resources : [] }; } async readResource(uri) { const res = await this.sendRequest("resources/read", { uri }); return res; } async request(method, params = null) { return await this.sendRequest(method, params); } get tools() { if (!this.toolsCache) throw new Error("MCP client is not initialized"); return this.toolsCache; } async cleanupResources() { if (this.receiverTask) await this.receiverTask.catch(() => { }); this.receiverTask = null; this.rejectAll(new Error("WebSocket disconnected")); if (this.connectionManager) { await this.connectionManager.stop(); this.connectionManager = null; this.ws = null; } this.toolsCache = null; } get publicIdentifier() { return { type: "websocket", url: this.url }; } }; // src/client/base.ts init_logging(); // 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; } }; // src/client/base.ts var BaseMCPClient = class { static { __name(this, "BaseMCPClient"); } config = {}; sessions = {}; activeSessions = []; constructor(config2) { if (config2) { this.config = config2; } } 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; } removeServer(name) { if (this.config.mcpServers?.[name]) { delete this.config.mcpServers[name]; this.activeSessions = this.activeSessions.filter((n) => n !== 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; } getAllA