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
JavaScript
"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