From c348b4902e09bd80eb6025dce739fee4c1b85fe8 Mon Sep 17 00:00:00 2001 From: uxname Date: Sat, 24 May 2025 15:52:17 +0300 Subject: [PATCH] feat: Integrate TypeScript and MCP tools for LangChain pipeline. --- bun.lock | 3 + package.json | 9 ++- src/index.ts | 199 +++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 177 insertions(+), 34 deletions(-) diff --git a/bun.lock b/bun.lock index 0af547b..4e3be2e 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "lefthook": "^1.11.13", "npm-check-updates": "^18.0.1", "npm-run-all": "^4.1.5", + "typescript": "^5.8.3", }, }, }, @@ -568,6 +569,8 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], diff --git a/package.json b/package.json index 7e2658b..e906894 100644 --- a/package.json +++ b/package.json @@ -7,19 +7,22 @@ "@typescript/native-preview": "^7.0.0-dev.20250523.1", "lefthook": "^1.11.13", "npm-check-updates": "^18.0.1", - "npm-run-all": "^4.1.5" + "npm-run-all": "^4.1.5", + "typescript": "^5.8.3" }, "private": true, "scripts": { "________________ BUILD AND RUN ________________": "", - "build": "npx tsgo", + "build": "npx tsc", + "build:go": "npx tsgo", "prebuild": "npx rimraf dist", "start": "node dist/index.js", "________________ FORMAT AND LINT ________________": "", "check": "npx run-p ts:check lint", "lint": "biome check", "lint:fix": "biome check --write", - "ts:check": "tsgo --noEmit", + "ts:check": "tsc --noEmit", + "ts:check:go": "tsgo --noEmit", "________________ OTHER ________________": "", "postinstall": "npm run prepare", "prepare": "npx lefthook install", diff --git a/src/index.ts b/src/index.ts index 6ba3be4..9ceaba1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,9 +7,17 @@ import { import { Runnable } from '@langchain/core/runnables'; import { DynamicStructuredTool } from '@langchain/core/tools'; import { ChatOpenAI } from '@langchain/openai'; +import dotenv from 'dotenv'; // Импортируем dotenv для загрузки переменных окружения import Handlebars from 'handlebars'; import { z } from 'zod'; +// MCP SDK imports +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'; + +dotenv.config(); // Загружаем переменные окружения из .env файла + interface PipelineStep { name: string; prompt: string; @@ -21,41 +29,160 @@ interface Pipeline { steps: PipelineStep[]; } -class MockMcpToolsService { - async getTools(): Promise { - const echoTool = new DynamicStructuredTool({ - name: 'echo', - description: 'Возвращает введенную строку.', - schema: z.object({ input: z.string().describe('Строка для эхо.') }), - func: async (input: { input: string }) => { - return input.input; - }, - }); +/** + * Сервис для взаимодействия с MCP сервером и предоставления инструментов LangChain. + */ +class McpToolsService { + private mcpClient: Client | null = null; + private transport: SSEClientTransport | null = null; + private readonly serverUrl: string; - const getCurrentDateTool = new DynamicStructuredTool({ - name: 'get_current_date', - description: 'Возвращает текущую дату в формате ISO.', - schema: z.object({}), - func: async () => { - return new Date().toISOString(); - }, - }); + constructor(serverUrl: string) { + this.serverUrl = serverUrl; + } - return [echoTool, getCurrentDateTool]; + /** + * Устанавливает соединение с MCP сервером. + */ + 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] Подключено к MCP серверу: ${this.serverUrl}`); + } catch (error) { + console.error( + `[MCP Service] Ошибка подключения к MCP серверу ${this.serverUrl}:`, + error, + ); + throw error; + } + } + + /** + * Закрывает соединение с MCP сервером. + */ + async disconnect(): Promise { + if (this.mcpClient) { + await this.mcpClient.close(); + this.mcpClient = null; + this.transport = null; + console.log(`[MCP Service] Отключено от MCP сервера: ${this.serverUrl}`); + } + } + + /** + * Получает список инструментов с MCP сервера и преобразует их в формат LangChain. + * @returns Массив инструментов LangChain. + */ + async getLangchainTools(): Promise { + if (!this.mcpClient) { + throw new Error('MCP клиент не подключен. Сначала вызовите .connect()'); + } + + const mcpTools: McpTool[] = (await this.mcpClient.listTools()).tools; + const langchainTools: DynamicStructuredTool[] = []; + + for (const mcpTool of mcpTools) { + // Простая конвертация JSON Schema в Zod schema. + // Для сложных схем рекомендуется использовать специализированную библиотеку (например, json-schema-to-zod). + 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': + // Для массивов без явного типа элементов, используем z.array(z.any()) + schemaType = z.array(z.any()); + break; + case 'object': + // Для объектов без явной вложенной схемы, используем z.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] Вызов инструмента MCP: ${mcpTool.name} с аргументами:`, + 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( + `Ошибка выполнения инструмента ${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 `[Изображение: ${c.mimeType}]`; + if (c.type === 'audio') return `[Аудио: ${c.mimeType}]`; + if (c.type === 'resource') return `[Ресурс: ${c.uri}]`; + return JSON.stringify(c); // Запасной вариант для других типов + }) + .join('\n'); + }, + }); + langchainTools.push(langchainTool); + } + return langchainTools; } } class PipelineExecutor { - private readonly mcpToolsService: MockMcpToolsService; + readonly mcpToolsService: McpToolsService; private cachedTools: DynamicStructuredTool[] | null = null; - constructor() { - this.mcpToolsService = new MockMcpToolsService(); + constructor(mcpServerUrl: string) { + this.mcpToolsService = new McpToolsService(mcpServerUrl); } private async _getOrLoadTools(): Promise { if (!this.cachedTools) { - this.cachedTools = await this.mcpToolsService.getTools(); + await this.mcpToolsService.connect(); // Устанавливаем соединение с MCP сервером + this.cachedTools = await this.mcpToolsService.getLangchainTools(); // Получаем инструменты LangChain } return this.cachedTools; } @@ -133,11 +260,13 @@ class PipelineExecutor { for (const toolCall of response.tool_calls) { try { - const tools = await this._getOrLoadTools(); + const tools = await this._getOrLoadTools(); // Получаем LangChain-совместимые инструменты const tool = tools.find((t) => t.name === toolCall.name); if (!tool) { - console.error(`Инструмент ${toolCall.name} не найден.`); + console.error( + `Инструмент ${toolCall.name} не найден в списке LangChain инструментов.`, + ); messages.push( new ToolMessage({ tool_call_id: toolCall.id!, @@ -147,11 +276,12 @@ class PipelineExecutor { continue; } + // Выполняем функцию LangChain инструмента, которая внутри вызывает MCP сервер const toolResult = await tool.func(toolCall.args); messages.push( new ToolMessage({ tool_call_id: toolCall.id!, - content: JSON.stringify(toolResult), + content: JSON.stringify(toolResult), // LangChain ожидает строковый контент здесь }), ); } catch (error) { @@ -174,8 +304,10 @@ class PipelineExecutor { } async function main() { - require('dotenv').config(); const openAIApiKey = process.env.OPENAI_API_KEY; + // Используйте переменную окружения для URL сервера MCP, с запасным вариантом по умолчанию + const mcpServerUrl = + process.env.MCP_SERVER_URL || 'https://santiment-mcp.dev.mind-dev.com/sse'; if (!openAIApiKey) { console.error( @@ -184,7 +316,7 @@ async function main() { process.exit(1); } - const pipelineExecutor = new PipelineExecutor(); + const pipelineExecutor = new PipelineExecutor(mcpServerUrl); // Передаем URL сервера const testPipeline: Pipeline = { description: 'Тестовый пайплайн', @@ -193,7 +325,7 @@ async function main() { steps: [ { name: 'Приветствие', - prompt: 'Скажи привет, используя имя {{username}}.', + prompt: 'Скажи привет пользователю, его имя - {{username}}.', }, { name: 'Использование инструмента', @@ -204,12 +336,15 @@ async function main() { name: 'Получение даты', prompt: 'Какая сейчас дата? Используй инструмент `get_current_date`.', }, - { name: 'Завершение', prompt: 'Напиши резюме нашего с тобой диалога' }, + { + name: 'Завершение', + prompt: 'Напомни как зовут пользователя? (из прошлых сообщений)', + }, ], }; const variables = { - username: 'Пользователь', + username: 'Вася', }; const systemPrompts = ['Отвечай кратко и по существу.']; @@ -224,6 +359,8 @@ async function main() { console.log('Результаты выполнения пайплайна:', results); } catch (error) { console.error('Ошибка при выполнении пайплайна:', error); + } finally { + await pipelineExecutor.mcpToolsService.disconnect(); } }