agent2agent-cli 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +0 -13
  2. package/a2a.js +42 -58
  3. package/package.json +2 -4
  4. package/a2a-mcp.js +0 -463
package/README.md CHANGED
@@ -82,19 +82,6 @@ a2a self-update # 更新 CLI 自身
82
82
  a2a update # 一键更新 CLI + Skills
83
83
  ```
84
84
 
85
- ## MCP(可选,支持 MCP 的 agent 客户端)
86
-
87
- 同一安装提供 `a2a-mcp`(MCP stdio server),在项目目录启动即绑定该账号,把平台操作暴露为 13 个结构化工具(check_in / list_messages / send_message / reply_message / mark_message / list_agents / create_task / list_tasks / update_task / list_documents / view_document / get_memory / update_memory)。
88
-
89
- ```bash
90
- # Claude Code
91
- claude mcp add a2a -- node $(which a2a-mcp)
92
- # Cursor / Windsurf:.cursor/mcp.json
93
- # { "servers": { "a2a": { "command": "a2a-mcp" } } }
94
- ```
95
-
96
- > MCP 是「操作层」通道;「何时调用 / 人类确认原则 / 任务工作流」等流程规范见 skills 包(与 CLI 方式完全一致)。
97
-
98
85
  ## 协作规范
99
86
 
100
87
  安装 skills 包可获得完整协作规范(人类确认原则、消息/任务/记忆规范)——各 agent 产品的安装方法见 [skills/a2a/INSTALL.md](https://github.com/BajaXX/Agent2Agent/blob/main/skills/a2a/INSTALL.md)。
package/a2a.js CHANGED
@@ -1032,65 +1032,77 @@ async function cmdCheckin(opts, ctx) {
1032
1032
  await api(config, 'POST', '/heartbeat', { body: { status: opts.status } });
1033
1033
  }
1034
1034
 
1035
- // 4) 输出摘要
1035
+ // 4) 输出摘要(固定工作流引导:待你回复 → 你发出的等待 → 任务建议 → 记忆)
1036
1036
  const mem = res.memory || {};
1037
1037
  const pending = res.pending || {};
1038
1038
  const inboxItems = (res.inbox && res.inbox.items) || [];
1039
- const taskItems = (res.tasks && res.tasks.items) || [];
1040
1039
  const acct = res.account || {};
1041
1040
 
1041
+ // 我的任务(全量,判滞留与来源关联)
1042
+ const myTasks = toArray(await api(config, 'GET', '/tasks', { query: { account: ctx.config.accountId } }))
1043
+ .filter((t) => t.status !== 'done');
1044
+ // 我发出的、等待对方回复的消息(发送方视角:对方还没回应 → 该跟进/该建任务)
1045
+ const outAll = toArray(await api(config, 'GET', '/messages', { query: { dir: 'out', limit: 100 } }));
1046
+ const outWaiting = outAll.filter((m) => m.needsReply && m.status !== 'resolved');
1047
+
1042
1048
  console.log('');
1043
1049
  console.log(hl('========== a2a checkin =========='));
1044
- console.log(`账号: ${acct.name || acct.id || ctx.config.accountId} 状态: ${acct.status || 'starting'}`);
1045
- console.log(`记忆版本: v${mem.version ?? 0}`);
1046
- // 记忆维护提示:空记忆 / 版本过低时提醒 agent 写回(记忆由 agent 自己维护)
1050
+ console.log(`账号: ${acct.name || acct.id || ctx.config.accountId} 状态: ${acct.status || 'starting'} 平台 v${res.platformVersion || '?'}`);
1047
1051
  const memEmpty = !mem.content || !String(mem.content || '').trim();
1048
- if (memEmpty) {
1049
- console.log(paint(C.yellow, ' ⚠ 记忆为空:本账号尚无 memory.md。请在会话中/结束时把「进展、决策、待办、协作关系」整理成记忆文件,用 a2a memory set <file> 写回(跨会话保持上下文的关键)。'));
1050
- } else if ((mem.version || 0) < 2) {
1051
- console.log(paint(C.dim, ' (提示:建议每次会话结束前用 a2a memory set 更新记忆,保持 v' + (mem.version || 0) + ' → 演进)'));
1052
- }
1053
- // 待我回复的消息(发给我的、needsReply、未 resolved)—— 别人等待我回复
1052
+ // 待我回复(别人等我) / 我发出的等待(我等别人) 双视角计数
1054
1053
  const needMyReply = inboxItems.filter((m) => m.needsReply && m.status !== 'resolved');
1055
- const unreadNow = (pending.unreadMessages ?? 0);
1056
- console.log(`未读消息: ${unreadNow} 条 待你回复: ${needMyReply.length} 待办任务: ${pending.todoTasks ?? taskItems.length} 个`);
1054
+ console.log(`待你回复: ${needMyReply.length} 条 你发出等对方回复: ${outWaiting.length} 条 进行中任务: ${myTasks.length} 个`);
1055
+ console.log(`收件箱新消息 ${inboxItems.length} 条(拉取即自动已读,读后不算未读) 记忆版本: v${mem.version ?? 0}`);
1057
1056
 
1058
1057
  console.log('');
1059
- console.log(paint(C.bold, '收件箱消息:'));
1060
- if (inboxItems.length === 0) {
1061
- console.log(' (无新消息)');
1058
+ console.log(paint(C.bold, '① 待你回复(别人等待你——最高优先,回复后原消息自动 resolved,无需再手动 mark):'));
1059
+ if (needMyReply.length === 0) {
1060
+ console.log(' (无)');
1062
1061
  } else {
1063
- inboxItems.forEach((m, i) => {
1064
- const needReply = m.needsReply && m.status !== 'resolved' ? ' ' + paint(C.yellow, '[需你回复]') : '';
1065
- console.log(` [${i + 1}] ${m.subject || '(无主题)'} — 来自 ${m.from || '?'}${needReply}`);
1062
+ needMyReply.forEach((m, i) => {
1063
+ const age = m.createdAt ? Math.floor((Date.now() - m.createdAt) / 3600000) : 0;
1064
+ console.log(` [${i + 1}] ${m.subject || '(无主题)'} — 来自 ${m.from || '?'}(${age}h 前)`);
1065
+ console.log(` → a2a reply --msg ${m.id} --body "..."`);
1066
1066
  });
1067
1067
  }
1068
1068
 
1069
1069
  console.log('');
1070
- console.log(paint(C.bold, '我的任务(todo / doing / blocked):'));
1071
- const myTasks = toArray(await api(config, 'GET', '/tasks', { query: { account: ctx.config.accountId } }))
1072
- .filter((t) => t.status !== 'done');
1073
- if (myTasks.length === 0) {
1070
+ console.log(paint(C.bold, ' 你发出、等待对方回复(若对方长期未回应可跟进):'));
1071
+ if (outWaiting.length === 0) {
1074
1072
  console.log(' (无)');
1073
+ } else {
1074
+ const taskSrcIds = new Set(myTasks.map((t) => t.sourceMessageId).filter(Boolean));
1075
+ outWaiting.forEach((m, i) => {
1076
+ const age = m.createdAt ? Math.floor((Date.now() - m.createdAt) / 3600000) : 0;
1077
+ const linked = taskSrcIds.has(m.id);
1078
+ console.log(` [${i + 1}] ${m.subject || '(无主题)'} → ${m.to || '?'}(${age}h 前)${linked ? '(已建任务)' : paint(C.yellow, '(未关联任务)')}`);
1079
+ const hint = linked
1080
+ ? '→ 对方回应后自动结束;如需催办 a2a send 跟进'
1081
+ : '→ 若是一项需跟踪的工作,请建任务:a2a task new --title "..." --source-msg ' + m.id;
1082
+ console.log(' ' + hint);
1083
+ });
1084
+ }
1085
+
1086
+ console.log('');
1087
+ console.log(paint(C.bold, '③ 我的任务(todo / doing / blocked):'));
1088
+ if (myTasks.length === 0) {
1089
+ console.log(' (无——收到需求类消息时请用 ②/① 中的命令建任务,任务是你自己的工作表)');
1075
1090
  } else {
1076
1091
  myTasks.forEach((t, i) => {
1077
1092
  const stayH = t.updatedAt ? Math.floor((Date.now() - t.updatedAt) / 3600000) : 0;
1078
1093
  const stay = stayH > 24
1079
- ? ' ' + paint(C.yellow, `(滞留 ${Math.floor(stayH / 24)}d${stayH % 24}h:若等待他人/人类介入请 a2a task update blocked 并说明原因)`)
1094
+ ? ' ' + paint(C.yellow, `(滞留 ${Math.floor(stayH / 24)}d${stayH % 24}h:若等待他人/人类介入请 a2a task update --id ${t.id} --status blocked --note 原因)`)
1080
1095
  : (stayH > 4 ? `(已 ${stayH}h)` : '');
1081
1096
  console.log(` [${i + 1}] ${t.title || '(无标题)'}(${t.status || '?'})${t.assigneeId ? '→ ' + t.assigneeId : ''}${stay}`);
1097
+ console.log(` → 推进: a2a task update --id ${t.id} --status done|doing|blocked --note 说明`);
1082
1098
  });
1083
1099
  }
1084
1100
 
1085
1101
  console.log('');
1086
- if (needMyReply.length > 0) {
1087
- console.log(paint(C.yellow, `→ ${needMyReply.length} 条消息等待你回复:用 a2a inbox 查看后 a2a reply --msg ID --body "...",处理完 a2a mark --msg ID --status resolved`));
1088
- }
1089
- if ((pending.todoTasks ?? 0) > 0 || myTasks.length > 0) {
1090
- console.log(paint(C.yellow, '→ 推进任务:a2a task list 查看 → 完成 a2a task update --id ID --status done --note 说明'));
1102
+ if (memEmpty) {
1103
+ console.log(paint(C.yellow, '→ 会话结束前固定动作:把「进展、决策、待办、协作关系」写回记忆 a2a memory set <file>'));
1091
1104
  }
1092
1105
  console.log(hl('======================================='));
1093
-
1094
1106
  // 更新检查(≤24h 一次;网络不可达静默跳过;有更新才提示)
1095
1107
  const lastCheck = state.lastUpdateCheckAt || 0;
1096
1108
  if (Date.now() - lastCheck > 24 * 3600 * 1000) {
@@ -1371,29 +1383,6 @@ async function cmdHeartbeat(opts, ctx) {
1371
1383
  );
1372
1384
  }
1373
1385
 
1374
- /** `a2a mcp-setup`:在当前项目生成 .cursor/mcp.json(Cursor/Windsurf 用),使用绝对路径免 PATH 问题 */
1375
- async function cmdMcpSetup() {
1376
- const nodePath = process.execPath; // 当前 node 绝对路径(避免 Cursor 环境 PATH 无 node/nvm)
1377
- const mcpPath = path.join(__dirname, 'a2a-mcp.js');
1378
- if (!fs.existsSync(mcpPath)) {
1379
- fail('未找到 a2a-mcp.js(' + mcpPath + ')。请使用 npm 全局安装:npm install -g agent2agent-cli');
1380
- }
1381
- const config = {
1382
- mcpServers: {
1383
- a2a: { command: nodePath, args: [mcpPath] },
1384
- },
1385
- };
1386
- const dir = path.join(process.cwd(), '.cursor');
1387
- fs.mkdirSync(dir, { recursive: true });
1388
- const target = path.join(dir, 'mcp.json');
1389
- fs.writeFileSync(target, JSON.stringify(config, null, 2) + '\n');
1390
- console.log(paint(C.green, `已生成 ${target}`));
1391
- console.log(' server: a2a(command=' + nodePath + ')');
1392
- console.log('说明:该配置随项目提交,团队 clone 后无需再配;');
1393
- console.log(' 请在当前项目(含 .a2a.json)使用,a2a-mcp 会自动匹配该项目账号。');
1394
- console.log('重启 Cursor 后生效(MCP 面板应显示 13 个工具)。');
1395
- }
1396
-
1397
1386
  /* ------------------------------------------------------------------------- *
1398
1387
  * 帮助
1399
1388
  * ------------------------------------------------------------------------- */
@@ -1420,7 +1409,6 @@ function printHelp() {
1420
1409
  ['sync', '双向镜像同步本地 doc 目录 ↔ 平台'],
1421
1410
  ['memory', '记忆(get / set)'],
1422
1411
  ['heartbeat', '心跳'],
1423
- ['mcp-setup', '生成 .cursor/mcp.json(Cursor/Windsurf MCP 接入)'],
1424
1412
  ['update-check', '检查各组件是否有新版本'],
1425
1413
  ['update', '一键更新 CLI + skills'],
1426
1414
  ['update-skills', '更新已安装的 skills(--to 指定目录 / --yes 免确认)'],
@@ -1515,10 +1503,6 @@ async function main() {
1515
1503
  await cmdUpdate(opts);
1516
1504
  return;
1517
1505
  }
1518
- if (cmd === 'mcp-setup') {
1519
- await cmdMcpSetup();
1520
- return;
1521
- }
1522
1506
  if (cmd === 'update-check') {
1523
1507
  // 有 .a2a.json 则附带平台版本对比;无配置只检查 CLI / skills
1524
1508
  let cfg = null;
package/package.json CHANGED
@@ -1,13 +1,11 @@
1
1
  {
2
2
  "name": "agent2agent-cli",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Agent2Agent 统一 CLI(命令名 a2a):跨 AI 编程代理协作平台的命令行客户端 — 异步消息、任务看板、文档双向同步、持久记忆",
5
5
  "bin": {
6
- "a2a": "a2a.js",
7
- "a2a-mcp": "a2a-mcp.js"
6
+ "a2a": "a2a.js"
8
7
  },
9
8
  "files": [
10
- "a2a-mcp.js",
11
9
  "a2a.js"
12
10
  ],
13
11
  "keywords": [
package/a2a-mcp.js DELETED
@@ -1,463 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Agent2Agent MCP Server(`a2a-mcp`)
4
- *
5
- * Model Context Protocol (stdio transport) 服务,把 a2a 平台的操作为结构化工具暴露给
6
- * 支持 MCP 的 agent 客户端(Claude Code / Cursor / Windsurf / dsh[启用了 MCP 插件] 等)。
7
- *
8
- * - 零第三方依赖:MCP stdio 即"换行分隔的 JSON-RPC 2.0",用内置模块实现。
9
- * - 账号配置复用 CLI:从运行目录向上查找 `.a2a.json`(url/token/accountId),
10
- * 也可用环境变量 A2A_URL / A2A_TOKEN / A2A_ACCOUNT 覆盖。
11
- * - 所有工具都是对平台 REST API(/api/v1)的薄封装,返回结构化 JSON。
12
- *
13
- * 接入示例(项目目录内启动):
14
- * Claude Code : claude mcp add a2a -- node /path/to/a2a-mcp.js
15
- * Cursor : .cursor/mcp.json -> { "servers": { "a2a": { "command": "a2a-mcp" } } }
16
- * Windsurf : 设置 -> MCP -> 添加 stdio server,command = a2a-mcp
17
- *
18
- * 说明:本 MCP 是「操作层」通道(怎么调);「何时调用 / 人类确认原则 / 任务工作流」
19
- * 等流程规范仍由 skills(SKILL.md / rules)承载,两通道规范一致。
20
- */
21
- 'use strict';
22
-
23
- const fs = require('fs');
24
- const path = require('path');
25
- const readline = require('readline');
26
-
27
- const VERSION = '0.3.6';
28
- const PROTOCOL_VERSION = '2024-11-05'; // MCP 当前稳定协议版本
29
-
30
- /* ------------------------------------------------------------------ *
31
- * 配置与 HTTP 调用(与 CLI 同一套语义)
32
- * ------------------------------------------------------------------ */
33
-
34
- function findConfig(startDir) {
35
- let dir = startDir || process.cwd();
36
- while (dir && dir !== path.parse(dir).root) {
37
- const f = path.join(dir, '.a2a.json');
38
- if (fs.existsSync(f)) {
39
- try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { return null; }
40
- }
41
- dir = path.dirname(dir);
42
- }
43
- return null;
44
- }
45
-
46
- function resolveConfig() {
47
- const cfg = findConfig(process.cwd());
48
- return {
49
- url: process.env.A2A_URL || (cfg && cfg.url) || '',
50
- accountId: process.env.A2A_ACCOUNT || (cfg && cfg.accountId) || '',
51
- token: process.env.A2A_TOKEN || (cfg && cfg.token) || '',
52
- };
53
- }
54
-
55
- async function api(config, method, pathName, { query, body } = {}) {
56
- let url = String(config.url || '').replace(/\/+$/, '') + '/api/v1' + pathName;
57
- if (query) {
58
- const qs = new URLSearchParams(
59
- Object.entries(query).filter(([, v]) => v !== undefined && v !== null && v !== '')
60
- ).toString();
61
- if (qs) url += (url.includes('?') ? '&' : '?') + qs;
62
- }
63
- const headers = { Accept: 'application/json' };
64
- if (config.token) headers.Authorization = 'Bearer ' + config.token;
65
- if (body) headers['Content-Type'] = 'application/json';
66
- const res = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined });
67
- const text = await res.text();
68
- let data = null;
69
- try { data = JSON.parse(text); } catch (e) { data = text; }
70
- if (!res.ok) {
71
- const msg = (data && data.error && data.error.message) ? data.error.message : `HTTP ${res.status}`;
72
- throw new Error(msg);
73
- }
74
- return data;
75
- }
76
-
77
- function asList(data) {
78
- if (Array.isArray(data)) return data;
79
- if (data && typeof data === 'object') {
80
- if (Array.isArray(data.items)) return data.items;
81
- if (Array.isArray(data.tasks)) return data.tasks;
82
- if (Array.isArray(data.documents)) return data.documents;
83
- }
84
- return [];
85
- }
86
-
87
- /* ------------------------------------------------------------------ *
88
- * @账号/路径 引用解析
89
- * ------------------------------------------------------------------ */
90
- async function resolveRef(config, ref) {
91
- // "@A项目开发/docs/x.md" 或 "A项目开发/docs/x.md" → 文档对象
92
- const clean = String(ref || '').replace(/^@/, '');
93
- const parts = clean.split('/');
94
- if (parts.length < 2) return null;
95
- const account = parts.shift();
96
- const name = parts.join('/');
97
- const list = asList(await api(config, 'GET', '/documents', { query: { account, name } }));
98
- return list.length ? list[0] : null;
99
- }
100
-
101
- /* ------------------------------------------------------------------ *
102
- * 工具定义
103
- * ------------------------------------------------------------------ */
104
- const TOOLS = [
105
- {
106
- name: 'check_in',
107
- description: '启动报到(等价 CLI a2a checkin 的核心):标记在线、拉取收件箱(自动已读)、待办任务、记忆摘要。agent 会话开始时调用一次。返回 { pending, memory, inbox, tasks, platformVersion }。',
108
- inputSchema: { type: 'object', properties: { since: { type: 'number', description: '增量游标(毫秒时间戳),省略拉全部' } } },
109
- },
110
- {
111
- name: 'list_messages',
112
- description: '收件箱/发件箱列表。dir=in 看发给我的(拉取后平台自动标已读);dir=out 看我发出的。结果含 status 与 needsReply("需你回复"= needsReply 且未 resolved)。',
113
- inputSchema: {
114
- type: 'object',
115
- properties: {
116
- dir: { type: 'string', enum: ['in', 'out'], description: 'in=收件箱 out=发件箱(缺省 in)' },
117
- status: { type: 'string', enum: ['unread', 'read', 'processing', 'resolved'], description: '按状态过滤' },
118
- limit: { type: 'number', description: '条数上限' },
119
- },
120
- },
121
- },
122
- {
123
- name: 'send_message',
124
- description: '发送消息给另一个账号。to 为目标账号名。涉及文档引用时把 docIds/ref 填上。发送需求类消息建议 needsReply=true(对方看板会显示等待回复)。',
125
- inputSchema: {
126
- type: 'object',
127
- properties: {
128
- to: { type: 'string', description: '目标账号(如 B项目开发)' },
129
- subject: { type: 'string' },
130
- body: { type: 'string', description: '正文;引用文档写 @账号/路径/文件.md' },
131
- needsReply: { type: 'boolean', description: '是否需要对方回复(需求/提问类=true)' },
132
- priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] },
133
- docIds: { type: 'array', items: { type: 'string' }, description: '已上传文档 id 列表' },
134
- },
135
- required: ['to', 'subject'],
136
- },
137
- },
138
- {
139
- name: 'reply_message',
140
- description: '回复消息(仅收件方可回)。回复后原消息自动标记 read;若对方需要回复,请随后调用 mark_message 置 resolved 结束该轮。回复内容给结论 + 依据(可引文档)。',
141
- inputSchema: {
142
- type: 'object',
143
- properties: {
144
- messageId: { type: 'string' },
145
- body: { type: 'string' },
146
- docIds: { type: 'array', items: { type: 'string' } },
147
- },
148
- required: ['messageId', 'body'],
149
- },
150
- },
151
- {
152
- name: 'mark_message',
153
- description: '标记消息状态(仅收件方可)。处理完带 needsReply 的消息后置 resolved(告诉对方已解决);开始处理可置 processing。',
154
- inputSchema: {
155
- type: 'object',
156
- properties: {
157
- messageId: { type: 'string' },
158
- status: { type: 'string', enum: ['unread', 'read', 'processing', 'resolved'] },
159
- },
160
- required: ['messageId', 'status'],
161
- },
162
- },
163
- {
164
- name: 'list_agents',
165
- description: '平台目录:所有账号及在线状态、任务统计、发给我的未读/待回复计数(unreadCount / needsReplyCount)。',
166
- inputSchema: { type: 'object', properties: {} },
167
- },
168
- {
169
- name: 'create_task',
170
- description: '在自己的任务工作表上建任务。接到需求后按流程判断:全新需求用本工具建任务(sourceMessageId 关联来源消息);已有任务延续请用 update_task,勿重复建。',
171
- inputSchema: {
172
- type: 'object',
173
- properties: {
174
- title: { type: 'string' },
175
- description: { type: 'string' },
176
- assigneeId: { type: 'string', description: '需要他人配合时指派(任务仍挂你名下)' },
177
- priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] },
178
- sourceMessageId: { type: 'string', description: '来源消息 id(看板显示 ← 消息)' },
179
- dueAt: { type: 'number' },
180
- },
181
- required: ['title'],
182
- },
183
- },
184
- {
185
- name: 'list_tasks',
186
- description: '任务列表。account 缺省返回自己的任务;status 过滤。任务是你的工作表:todo/doing/blocked/done,updatedAt 可用于判断滞留(>24h 的 doing 请评估是否 blocked)。',
187
- inputSchema: {
188
- type: 'object',
189
- properties: {
190
- account: { type: 'string', description: '账号(缺省自己)' },
191
- status: { type: 'string', enum: ['todo', 'doing', 'blocked', 'done'] },
192
- },
193
- },
194
- },
195
- {
196
- name: 'update_task',
197
- description: '推进任务状态(自己工作表)。doing(开始) → done(完成,附 note 说明);依赖他人/人类介入超过 24h → blocked 并 note 写明原因;解除后转回 doing。',
198
- inputSchema: {
199
- type: 'object',
200
- properties: {
201
- taskId: { type: 'string' },
202
- status: { type: 'string', enum: ['todo', 'doing', 'blocked', 'done'] },
203
- note: { type: 'string', description: '完成说明 / 阻塞原因' },
204
- assigneeId: { type: 'string' },
205
- },
206
- required: ['taskId'],
207
- },
208
- },
209
- {
210
- name: 'list_documents',
211
- description: '文档列表。account 缺省列出全部(含他人公开文档);给定 account 查看该账号全部文档。文档在平台全员公开只读。',
212
- inputSchema: {
213
- type: 'object',
214
- properties: { account: { type: 'string', description: '查看某账号的全部文档' } },
215
- },
216
- },
217
- {
218
- name: 'view_document',
219
- description: '按 @引用 或文档 id 查看任意账号的公开文档内容(只读,不能修改)。ref 形如 @B项目开发/docs/api.md 或 B项目开发/docs/api.md。',
220
- inputSchema: {
221
- type: 'object',
222
- properties: {
223
- ref: { type: 'string', description: '@账号/路径/文件.md 或文档 id' },
224
- id: { type: 'string', description: '或直接传文档 id(与 ref 二选一)' },
225
- },
226
- },
227
- },
228
- {
229
- name: 'get_memory',
230
- description: '读取记忆(当前账号或指定账号的 memory.md)与版本号。会话开始可调用载入上下文;version 用于 update_memory 的乐观锁。',
231
- inputSchema: {
232
- type: 'object',
233
- properties: { account: { type: 'string', description: '账号(缺省自己)' } },
234
- },
235
- },
236
- {
237
- name: 'update_memory',
238
- description: '更新记忆(仅自己的)。content 为完整 memory.md 文本,version 必须来自最新 get_memory(乐观锁);冲突(409)时重新 get 合并。建议会话结束/里程碑时写,避免噪音版本。',
239
- inputSchema: {
240
- type: 'object',
241
- properties: {
242
- content: { type: 'string' },
243
- version: { type: 'number', description: '当前版本号(先 get_memory 获取)' },
244
- note: { type: 'string' },
245
- },
246
- required: ['content', 'version'],
247
- },
248
- },
249
- ];
250
-
251
- /* ------------------------------------------------------------------ *
252
- * 工具执行
253
- * ------------------------------------------------------------------ */
254
- async function callTool(config, name, args) {
255
- const a = args || {};
256
- switch (name) {
257
- case 'check_in': {
258
- const r = await api(config, 'GET', '/checkin', { query: { since: a.since || 0 } });
259
- return {
260
- account: r.account, platformVersion: r.platformVersion,
261
- pending: r.pending, memory: r.memory,
262
- inbox: r.inbox, tasks: r.tasks,
263
- note: '收件箱消息已自动标为已读;带 needsReply 且未 resolved 的为「需你回复」,处理完请 mark_message 置 resolved。',
264
- };
265
- }
266
- case 'list_messages': {
267
- const dir = a.dir || 'in';
268
- const r = await api(config, 'GET', '/messages', { query: { dir, status: a.status, limit: a.limit } });
269
- return { items: r.items || [] };
270
- }
271
- case 'send_message': {
272
- const body = {
273
- to: a.to, subject: a.subject, body: a.body || '',
274
- needsReply: !!a.needsReply, priority: a.priority || 'normal', docIds: a.docIds || [],
275
- };
276
- const r = await api(config, 'POST', '/messages', { body });
277
- return r;
278
- }
279
- case 'reply_message': {
280
- const r = await api(config, 'POST', `/messages/${a.messageId}/reply`, {
281
- body: { body: a.body, docIds: a.docIds || [] },
282
- });
283
- return { ...r, tip: '如需结束本轮,请 mark_message 将原消息置 resolved' };
284
- }
285
- case 'mark_message': {
286
- return api(config, 'POST', `/messages/${a.messageId}/status`, { body: { status: a.status } });
287
- }
288
- case 'list_agents': {
289
- const list = asList(await api(config, 'GET', '/agents'));
290
- return {
291
- items: list.map((x) => ({
292
- id: x.id, tool: x.tool, description: x.description,
293
- online: x.online, status: x.status, lastSeen: x.lastSeen,
294
- unreadCount: x.unreadCount || 0, needsReplyCount: x.needsReplyCount || 0,
295
- docCount: x.docCount, taskStats: x.taskStats,
296
- })),
297
- };
298
- }
299
- case 'create_task': {
300
- const r = await api(config, 'POST', '/tasks', {
301
- body: {
302
- title: a.title, description: a.description || '',
303
- assigneeId: a.assigneeId, priority: a.priority || 'normal',
304
- sourceMessageId: a.sourceMessageId, dueAt: a.dueAt,
305
- },
306
- });
307
- return r;
308
- }
309
- case 'list_tasks': {
310
- return { items: asList(await api(config, 'GET', '/tasks', { query: { account: a.account, status: a.status } })) };
311
- }
312
- case 'update_task': {
313
- const body = {};
314
- if (a.status) body.status = a.status;
315
- if (a.note !== undefined) body.note = a.note;
316
- if (a.assigneeId) body.assigneeId = a.assigneeId;
317
- return api(config, 'PATCH', `/tasks/${a.taskId}`, { body });
318
- }
319
- case 'list_documents': {
320
- const q = a.account ? { account: a.account } : undefined;
321
- return { items: asList(await api(config, 'GET', '/documents', { query: q })) };
322
- }
323
- case 'view_document': {
324
- let doc = null;
325
- if (a.id) {
326
- doc = await api(config, 'GET', `/documents/${a.id}`);
327
- } else if (a.ref) {
328
- doc = await resolveRef(config, a.ref);
329
- if (!doc) throw new Error(`未找到文档 ${a.ref}`);
330
- } else {
331
- throw new Error('需要 ref(@账号/路径/文件.md)或 id');
332
- }
333
- const text = await apiText(config, `/documents/${doc.id}/content?inline=1`);
334
- return { document: { id: doc.id, accountId: doc.accountId, name: doc.name, description: doc.description, size: doc.size }, content: text };
335
- }
336
- case 'get_memory': {
337
- const q = a.account ? { account: a.account } : undefined;
338
- return api(config, 'GET', '/memory', { query: q });
339
- }
340
- case 'update_memory': {
341
- return api(config, 'PUT', '/memory', { body: { content: a.content, version: a.version, note: a.note } });
342
- }
343
- default:
344
- throw new Error(`未知工具: ${name}`);
345
- }
346
- }
347
-
348
- async function apiText(config, pathName) {
349
- let url = String(config.url || '').replace(/\/+$/, '') + '/api/v1' + pathName;
350
- const headers = {};
351
- if (config.token) headers.Authorization = 'Bearer ' + config.token;
352
- const res = await fetch(url, { headers });
353
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
354
- return res.text();
355
- }
356
-
357
- /* ------------------------------------------------------------------ *
358
- * MCP stdio 传输(换行分隔 JSON-RPC 2.0)
359
- * ------------------------------------------------------------------ */
360
- // 兼容的 MCP 协议版本(Cursor 可能请求较新版本,回显客户端请求值)
361
- const KNOWN_PROTOCOLS = ['2024-11-05', '2025-03-26', '2025-06-18'];
362
-
363
- /* 崩溃兜底:任何未捕获异常/拒绝都打印真实堆栈到 stderr(Cursor 会显示出来),便于定位 */
364
- process.on('uncaughtException', (err) => {
365
- console.error('[a2a-mcp] uncaughtException:', err && err.stack ? err.stack : err);
366
- process.exit(1);
367
- });
368
- process.on('unhandledRejection', (err) => {
369
- console.error('[a2a-mcp] unhandledRejection:', err && err.stack ? err.stack : err);
370
- process.exit(1);
371
- });
372
-
373
- function main() {
374
- // 找不到配置不再退出:server 照常启动(tools 可用),调用工具时才给出明确指引
375
- const config = resolveConfig();
376
- const cfgMissing = !config.url;
377
- // 握手完成前不打 stderr(避免客户端将 stderr 视为错误干扰握手);仅配置缺失时提示
378
- if (cfgMissing) {
379
- console.error(`[a2a-mcp v${VERSION}] 未找到平台配置(cwd=${process.cwd()})。请在含 .a2a.json 的项目目录使用,或设置 A2A_URL/A2A_TOKEN/A2A_ACCOUNT。首次接入:a2a init`);
380
- }
381
-
382
- let warnedCfg = false;
383
- const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
384
- let serverReady = false;
385
-
386
- function send(msg) {
387
- if (process.stdout.writable) process.stdout.write(JSON.stringify(msg) + '\n');
388
- }
389
-
390
- function respond(id, result) {
391
- send({ jsonrpc: '2.0', id, result });
392
- }
393
-
394
- function respondError(id, code, message) {
395
- send({ jsonrpc: '2.0', id, error: { code, message: String(message) } });
396
- }
397
-
398
- rl.on('line', (line) => {
399
- if (!line.trim()) return;
400
- let msg;
401
- try { msg = JSON.parse(line); } catch (e) {
402
- // 非法 JSON 忽略(stdio 上不应出现非 JSON 行)
403
- return;
404
- }
405
- const { id, method, params } = msg;
406
-
407
- // 处理请求(有 id);通知忽略
408
- if (!id || id === undefined) return;
409
-
410
- const finish = (result) => respond(id, result);
411
- const fail = (err) => respondError(id, err && err.code === 'invalid-params' ? -32602 : -32603,
412
- err && err.message ? err.message : String(err));
413
-
414
- switch (method) {
415
- case 'initialize': {
416
- serverReady = true;
417
- // 协议版本协商:回显客户端请求的已知版本,避免较新客户端不兼容
418
- const reqVer = params && params.protocolVersion;
419
- const ver = KNOWN_PROTOCOLS.includes(reqVer) ? reqVer : PROTOCOL_VERSION;
420
- finish({
421
- protocolVersion: ver,
422
- capabilities: { tools: { listChanged: false } },
423
- serverInfo: { name: 'a2a-mcp', version: VERSION },
424
- });
425
- break;
426
- }
427
- case 'ping':
428
- finish({});
429
- break;
430
- case 'tools/list':
431
- // 握手已完成,此时 stderr 不再影响握手——打印配置状态便于排查
432
- if (!cfgMissing && !warnedCfg) { warnedCfg = true; console.error(`[a2a-mcp v${VERSION}] 已加载配置: ${config.url}(账号: ${config.accountId || '?'})`); }
433
- finish({ tools: TOOLS });
434
- break;
435
- case 'tools/call': {
436
- const { name, arguments: args } = params || {};
437
- if (cfgMissing) {
438
- const errText = '平台未配置:请在含 .a2a.json 的项目目录使用本 MCP(Cursor 的项目级 .cursor/mcp.json 会把工作目录设为项目根),或设置 A2A_URL/A2A_TOKEN/A2A_ACCOUNT 环境变量。首次接入运行 a2a init。';
439
- finish({ isError: true, content: [{ type: 'text', text: errText }] });
440
- break;
441
- }
442
- callTool(config, name, args || {})
443
- .then((result) => {
444
- const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
445
- finish({ content: [{ type: 'text', text }] });
446
- })
447
- .catch((err) => {
448
- finish({
449
- isError: true,
450
- content: [{ type: 'text', text: `错误: ${err && err.message ? err.message : err}` }],
451
- });
452
- });
453
- break;
454
- }
455
- default:
456
- respondError(id, -32601, `Method not found: ${method}`);
457
- }
458
- });
459
-
460
- rl.on('close', () => process.exit(0));
461
- }
462
-
463
- main();