@geekmai/anteey-mcp-client
Version:
Anteey MCP 客户端 - 连接外部 AI 工具与 Anteey 笔记应用
658 lines (578 loc) • 16.9 kB
JavaScript
#!/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 请求..."));