UNPKG

@geekmai/anteey-mcp-client

Version:

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

567 lines (503 loc) 15.4 kB
#!/usr/bin/env node const http = require("http"); const readline = require("readline"); const { URL } = require("url"); 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, }); // 记录活动请求 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, "" ); } // 辅助函数:解码 Unicode 编码的查询字符串 function decodeUnicodeQuery(query) { if (!query) return ""; // 检查是否是类似 "u56feu5e8au529fu80fd" 这样的编码格式 // 这种格式是多个 "u" + 4位十六进制数字组成的 if (/^u[0-9a-fA-F]+/.test(query)) { try { // 使用正则表达式匹配所有的 u + 4位十六进制数字 const matches = query.match(/u([0-9a-fA-F]{4})/g); if (matches) { let result = ""; for (const match of matches) { const hexCode = match.substring(1); // 移除 "u" const charCode = parseInt(hexCode, 16); result += String.fromCharCode(charCode); } console.error(`Unicode解码: ${query} -> ${result}`); return result; } } catch (error) { console.error(`Unicode解码失败: ${error.message}`); return query; // 解码失败时返回原字符串 } } return query; // 不是Unicode编码格式,直接返回 } console.error("Anteey MCP 客户端已启动 (Raycast 版)"); // 加载配置 loadConfig() .then((loadedConfig) => { config = loadedConfig; // 优先使用环境变量,如果没有则使用配置文件 const API_KEY = process.env.ANTEEY_MCP_API_KEY || config.apiKey; const ANTEEY_MCP_URL = process.env.ANTEEY_MCP_BASE_URL || config.serverUrl; config.apiKey = API_KEY; config.serverUrl = ANTEEY_MCP_URL; console.error(`使用 API 密钥: ${config.apiKey}`); console.error(`使用 API 地址: ${config.serverUrl}`); if (!config.apiKey) { console.error('⚠️ 未设置 API 密钥,请使用 "anteey-mcp config" 设置'); } else { // 配置加载完成后测试连接 setTimeout(() => testConnection(), 1000); } }) .catch((error) => { console.error("加载配置失败:", error.message); // 使用默认配置 config = { apiKey: process.env.ANTEEY_MCP_API_KEY || "", serverUrl: process.env.ANTEEY_MCP_BASE_URL || "http://localhost:43211/api/mcp", timeout: 30000, }; console.error(`使用默认配置 - API 密钥: ${config.apiKey}`); console.error(`使用默认配置 - API 地址: ${config.serverUrl}`); }); // 监听标准输入的每一行 rl.on("line", async (line) => { try { console.error(`收到请求: ${line}`); if (!line.trim()) { console.error("收到空输入,忽略"); return; } // 解析 JSON 请求 const request = JSON.parse(line); // 确保这是一个 JSON-RPC 请求 if (!request.jsonrpc || request.jsonrpc !== "2.0") { console.error("非 JSON-RPC 2.0 请求,忽略"); return; } const { id, method, params } = request; // 处理取消请求 if (method === "$/cancelRequest") { const cancelId = params?.id; if (cancelId && activeRequests.has(cancelId)) { console.error(`取消请求: ${cancelId}`); const controller = activeRequests.get(cancelId); controller.abort(); activeRequests.delete(cancelId); // 发送成功响应 console.log( JSON.stringify({ jsonrpc: "2.0", id: id, result: null, }) ); } else { // 即使找不到请求也返回成功响应 console.error(`未找到要取消的请求: ${cancelId}`); console.log( JSON.stringify({ jsonrpc: "2.0", id: id, result: null, }) ); } return; } console.error(`处理 JSON-RPC 请求: method=${method}, id=${id}`); // 处理初始化请求 if (method === "initialize") { console.log( JSON.stringify({ jsonrpc: "2.0", id: id, result: { protocolVersion: "2024-11-05", serverInfo: { name: "anteey-mcp-server", version: packageJson.version, vendor: "Anteey Team", }, capabilities: { tools: {}, prompts: {}, resources: {}, }, }, }) ); 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: "搜索查询", }, }, required: ["query"], }, }, ], }, }) ); return; } // 处理工具调用请求 if (method === "tools/call") { const toolName = params?.name; const args = params?.arguments || {}; if (toolName === "search_notes") { if (!config) { console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32603, message: "配置未加载", }, }) ); return; } const rawQuery = args.query || ""; const query = decodeUnicodeQuery(rawQuery); console.error(`原始查询: ${rawQuery}`); console.error(`解码后查询: ${query}`); try { // 创建 AbortController 用于取消请求 const controller = new AbortController(); activeRequests.set(id, controller); const notes = await searchNotes(query, controller.signal); console.error(`找到 ${notes.length} 条笔记`); // 请求完成,从活动请求中移除 activeRequests.delete(id); // 如果请求已被取消,不要发送响应 if (controller.signal.aborted) { console.error(`请求 ${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${sanitizeText(note.content)}` ) .join("\n\n")}`, }, ], }, }) ); } catch (error) { // 请求出错,从活动请求中移除 activeRequests.delete(id); // 如果是请求被取消,返回特定错误码 if (error.name === "AbortError") { console.error("请求被取消"); console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32800, message: "请求已取消", }, }) ); return; } console.error("搜索笔记时出错:", error); 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; } // 处理搜索请求(保留向后兼容性) if (method === "search") { if (!config) { console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32603, message: "配置未加载", }, }) ); return; } const rawQuery = params?.query || ""; const query = decodeUnicodeQuery(rawQuery); console.error(`原始查询: ${rawQuery}`); console.error(`解码后查询: ${query}`); try { // 创建 AbortController 用于取消请求 const controller = new AbortController(); activeRequests.set(id, controller); const notes = await searchNotes(query, controller.signal); console.error(`找到 ${notes.length} 条笔记`); // 请求完成,从活动请求中移除 activeRequests.delete(id); // 如果请求已被取消,不要发送响应 if (controller.signal.aborted) { console.error(`请求 ${id} 已被取消,不发送响应`); return; } console.log( JSON.stringify({ jsonrpc: "2.0", id: id, result: { items: notes.map((note) => ({ id: note.id, title: sanitizeText(note.title) || "无标题笔记", subtitle: note.cardType || "", text: { value: sanitizeText(note.content), type: "markdown", }, accessoryTitle: new Date(note.updatedAt).toLocaleDateString(), actions: [ { title: "打开笔记", type: "open", target: `anteey://open?noteId=${note.id}`, }, ], })), }, }) ); } catch (error) { // 请求出错,从活动请求中移除 activeRequests.delete(id); // 如果是请求被取消,返回特定错误码 if (error.name === "AbortError") { console.error("请求被取消"); console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32800, message: "请求已取消", }, }) ); return; } console.error("搜索笔记时出错:", error); console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32603, message: error.message || "搜索笔记时出错", }, }) ); } return; } // 处理未知方法 console.error(`未知方法: ${method}`); console.log( JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: -32601, message: `方法不存在: ${method}`, }, }) ); } catch (error) { console.error("处理请求时出错:", error); // 尝试发送 JSON-RPC 错误响应 try { console.log( JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32603, message: error.message || "内部错误", }, }) ); } catch (e) { console.error("发送错误响应时出错:", e); } } }); // 调用 Anteey MCP API 搜索笔记 function searchNotes(query, signal) { return new Promise((resolve, reject) => { try { const urlObj = new URL(`${config.serverUrl}/notes/search`); urlObj.searchParams.append("query", query); console.error(`请求 URL: ${urlObj.toString()}`); // 使用传入的 signal const options = { headers: { Authorization: `Bearer ${config.apiKey}`, }, signal, }; const req = http.get(urlObj, options, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { if (res.statusCode !== 200) { console.error(`API 请求失败: ${res.statusCode}`); console.error(`响应内容: ${data}`); 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) { console.error("解析 API 响应时出错:", error); reject(error); } }); }); req.on("error", (error) => { console.error("API 请求出错:", error); reject(error); }); req.end(); } catch (error) { console.error("创建请求时出错:", error); reject(error); } }); } // 初始测试,确认 API 连接正常 async function testConnection() { try { console.error("测试 API 连接..."); const url = `${config.serverUrl}/notes/search?query=test&limit=1`; http .get( url, { headers: { Authorization: `Bearer ${config.apiKey}`, }, timeout: config.timeout, // 添加超时 }, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { console.error(`API 状态响应: ${res.statusCode}`); console.error(`响应内容: ${data}`); if (res.statusCode === 200) { console.error("API 连接正常"); } else { console.error("API 连接异常"); } }); } ) .on("error", (error) => { console.error("API 连接测试失败:", error); }) .on("timeout", () => { console.error("API 连接测试超时"); }); } catch (error) { console.error("测试 API 连接时出错:", error); } } // 处理进程终止 process.on("SIGINT", () => { console.error("收到 SIGINT,正在退出..."); process.exit(0); }); process.on("SIGTERM", () => { console.error("收到 SIGTERM,正在退出..."); process.exit(0); }); // 注释掉自动测试,避免在配置加载前运行 // testConnection();