import { DynamicStructuredTool } from '@langchain/core/tools'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { Tool as McpTool, TextContent } from '@modelcontextprotocol/sdk/types'; import { z } from 'zod'; export class McpToolsService { private mcpClient: Client | null = null; private transport: SSEClientTransport | null = null; private readonly serverUrl: string; constructor(serverUrl: string) { this.serverUrl = serverUrl; } async connect(): Promise { if (this.mcpClient && this.transport) { return; } try { this.transport = new SSEClientTransport(new URL(this.serverUrl)); this.mcpClient = new Client({ name: 'my-langchain-mcp-client', version: '1.0.0', }); await this.mcpClient.connect(this.transport); console.log(`[MCP Service] Connected to MCP server: ${this.serverUrl}`); } catch (error) { console.error( `[MCP Service] Error connecting to MCP server ${this.serverUrl}:`, error, ); throw error; } } async disconnect(): Promise { if (this.mcpClient) { await this.mcpClient.close(); this.mcpClient = null; this.transport = null; console.log( `[MCP Service] Disconnected from MCP server: ${this.serverUrl}`, ); } } async getLangchainTools(): Promise { if (!this.mcpClient) { throw new Error('MCP client is not connected. Call .connect() first'); } const mcpTools: McpTool[] = (await this.mcpClient.listTools()).tools; const langchainTools: DynamicStructuredTool[] = []; for (const mcpTool of mcpTools) { const properties: Record = {}; for (const key in mcpTool.inputSchema.properties) { // biome-ignore lint/suspicious/noExplicitAny: const prop = mcpTool.inputSchema.properties[key] as any; let schemaType: z.ZodTypeAny; switch (prop.type) { case 'string': schemaType = z.string(); break; case 'number': schemaType = z.number(); break; case 'boolean': schemaType = z.boolean(); break; case 'array': schemaType = z.array(z.any()); break; case 'object': schemaType = z.object({}); break; default: schemaType = z.any(); } if (!(mcpTool.inputSchema.required || []).includes(key)) { schemaType = schemaType.optional(); } properties[key] = schemaType.describe(prop.description || ''); } const zodSchema = z.object(properties); const langchainTool = new DynamicStructuredTool({ name: mcpTool.name, description: mcpTool.description || '', schema: zodSchema, func: async (args: Record) => { console.log( `[MCP Tool Call] Calling MCP tool: ${mcpTool.name} with arguments:`, args, ); const result = await this.mcpClient!.callTool({ name: mcpTool.name, arguments: args, }); if (result.isError) { // biome-ignore lint/suspicious/noExplicitAny: const errorContent = (result.content as any[]) .map((c) => (c as TextContent).text || '') .join('\n'); throw new Error( `Error executing tool ${mcpTool.name}: ${errorContent}`, ); } if (result.structuredContent) { return result.structuredContent; } // biome-ignore lint/suspicious/noExplicitAny: return (result.content as any[]) .map((c) => { if (c.type === 'text') return (c as TextContent).text; if (c.type === 'image') return `[Image: ${c.mimeType}]`; if (c.type === 'audio') return `[Audio: ${c.mimeType}]`; if (c.type === 'resource') return `[Resource: ${c.uri}]`; return JSON.stringify(c); }) .join('\n'); }, }); langchainTools.push(langchainTool); } return langchainTools; } }