UNPKG

@geekmai/anteey-mcp-client

Version:

Anteey MCP 客户端 - 连接外部 AI 工具与 Anteey 笔记应用

658 lines (578 loc) 16.9 kB
#!/usr/bin/env node /** * @file cursor.js * @description Anteey MCP Cursor 适配器 */ const http = require("http"); const readline = require("readline"); const chalk = require("chalk"); const path = require("path"); const { loadConfig } = require("./config"); // 读取package.json中的版本号 const packageJson = require(path.join(__dirname, "..", "package.json")); // 创建 readline 接口用于读取标准输入 const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false, }); console.error(chalk.cyan("🚀 Anteey MCP Cursor 适配器已启动")); // 记录活动请求 const activeRequests = new Map(); // 初始化配置 let config = null; // 辅助函数:安全地处理可能包含特殊字符的文本 function sanitizeText(text) { if (!text) return ""; // 移除控制字符,但保留正常的换行和制表符 return text.replace( /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, "" ); } // 加载配置 loadConfig() .then((loadedConfig) => { config = loadedConfig; console.error(chalk.gray(`服务器: ${config.serverUrl}`)); console.error( chalk.gray( `API 密钥: ${ config.apiKey ? config.apiKey.substring(0, 10) + "..." : "(未设置)" }` ) ); if (!config.apiKey) { console.error( chalk.yellow('⚠️ 未设置 API 密钥,请使用 "anteey-mcp config" 设置') ); } }) .catch((error) => { console.error(chalk.red("加载配置失败:"), error.message); process.exit(1); }); // 监听标准输入的每一行 rl.on("line", async (line) => { try { console.error(chalk.gray(`[收到] ${line}`)); if (!line.trim()) { console.error(chalk.gray("收到空输入,忽略")); return; } // 解析 JSON 请求 const request = JSON.parse(line); // 确保这是一个 JSON-RPC 请求 if (!request.jsonrpc || request.jsonrpc !== "2.0") { console.error(chalk.yellow("非 JSON-RPC 2.0 请求,忽略")); return; } const { id, method, params } = request; // 处理取消请求 if (method === "$/cancelRequest") { const cancelId = params?.id; if (cancelId && activeRequests.has(cancelId)) { console.error(chalk.yellow(`取消请求: ${cancelId}`)); const controller = activeRequests.get(cancelId); controller.abort(); activeRequests.delete(cancelId); } // 发送成功响应 console.log( JSON.stringify({ jsonrpc: "2.0", id: id, result: null, }) ); return; } console.error(chalk.blue(`[处理] ${method} (id: ${id})`)); // 处理初始化请求 if (method === "initialize") { console.log( JSON.stringify({ jsonrpc: "2.0", id: id, result: { protocolVersion: "2024-11-05", serverInfo: { name: "anteey-mcp-cursor", version: packageJson.version, vendor: "Anteey Team", }, capabilities: { tools: {}, resources: {}, }, }, }) ); return; } // 处理已初始化通知 if (method === "notifications/initialized") { console.error(chalk.green("✅ 客户端已初始化")); return; } // 处理工具列表请求 if (method === "tools/list") { console.log( JSON.stringify({ jsonrpc: "2.0", id: id, result: { tools: [ { name: "search_notes", description: "在 Anteey 中搜索笔记", inputSchema: { type: "object", properties: { query: { type: "string", description: "搜索关键词", }, limit: { type: "integer", description: "返回结果数量限制", default: 10, }, }, required: ["query"], }, }, { name: "get_note", description: "获取特定笔记的详细内容", inputSchema: { type: "object", properties: { noteId: { type: "string", description: "笔记 ID", }, }, required: ["noteId"], }, }, { name: "get_recent_notes", description: "获取最近更新的笔记", inputSchema: { type: "object", properties: { limit: { type: "integer", description: "返回结果数量限制", default: 10, }, }, }, }, ], }, }) ); return; } // 处理工具调用请求 if (method === "tools/call") { if (!config) { console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32603, message: "配置未加载", }, }) ); return; } const toolName = params?.name; const args = params?.arguments || {}; if (toolName === "search_notes") { const query = args.query || ""; const limit = args.limit || 10; console.error(chalk.blue(`[搜索] 查询: "${query}", 限制: ${limit}`)); try { // 创建 AbortController 用于取消请求 const controller = new AbortController(); activeRequests.set(id, controller); const notes = await searchNotes(query, limit, controller.signal); // 请求完成,从活动请求中移除 activeRequests.delete(id); // 如果请求已被取消,不要发送响应 if (controller.signal.aborted) { console.error(chalk.yellow(`请求 ${id} 已被取消`)); return; } console.error(chalk.green(`[结果] 找到 ${notes.length} 条笔记`)); console.log( JSON.stringify({ jsonrpc: "2.0", id: id, result: { content: [ { type: "text", text: `在 Anteey 中找到 ${ notes.length } 条相关笔记:\n\n${notes .map( (note) => `**${sanitizeText(note.title) || "无标题笔记"}** (${ note.cardType || "未分类" })\n${sanitizeText(note.content)}\n---` ) .join("\n\n")}`, }, ], }, }) ); } catch (error) { activeRequests.delete(id); if (error.name === "AbortError") { console.error(chalk.yellow("请求被取消")); return; } console.error(chalk.red("[错误]"), error.message); console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32603, message: error.message || "搜索笔记时出错", }, }) ); } return; } if (toolName === "get_note") { const noteId = args.noteId; if (!noteId) { console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32602, message: "缺少必需参数: noteId", }, }) ); return; } try { const controller = new AbortController(); activeRequests.set(id, controller); const note = await getNoteById(noteId, controller.signal); activeRequests.delete(id); if (controller.signal.aborted) { console.error(chalk.yellow(`请求 ${id} 已被取消`)); return; } if (!note) { console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32603, message: "笔记不存在", }, }) ); return; } console.log( JSON.stringify({ jsonrpc: "2.0", id: id, result: { content: [ { type: "text", text: `**${sanitizeText(note.title) || "无标题笔记"}** (${ note.cardType || "未分类" })\n\n${sanitizeText(note.content)}`, }, ], }, }) ); } catch (error) { activeRequests.delete(id); if (error.name === "AbortError") { return; } console.error(chalk.red("[错误]"), error.message); console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32603, message: error.message || "获取笔记时出错", }, }) ); } return; } if (toolName === "get_recent_notes") { const limit = args.limit || 10; try { const controller = new AbortController(); activeRequests.set(id, controller); const notes = await getRecentNotes(limit, controller.signal); activeRequests.delete(id); if (controller.signal.aborted) { console.error(chalk.yellow(`请求 ${id} 已被取消`)); return; } console.log( JSON.stringify({ jsonrpc: "2.0", id: id, result: { content: [ { type: "text", text: `最近更新的 ${notes.length} 条笔记:\n\n${notes .map( (note) => `**${sanitizeText(note.title) || "无标题笔记"}** (${ note.cardType || "未分类" })\n更新时间: ${new Date( note.updatedAt ).toLocaleString()}\n${sanitizeText( note.content )}\n---` ) .join("\n\n")}`, }, ], }, }) ); } catch (error) { activeRequests.delete(id); if (error.name === "AbortError") { return; } console.error(chalk.red("[错误]"), error.message); console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32603, message: error.message || "获取最近笔记时出错", }, }) ); } return; } // 处理未知工具 console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32601, message: `未知工具: ${toolName}`, }, }) ); return; } // 处理未知方法 console.error(chalk.yellow(`[未知方法] ${method}`)); console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32601, message: `方法不存在: ${method}`, }, }) ); } catch (error) { console.error(chalk.red("[错误]"), error.message); // 尝试发送 JSON-RPC 错误响应 try { console.log( JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32603, message: error.message || "内部错误", }, }) ); } catch (e) { console.error(chalk.red("[严重错误]"), e.message); } } }); // 搜索笔记函数 function searchNotes(query, limit = 10, signal) { return new Promise((resolve, reject) => { try { const url = new URL(`${config.serverUrl}/notes/search`); url.searchParams.append("query", query); url.searchParams.append("limit", limit.toString()); const req = http.get( url, { headers: { Authorization: `Bearer ${config.apiKey}`, }, signal, }, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { if (res.statusCode !== 200) { reject(new Error(`API 请求失败: ${res.statusCode}`)); return; } try { const response = JSON.parse(data); if (!response.success) { reject(new Error(response.error || "未知错误")); return; } resolve(response.data || []); } catch (error) { reject(new Error("解析 API 响应失败")); } }); } ); req.on("error", (error) => { reject(error); }); req.end(); } catch (error) { reject(error); } }); } // 获取单个笔记函数 function getNoteById(noteId, signal) { return new Promise((resolve, reject) => { try { const url = `${config.serverUrl}/notes/${noteId}`; const req = http.get( url, { headers: { Authorization: `Bearer ${config.apiKey}`, }, signal, }, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { if (res.statusCode === 404) { resolve(null); return; } if (res.statusCode !== 200) { reject(new Error(`API 请求失败: ${res.statusCode}`)); return; } try { const response = JSON.parse(data); if (!response.success) { reject(new Error(response.error || "未知错误")); return; } resolve(response.data); } catch (error) { reject(new Error("解析 API 响应失败")); } }); } ); req.on("error", (error) => { reject(error); }); req.end(); } catch (error) { reject(error); } }); } // 获取最近笔记函数 function getRecentNotes(limit = 10, signal) { return new Promise((resolve, reject) => { try { const url = new URL(`${config.serverUrl}/notes/recent`); url.searchParams.append("limit", limit.toString()); const req = http.get( url, { headers: { Authorization: `Bearer ${config.apiKey}`, }, signal, }, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { if (res.statusCode !== 200) { reject(new Error(`API 请求失败: ${res.statusCode}`)); return; } try { const response = JSON.parse(data); if (!response.success) { reject(new Error(response.error || "未知错误")); return; } resolve(response.data || []); } catch (error) { reject(new Error("解析 API 响应失败")); } }); } ); req.on("error", (error) => { reject(error); }); req.end(); } catch (error) { reject(error); } }); } // 处理进程终止 process.on("SIGINT", () => { console.error(chalk.yellow("收到 SIGINT,正在退出...")); process.exit(0); }); process.on("SIGTERM", () => { console.error(chalk.yellow("收到 SIGTERM,正在退出...")); process.exit(0); }); console.error(chalk.gray("等待 JSON-RPC 请求..."));