UNPKG

youling-agent-mcp

Version:

MCP服务器实现POMDP研发任务拆解

380 lines 15.5 kB
import express from 'express'; import path from 'path'; import fs from 'fs'; import { randomUUID } from 'crypto'; import { pomdpTaskDecomposer } from './pomdp-decomposer.js'; import { z } from 'zod'; import dotenv from 'dotenv'; // 采用标准ESM导入语法 // 导入MCP SDK import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; // 导入自定义logger,在stdio模式下禁用普通日志输出 import { logger } from '../bin/logger.js'; // 加载环境变量 dotenv.config(); // 配置日志文件 const logDir = path.resolve(process.cwd(), '..'); const logFile = path.join(logDir, 'mcp-server.log'); // 创建自定义日志函数 const logToFileAndConsole = (message) => { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}\n`; // 输出到控制台,但在stdio模式下不输出 logger.log(message); // 输出到文件(始终执行,与运行模式无关) fs.appendFileSync(logFile, logMessage); }; // 清空之前的日志文件 fs.writeFileSync(logFile, `[${new Date().toISOString()}] === MCP服务器日志开始 ===\n`); logToFileAndConsole('日志文件已创建: ' + logFile); // 创建Express应用 const app = express(); app.use(express.json()); // 添加JSON解析中间件 app.use(express.urlencoded({ extended: true })); // 创建MCP服务器实例 const mcpServer = new McpServer({ name: "pomdp-task-decomposer", version: "1.0.0", // 设置请求超时时间为10分钟 defaultTimeout: 10 * 60 * 1000, // 10分钟 (600000毫秒) // 身份验证函数,可以使用环境变量中的密钥 authenticate: async (auth) => { const secretKey = process.env.MCP_SECRET_KEY || 'default-secret-key'; logToFileAndConsole(`收到身份验证请求: ${JSON.stringify(auth)}`); if (auth?.bearer === secretKey) { logToFileAndConsole('身份验证成功: authenticated-user'); return { userId: 'authenticated-user' }; } logToFileAndConsole('使用匿名身份: anonymous-user'); return { userId: 'anonymous-user' }; } }); // 注册POMDP任务分解资源 - 使用字符串URI模板 mcpServer.registerResource("task-decompose", "task:///decompose/{task}", // URI模板格式 // 资源元数据 { title: "POMDP任务分解器", description: "使用POMDP方法分解研发任务描述" }, // 资源处理函数 async (uri, extra) => { try { // 从查询参数或URI路径中获取任务描述 const taskDescription = extra.question || uri.pathname.slice(1) || ""; if (!taskDescription) { throw new Error("请提供任务描述"); } logToFileAndConsole(`处理任务分解请求: ${taskDescription.substring(0, 100)}...`); // 使用POMDP分解任务 const decompositionResult = await pomdpTaskDecomposer(taskDescription); // 返回结果 return { contents: [ { uri: uri.href, text: decompositionResult, contentType: 'text/markdown' } ] }; } catch (error) { logToFileAndConsole(`处理资源请求时出错: ${error instanceof Error ? error.message : String(error)}`); throw error; } }); // 注册POMDP任务分解工具 // 使用与Sdk示例相同的格式注册工具 mcpServer.tool('decompose-task', '使用POMDP方法将研发任务分解为详细计划', { taskDescription: z.string().describe('需要分解的研发任务描述'), }, // 工具处理函数 async (params) => { const { taskDescription } = params; try { // 已从参数中获取任务描述 logToFileAndConsole(`处理工具调用: decompose-task, 描述: ${taskDescription.substring(0, 100)}...`); // 使用POMDP分解任务 const result = await pomdpTaskDecomposer(taskDescription); return { content: [ { type: "text", text: result, contentType: "text/markdown" } ] }; } catch (error) { console.error('处理工具请求时出错:', error); return { content: [ { type: "text", text: `错误: ${error instanceof Error ? error.message : String(error)}` } ] }; } }); // 注册对话消息处理工具 // 参考 chat_client.py 实现 mcpServer.tool('chat-message', '使用用户消息进行对话交互', { message: z.string().describe('用户消息内容'), session_id: z.string().describe('会话标识符'), task_description: z.string().optional().describe('(可选) 任务描述概要,用于提供上下文'), history: z.array(z.tuple([ z.string().describe('用户消息'), z.string().describe('助手消息') ])).optional().describe('(可选) 历史聊天记录,格式为[(user_msg, assistant_msg), ...]'), }, // 工具处理函数 async (params) => { const { message, session_id, task_description, history } = params; try { // 打印调用信息 logToFileAndConsole(`处理对话消息: ${message.substring(0, 100)}... 会话 ID: ${session_id}`); // 准备请求数据 const requestData = { params: { message, session_id } }; // 添加可选参数 if (task_description !== undefined) { requestData.params.task_description = task_description; } if (history !== undefined) { requestData.params.history = history; } // 创建对话处理URL // 默认为本地服务,可通过环境变量配置 const chatServiceUrl = process.env.CHAT_SERVICE_URL || 'http://localhost:8080'; const url = `${chatServiceUrl}/chat_service/process_message`; // 发送HTTP请求 const axios = await import('axios'); const startTime = Date.now(); const response = await axios.default.post(url, requestData); const endTime = Date.now(); // 检查响应状态 if (response.data.status !== 'success') { const errorMessage = response.data.error || '未知错误'; throw new Error(`调用聊天服务出错: ${errorMessage}`); } // 返回数据 const responseData = response.data.data || {}; logToFileAndConsole(`处理完成,用时:${endTime - startTime}ms`); // 处理返回文本,包含response和tree_content let responseText = responseData.response || '无响应内容'; if (responseData.tree_content) { responseText += '\n\n```tree_content\n' + responseData.tree_content + '\n```'; } return { content: [ { type: "text", text: responseText, contentType: "text/markdown" } ], // 返回完整的响应数据作为额外内容 extra: { history: responseData.history || [], process_time: responseData.process_time || (endTime - startTime), focus_key: responseData.focus_key, tree_content: responseData.tree_content } }; } catch (error) { logToFileAndConsole(`处理聊天消息时出错: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `错误: ${error instanceof Error ? error.message : String(error)}` } ] }; } }); // 存储活跃的会话信息 const activeSessions = new Map(); // 使用构造函数创建传输层实例 const transport = new StreamableHTTPServerTransport({ // 生成唯一会话ID sessionIdGenerator: () => { const sessionId = randomUUID(); logToFileAndConsole(`生成新会话ID: ${sessionId}`); return sessionId; }, enableJsonResponse: true, // 使用JSON响应而不是SSE流 // 会话初始化回调 onsessioninitialized: (sessionId) => { logToFileAndConsole(`新会话已初始化: ${sessionId}`); // 记录新会话 activeSessions.set(sessionId, { id: sessionId, createdAt: new Date(), lastAccessedAt: new Date(), requestCount: 0 }); logToFileAndConsole(`当前活跃会话数: ${activeSessions.size}`); } // 移除不支持的perstrategy参数 }); // 添加健康检查端点 app.get('/health', (req, res) => { res.status(200).send('服务运行正常'); }); // 添加调试端点来查看当前活跃会话 app.get('/debug/sessions', (req, res) => { const sessions = Array.from(activeSessions.entries()).map(([id, session]) => ({ id, createdAt: session.createdAt, lastAccessedAt: session.lastAccessedAt, requestCount: session.requestCount })); res.json({ totalSessions: activeSessions.size, sessions }); logToFileAndConsole(`已请求会话调试信息,当前有 ${activeSessions.size} 个活跃会话`); }); // 定义MCP请求处理函数 const mcpHandler = (req, res) => { const sessionId = req.headers['mcp-session-id']; const requestBody = req.body; const method = requestBody?.method || '未知方法'; const id = requestBody?.id || '无ID'; // 记录详细请求信息 logToFileAndConsole(`收到MCP请求: ${req.method} ${req.path} Method=${method} ID=${id}`); logToFileAndConsole(`请求头: ${JSON.stringify(req.headers)}`); logToFileAndConsole(`请求体: ${JSON.stringify(requestBody, null, 2)}`); // 跟踪会话状态 if (sessionId && activeSessions.has(sessionId)) { const session = activeSessions.get(sessionId); if (session) { session.lastAccessedAt = new Date(); session.requestCount += 1; logToFileAndConsole(`更新了会话 ${sessionId} 信息,请求次数: ${session.requestCount}`); } } else if (method === 'mcp/initialize') { logToFileAndConsole(`收到初始化请求,将创建新会话`); } else if (!sessionId) { logToFileAndConsole(`警告: 收到没有会话ID的非初始化请求`); } else { logToFileAndConsole(`错误: 收到未知会话ID: ${sessionId}`); } // 添加响应结束监听器来记录响应情况 const oldSend = res.send; res.send = function (body) { // 记录返回数据 if (body) { try { const responseStr = typeof body === 'object' ? JSON.stringify(body) : String(body); logToFileAndConsole(`返回数据: ${responseStr}`); // 特别检测和记录初始化错误 if (responseStr.includes('Server already initialized')) { logToFileAndConsole(`检测到 'Server already initialized' 错误,请求ID: ${id}`); logToFileAndConsole(`错误上下文 - 请求方法: ${method}, 会话ID: ${sessionId || '无'}`); logToFileAndConsole(`错误请求头: ${JSON.stringify(req.headers, null, 2)}`); } } catch (e) { logToFileAndConsole(`无法解析响应数据: ${e}`); } } logToFileAndConsole(`请求完成: ${req.method} ${req.path}`); return oldSend.apply(res, [body]); }; // 捕获错误 const oldStatus = res.status; res.status = function (code) { logToFileAndConsole(`响应状态码: ${code}`); return oldStatus.call(res, code); }; // 将请求传递给transport处理 transport.handleRequest(req, res, requestBody); }; // 根据运行模式选择不同的传输层 const transportMode = process.env.MCP_TRANSPORT_MODE || 'http'; logToFileAndConsole(`启动MCP服务器,传输模式: ${transportMode}`); if (transportMode === 'stdio') { // Windows环境检测 const isWindows = process.platform === 'win32'; logToFileAndConsole(`检测到运行环境: ${isWindows ? 'Windows' : 'Unix-like'} (${process.platform})`); try { // 使用官方 StdioServerTransport 创建 stdio 传输层实例 // 注意:官方实现不接受配置选项,仅接受自定义stdin/stdout const stdioTransport = new StdioServerTransport(); // 重要:确保在Windows环境下使用正确的编码 if (isWindows) { // 在Windows下设置 stdin/stdout 的编码为 utf8 if (process.stdin.setEncoding) { process.stdin.setEncoding('utf8'); } if (process.stdout.setDefaultEncoding) { process.stdout.setDefaultEncoding('utf8'); } } // 添加更详细的错误处理逻辑 process.on('uncaughtException', (error) => { logToFileAndConsole(`检测到未捕获的异常: ${error.message}`); logToFileAndConsole(`错误堆栈: ${error.stack}`); // 不要立即退出,让错误信息有机会被记录 setTimeout(() => process.exit(1), 500); }); // 处理意外中断信号 process.on('SIGINT', () => { logToFileAndConsole('接收到中断信号,正在关闭MCP服务器...'); process.exit(0); }); // 连接MCP服务器到stdio传输层 // 这会自动处理 init 和 tool list 等标准 MCP 请求 logToFileAndConsole('正在连接到stdio传输层...'); mcpServer.connect(stdioTransport).then(() => { logToFileAndConsole('MCP服务器已成功连接到stdio传输层,使用官方 StdioServerTransport'); logToFileAndConsole('stdio 模式已准备好处理 init 和 tool list 等 MCP 请求'); }).catch((err) => { logToFileAndConsole(`连接MCP服务器到stdio传输层失败: ${err.message}`); if (err.stack) { logToFileAndConsole(`错误堆栈: ${err.stack}`); } process.exit(1); }); } catch (error) { logToFileAndConsole(`创建stdio传输层失败: ${error instanceof Error ? error.message : String(error)}`); if (error instanceof Error && error.stack) { logToFileAndConsole(`错误堆栈: ${error.stack}`); } process.exit(1); } } else { // HTTP模式 - 使用原有的HTTP传输层 mcpServer.connect(transport).then(() => { // 注册MCP路由处理器 app.post('/mcp', mcpHandler); // 处理POST请求 app.get('/mcp', mcpHandler); // 处理GET请求的SSE流 // 启动HTTP服务器 const PORT = process.env.PORT || 3000; app.listen(PORT, () => { logToFileAndConsole(`MCP服务器运行在端口 ${PORT}`); logToFileAndConsole(`MCP端点: http://localhost:${PORT}/mcp`); }); }).catch((err) => { logToFileAndConsole(`启动MCP服务器失败: ${err.message}`); process.exit(1); }); } //# sourceMappingURL=mcp-server.js.map