Files
aptos-hackathon/src/services/mcp-tools.service.ts
T

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