UNPKG

dynamic-interaction

Version:

Dynamic interaction 动态交互mcp,用于cursor、windsurf、trae 等 AI 智能编辑器 Agent 运行时交互使用

224 lines (213 loc) 10.2 kB
"use strict"; /** * MCP 服务器配置 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.mcpServer = void 0; exports.configureMcpServer = configureMcpServer; exports.startMcpServer = startMcpServer; const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const config_1 = require("../config"); const solicit_input_1 = require("./solicit-input"); const image_1 = require("../utils/image"); const session_1 = require("../types/session"); const logger_1 = require("../logger"); const zod_1 = require("zod"); const lifecycle_1 = require("../server/core/lifecycle"); const app_1 = require("../server/core/app"); const port_1 = require("../server/port"); const config_2 = require("../config"); // 创建 MCP 服务器实例 exports.mcpServer = new mcp_js_1.McpServer({ name: config_1.MCP_CONFIG.name, version: config_1.MCP_CONFIG.version, capabilities: { resources: {}, tools: {}, }, }); /** * 配置 MCP 服务器工具 */ function configureMcpServer() { // 配置 solicit-input 工具 exports.mcpServer.registerTool("solicit-input", { description: ` # 工具名称: solicit-input **功能描述:** 启动一个 Web UI 界面,用于收集用户的多模态反馈。当需要人类用户对 AI 的工作结果进行验证、提供修正意见或进行确认时,应调用此工具。 **参数 (Args):** * \`project_directory\` (str, 必填): 需要用户审核的项目目录的绝对路径。 * \`summary\` (str, 必填): 向用户展示的 AI 工作摘要。应清晰说明 AI 完成了什么,并引导用户反馈。内容为 Markdown 格式。 * \`timeout\` (int, 无效值): 等待用户反馈的超时时间(秒)。默认值: ${config_1.SESSION_TIMEOUT}。 **用户交互流程:** 1. UI 界面会显示 \`summary\` 中的工作摘要。 2. 用户可以通过执行命令、输入文本、上传图片等方式提供反馈。 **返回值 (Returns):** 一个包含用户所有反馈的列表 (List)。每个元素都是一个对象,如 \`{ type: "text", text: "..." }\` 或 \`{ type: "image", data: "..." }\`。超时或无反馈则返回空列表 \`{type: "text", text: "用户未提供反馈"}\`。 `, inputSchema: { summary: zod_1.z.string().describe("向用户展示的 AI 工作摘要。应清晰说明 AI 完成了什么,并引导用户反馈。内容为 Markdown 格式。"), project_directory: zod_1.z.string().describe("需要用户审核的项目目录的绝对路径。"), timeout: zod_1.z.number().optional().describe(`等待用户反馈的超时时间(秒)。默认值: ${config_1.SESSION_TIMEOUT}。`).default(config_1.SESSION_TIMEOUT) }, annotations: { displayName: "用户反馈收集器" } }, async ({ summary, project_directory, timeout }) => { logger_1.logger.info(`MCP: 请求用户输入。项目目录: ${project_directory}, 摘要: ${summary}`); try { // 检查HTTP服务器是否已启动,如果未启动则启动它 if (lifecycle_1.lifecycleManager.state === 'stopped') { logger_1.logger.info('HTTP服务器未启动,正在启动...'); try { await (0, port_1.freePortIfOccupied)(config_2.PORT); await (0, app_1.startServer)(); logger_1.logger.info(`HTTP服务器已懒启动,监听地址: http://localhost:${config_2.PORT}`); } catch (error) { logger_1.logger.error('启动HTTP服务器失败:', error); return { content: [ { type: "text", text: `错误: 启动HTTP服务器失败: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } // 调用实际的 solicitUserInput 函数,与前端 UI 建立会话并等待反馈 const feedback = await (0, solicit_input_1.solicitUserInput)(project_directory, summary, session_1.SessionMode.INTERACTIVE); // 将反馈转换为 MCP 期望的返回格式(List<any>) const content = []; if (feedback.text) { // 检测特殊的会话状态标记 if (feedback.text === '__SESSION_TIMEOUT__') { content.push({ type: "text", text: config_1.TIMEOUT_PROMPT }); logger_1.logger.warn("MCP: 会话超时标记被检测到,返回超时消息"); } else if (feedback.text === '__SESSION_CLOSED__') { // 用户主动关闭会话,同样返回 continue content.push({ type: "text", text: config_1.TIMEOUT_PROMPT }); logger_1.logger.warn("MCP: 会话关闭标记被检测到,返回 continue"); } else { content.push({ type: "text", text: `用户反馈: ${feedback.text}` }); } } if (feedback.imageData) { logger_1.logger.debug("MCP: 收到图片反馈:", feedback.imageData); const datas = Array.isArray(feedback.imageData) ? feedback.imageData : [feedback.imageData]; datas.forEach((img) => { try { const { data, mimeType } = (0, image_1.normalizeImageFeedback)(img); content.push({ type: "image", data, mimeType }); } catch (e) { logger_1.logger.error("图片解析失败", e); } }); } if (feedback.commandOutput) { content.push({ type: "command_output", text: `命令输出: ${feedback.commandOutput}` }); } // 如果没有内容(例如超时或空反馈),返回默认 continue if (content.length === 0) { content.push({ type: "text", text: "用户未提供反馈,继续执行" }); } logger_1.logger.info(`MCP: 准备返回给 Agent 的最终内容: ${JSON.stringify(content, null, 2)}`); return { content }; } catch (error) { // 发生错误时通知调用方 return { content: [ { type: "text", text: `错误: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); // 配置 notify-user 工具(通知模式) exports.mcpServer.registerTool("notify-user", { description: ` # 工具名称: notify-user **功能描述:** 发送通知消息给用户,不等待用户响应。适用于 AI 需要告知用户工作进展或状态更新,但不需要用户反馈的场景。 **参数 (Args):** * \`project_directory\` (str, 必填): 相关项目目录的绝对路径。 * \`summary\` (str, 必填): 通知内容。应清晰说明 AI 的工作状态或进展。内容为 Markdown 格式。 **用户交互流程:** 1. 通知消息会在用户界面中显示。 2. 系统不会等待用户响应,立即返回成功状态。 **返回值 (Returns):** 立即返回通知发送成功的确认信息。 `, inputSchema: { summary: zod_1.z.string().describe("通知内容。应清晰说明 AI 的工作状态或进展。内容为 Markdown 格式。"), project_directory: zod_1.z.string().describe("相关项目目录的绝对路径。") }, annotations: { displayName: "用户通知器" } }, async ({ summary, project_directory }) => { logger_1.logger.info(`MCP: 发送通知。项目目录: ${project_directory}, 摘要: ${summary}`); try { // 检查HTTP服务器是否已启动,如果未启动则启动它 if (lifecycle_1.lifecycleManager.state === 'stopped') { logger_1.logger.info('HTTP服务器未启动,正在启动...'); try { await (0, port_1.freePortIfOccupied)(config_2.PORT); await (0, app_1.startServer)(); logger_1.logger.info(`HTTP服务器已懒启动,监听地址: http://localhost:${config_2.PORT}`); } catch (error) { logger_1.logger.error('启动HTTP服务器失败:', error); return { content: [ { type: "text", text: `错误: 启动HTTP服务器失败: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } // 调用通知函数,立即返回 const feedback = await (0, solicit_input_1.notifyUser)(project_directory, summary); logger_1.logger.info(`MCP: 通知已发送: ${JSON.stringify(feedback)}`); return { content: [ { type: "text", text: `通知已成功发送: ${feedback.text || '通知已显示在用户界面'}`, }, ], }; } catch (error) { // 发生错误时通知调用方 return { content: [ { type: "text", text: `错误: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); } /** * 启动 MCP 服务器 */ async function startMcpServer() { const transport = new stdio_js_1.StdioServerTransport(); await exports.mcpServer.connect(transport); }