132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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<DynamicStructuredTool[]> {
|
|
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<string, z.ZodTypeAny> = {};
|
|
for (const key in mcpTool.inputSchema.properties) {
|
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
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<string, unknown>) => {
|
|
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: <explanation>
|
|
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: <explanation>
|
|
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;
|
|
}
|
|
}
|