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
JavaScript
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
};