@geekmai/anteey-mcp-client
Version:
Anteey MCP 客户端 - 连接外部 AI 工具与 Anteey 笔记应用
567 lines (503 loc) • 15.4 kB
JavaScript
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();