@h1deya/langchain-mcp-tools
Version:
MCP To LangChain Tools Conversion Utility
207 lines (206 loc) • 9.62 kB
JavaScript
import { DynamicStructuredTool } from "@langchain/core/tools";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
import { CallToolResultSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
import { jsonSchemaToZod } from "@n8n/json-schema-to-zod";
import { Logger } from "./logger.js";
// Custom error type for MCP server initialization failures
class McpInitializationError extends Error {
serverName;
details;
constructor(serverName, message, details) {
super(message);
this.serverName = serverName;
this.details = details;
this.name = "McpInitializationError";
}
}
/**
* Initializes multiple MCP (Model Context Protocol) servers and converts them into LangChain tools.
* This function concurrently sets up all specified servers and aggregates their tools.
*
* @param configs - A mapping of server names to their respective configurations
* @param options - Optional configuration settings
* @param options.logLevel - Log verbosity level ("fatal" | "error" | "warn" | "info" | "debug" | "trace")
* @param options.logger - Custom logger implementation that follows the McpToolsLogger interface.
* If provided, overrides the default Logger instance.
*
* @returns A promise that resolves to:
* - tools: Array of StructuredTool instances ready for use with LangChain
* - cleanup: Function to properly terminate all server connections
*
* @throws McpInitializationError if any server fails to initialize
*
* @example
* const { tools, cleanup } = await convertMcpToLangchainTools({
* filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."] },
* fetch: { command: "uvx", args: ["mcp-server-fetch"] }
* });
*/
export async function convertMcpToLangchainTools(configs, options) {
const allTools = [];
const cleanupCallbacks = [];
const logger = options?.logger || new Logger({ level: options?.logLevel || "info" });
const serverInitPromises = Object.entries(configs).map(async ([name, config]) => {
const result = await convertSingleMcpToLangchainTools(name, config, logger);
return { name, result };
});
// Track server names alongside their promises
const serverNames = Object.keys(configs);
// Concurrently initialize all the MCP servers
const results = await Promise.allSettled(serverInitPromises);
// Process successful initializations and log failures
results.forEach((result, index) => {
if (result.status === "fulfilled") {
const { result: { tools, cleanup } } = result.value;
allTools.push(...tools);
cleanupCallbacks.push(cleanup);
}
else {
logger.error(`MCP server "${serverNames[index]}": failed to initialize: ${result.reason.details}`);
throw result.reason;
}
});
async function cleanup() {
// Concurrently execute all the callbacks
const results = await Promise.allSettled(cleanupCallbacks.map(callback => callback()));
// Log any cleanup failures
const failures = results.filter(result => result.status === "rejected");
failures.forEach((failure, index) => {
logger.error(`MCP server "${serverNames[index]}": failed to close: ${failure.reason}`);
});
}
logger.info(`MCP servers initialized: ${allTools.length} tool(s) available in total`);
allTools.forEach((tool) => logger.debug(`- ${tool.name}`));
return { tools: allTools, cleanup };
}
/**
* Initializes a single MCP server and converts its capabilities into LangChain tools.
* Sets up a connection to the server, retrieves available tools, and creates corresponding
* LangChain tool instances.
*
* @param serverName - Unique identifier for the server instance
* @param config - Server configuration including command, arguments, and environment variables
* @param logger - McpToolsLogger instance for recording operation details
*
* @returns A promise that resolves to:
* - tools: Array of StructuredTool instances from this server
* - cleanup: Function to properly terminate the server connection
*
* @throws McpInitializationError if server initialization fails
* (includes connection errors, tool listing failures)
*
* @internal This function is meant to be called by convertMcpToLangchainTools
*/
async function convertSingleMcpToLangchainTools(serverName, config, logger) {
let transport = null;
try {
let client = null;
logger.info(`MCP server "${serverName}": initializing with: ${JSON.stringify(config)}`);
let url = undefined;
try {
url = new URL(config.url);
}
catch {
// Ignore
}
if (url?.protocol === "http:" || url?.protocol === "https:") {
transport = new SSEClientTransport(url);
}
else if (url?.protocol === "ws:" || url?.protocol === "wss:") {
transport = new WebSocketClientTransport(url);
}
else {
// NOTE: Some servers (e.g. Brave) seem to require PATH to be set.
// To avoid confusion, it was decided to automatically append it to the env
// if not explicitly set by the config.
const stdioServerConfig = config;
const env = { ...stdioServerConfig.env };
if (!env.PATH) {
env.PATH = process.env.PATH || "";
}
transport = new StdioClientTransport({
command: stdioServerConfig.command,
args: stdioServerConfig.args,
env,
stderr: stdioServerConfig.stderr,
cwd: stdioServerConfig.cwd
});
}
client = new Client({
name: "mcp-client",
version: "0.0.1",
}, {
capabilities: {},
});
await client.connect(transport);
logger.info(`MCP server "${serverName}": connected`);
const toolsResponse = await client.request({ method: "tools/list" }, ListToolsResultSchema);
const tools = toolsResponse.tools.map((tool) => (new DynamicStructuredTool({
name: tool.name,
description: tool.description || "",
// FIXME
// eslint-disable-next-line @typescript-eslint/no-explicit-any
schema: jsonSchemaToZod(tool.inputSchema),
func: async function (input) {
logger.info(`MCP tool "${serverName}"/"${tool.name}" received input:`, input);
try {
// Execute tool call
const result = await client?.request({
method: "tools/call",
params: {
name: tool.name,
arguments: input,
},
}, CallToolResultSchema);
// Handles null/undefined cases gracefully
if (!result?.content) {
logger.info(`MCP tool "${serverName}"/"${tool.name}" received null/undefined result`);
return "";
}
const textContent = result.content
.filter(content => content.type === "text")
.map(content => content.text)
.join("\n\n");
// const textItems = result.content
// .filter(content => content.type === "text")
// .map(content => content.text)
// const textContent = JSON.stringify(textItems);
// Log rough result size for monitoring
const size = new TextEncoder().encode(textContent).length;
logger.info(`MCP tool "${serverName}"/"${tool.name}" received result (size: ${size})`);
// If no text content, return a clear message describing the situation
return textContent || "No text content available in response";
}
catch (error) {
logger.warn(`MCP tool "${serverName}"/"${tool.name}" caused error: ${error}`);
return `Error executing MCP tool: ${error}`;
}
},
})));
logger.info(`MCP server "${serverName}": ${tools.length} tool(s) available:`);
tools.forEach((tool) => logger.info(`- ${tool.name}`));
async function cleanup() {
if (transport) {
await transport.close();
logger.info(`MCP server "${serverName}": session closed`);
}
}
return { tools, cleanup };
}
catch (error) {
// Proper cleanup in case of initialization error
if (transport) {
try {
await transport.close();
}
catch (cleanupError) {
// Log cleanup error but don't let it override the original error
logger.error(`Failed to cleanup during initialization error: ${cleanupError}`);
}
}
throw new McpInitializationError(serverName, `Failed to initialize MCP server: ${error instanceof Error ? error.message : String(error)}`, error);
}
}