agent2agent-cli 0.3.5 → 0.3.7
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.
- package/README.md +0 -13
- package/a2a.js +0 -28
- package/package.json +2 -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
|
@@ -1371,29 +1371,6 @@ async function cmdHeartbeat(opts, ctx) {
|
|
|
1371
1371
|
);
|
|
1372
1372
|
}
|
|
1373
1373
|
|
|
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
1374
|
/* ------------------------------------------------------------------------- *
|
|
1398
1375
|
* 帮助
|
|
1399
1376
|
* ------------------------------------------------------------------------- */
|
|
@@ -1420,7 +1397,6 @@ function printHelp() {
|
|
|
1420
1397
|
['sync', '双向镜像同步本地 doc 目录 ↔ 平台'],
|
|
1421
1398
|
['memory', '记忆(get / set)'],
|
|
1422
1399
|
['heartbeat', '心跳'],
|
|
1423
|
-
['mcp-setup', '生成 .cursor/mcp.json(Cursor/Windsurf MCP 接入)'],
|
|
1424
1400
|
['update-check', '检查各组件是否有新版本'],
|
|
1425
1401
|
['update', '一键更新 CLI + skills'],
|
|
1426
1402
|
['update-skills', '更新已安装的 skills(--to 指定目录 / --yes 免确认)'],
|
|
@@ -1515,10 +1491,6 @@ async function main() {
|
|
|
1515
1491
|
await cmdUpdate(opts);
|
|
1516
1492
|
return;
|
|
1517
1493
|
}
|
|
1518
|
-
if (cmd === 'mcp-setup') {
|
|
1519
|
-
await cmdMcpSetup();
|
|
1520
|
-
return;
|
|
1521
|
-
}
|
|
1522
1494
|
if (cmd === 'update-check') {
|
|
1523
1495
|
// 有 .a2a.json 则附带平台版本对比;无配置只检查 CLI / skills
|
|
1524
1496
|
let cfg = null;
|
package/package.json
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent2agent-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
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.5';
|
|
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
|
-
if (cfgMissing) {
|
|
378
|
-
console.error(`[a2a-mcp v${VERSION}] 当前工作目录: ${process.cwd()}`);
|
|
379
|
-
console.error('[a2a-mcp] 未找到平台配置(.a2a.json):请在含 .a2a.json 的项目目录使用;');
|
|
380
|
-
console.error('[a2a-mcp] 或设置 A2A_URL / A2A_TOKEN / A2A_ACCOUNT 环境变量;首次接入:a2a init');
|
|
381
|
-
} else {
|
|
382
|
-
console.error(`[a2a-mcp v${VERSION}] 已加载配置: ${config.url}(账号: ${config.accountId || '?'})`);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
386
|
-
let serverReady = false;
|
|
387
|
-
|
|
388
|
-
function send(msg) {
|
|
389
|
-
if (process.stdout.writable) process.stdout.write(JSON.stringify(msg) + '\n');
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function respond(id, result) {
|
|
393
|
-
send({ jsonrpc: '2.0', id, result });
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function respondError(id, code, message) {
|
|
397
|
-
send({ jsonrpc: '2.0', id, error: { code, message: String(message) } });
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
rl.on('line', (line) => {
|
|
401
|
-
if (!line.trim()) return;
|
|
402
|
-
let msg;
|
|
403
|
-
try { msg = JSON.parse(line); } catch (e) {
|
|
404
|
-
// 非法 JSON 忽略(stdio 上不应出现非 JSON 行)
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const { id, method, params } = msg;
|
|
408
|
-
|
|
409
|
-
// 处理请求(有 id);通知忽略
|
|
410
|
-
if (!id || id === undefined) return;
|
|
411
|
-
|
|
412
|
-
const finish = (result) => respond(id, result);
|
|
413
|
-
const fail = (err) => respondError(id, err && err.code === 'invalid-params' ? -32602 : -32603,
|
|
414
|
-
err && err.message ? err.message : String(err));
|
|
415
|
-
|
|
416
|
-
switch (method) {
|
|
417
|
-
case 'initialize': {
|
|
418
|
-
serverReady = true;
|
|
419
|
-
// 协议版本协商:回显客户端请求的已知版本,避免较新客户端不兼容
|
|
420
|
-
const reqVer = params && params.protocolVersion;
|
|
421
|
-
const ver = KNOWN_PROTOCOLS.includes(reqVer) ? reqVer : PROTOCOL_VERSION;
|
|
422
|
-
finish({
|
|
423
|
-
protocolVersion: ver,
|
|
424
|
-
capabilities: { tools: { listChanged: false } },
|
|
425
|
-
serverInfo: { name: 'a2a-mcp', version: VERSION },
|
|
426
|
-
});
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
case 'ping':
|
|
430
|
-
finish({});
|
|
431
|
-
break;
|
|
432
|
-
case 'tools/list':
|
|
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();
|