agent2agent-cli 0.3.1 → 0.3.3
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 +13 -0
- package/a2a-mcp.js +437 -0
- package/a2a.js +53 -15
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -82,6 +82,19 @@ 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
|
+
|
|
85
98
|
## 协作规范
|
|
86
99
|
|
|
87
100
|
安装 skills 包可获得完整协作规范(人类确认原则、消息/任务/记忆规范)——各 agent 产品的安装方法见 [skills/a2a/INSTALL.md](https://github.com/BajaXX/Agent2Agent/blob/main/skills/a2a/INSTALL.md)。
|
package/a2a-mcp.js
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
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.3';
|
|
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
|
+
function main() {
|
|
361
|
+
const config = resolveConfig();
|
|
362
|
+
if (!config.url) {
|
|
363
|
+
console.error('[a2a-mcp] 未找到平台配置:请在项目根运行(含 .a2a.json),或设置 A2A_URL / A2A_TOKEN / A2A_ACCOUNT');
|
|
364
|
+
console.error('[a2a-mcp] 首次接入:a2a init');
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
369
|
+
let serverReady = false;
|
|
370
|
+
|
|
371
|
+
function send(msg) {
|
|
372
|
+
if (process.stdout.writable) process.stdout.write(JSON.stringify(msg) + '\n');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function respond(id, result) {
|
|
376
|
+
send({ jsonrpc: '2.0', id, result });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function respondError(id, code, message) {
|
|
380
|
+
send({ jsonrpc: '2.0', id, error: { code, message: String(message) } });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
rl.on('line', (line) => {
|
|
384
|
+
if (!line.trim()) return;
|
|
385
|
+
let msg;
|
|
386
|
+
try { msg = JSON.parse(line); } catch (e) {
|
|
387
|
+
// 非法 JSON 忽略(stdio 上不应出现非 JSON 行)
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const { id, method, params } = msg;
|
|
391
|
+
|
|
392
|
+
// 处理请求(有 id);通知忽略
|
|
393
|
+
if (!id || id === undefined) return;
|
|
394
|
+
|
|
395
|
+
const finish = (result) => respond(id, result);
|
|
396
|
+
const fail = (err) => respondError(id, err && err.code === 'invalid-params' ? -32602 : -32603,
|
|
397
|
+
err && err.message ? err.message : String(err));
|
|
398
|
+
|
|
399
|
+
switch (method) {
|
|
400
|
+
case 'initialize':
|
|
401
|
+
serverReady = true;
|
|
402
|
+
finish({
|
|
403
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
404
|
+
capabilities: { tools: { listChanged: false } },
|
|
405
|
+
serverInfo: { name: 'a2a-mcp', version: VERSION },
|
|
406
|
+
});
|
|
407
|
+
break;
|
|
408
|
+
case 'ping':
|
|
409
|
+
finish({});
|
|
410
|
+
break;
|
|
411
|
+
case 'tools/list':
|
|
412
|
+
finish({ tools: TOOLS });
|
|
413
|
+
break;
|
|
414
|
+
case 'tools/call': {
|
|
415
|
+
const { name, arguments: args } = params || {};
|
|
416
|
+
callTool(config, name, args || {})
|
|
417
|
+
.then((result) => {
|
|
418
|
+
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
419
|
+
finish({ content: [{ type: 'text', text }] });
|
|
420
|
+
})
|
|
421
|
+
.catch((err) => {
|
|
422
|
+
finish({
|
|
423
|
+
isError: true,
|
|
424
|
+
content: [{ type: 'text', text: `错误: ${err && err.message ? err.message : err}` }],
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
default:
|
|
430
|
+
respondError(id, -32601, `Method not found: ${method}`);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
rl.on('close', () => process.exit(0));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
main();
|
package/a2a.js
CHANGED
|
@@ -757,10 +757,13 @@ async function cmdUpdateSkills(opts) {
|
|
|
757
757
|
console.log('');
|
|
758
758
|
process.stdout.write(paint(C.dim, '确认更新?(y/N,30 秒无输入自动取消) '));
|
|
759
759
|
const nextLine = createLineReader();
|
|
760
|
+
let confirmTimer = null;
|
|
760
761
|
const ans = await Promise.race([
|
|
761
762
|
nextLine(),
|
|
762
|
-
new Promise((r) => setTimeout(() => r(''), 30000)),
|
|
763
|
+
new Promise((r) => { confirmTimer = setTimeout(() => r(''), 30000); }),
|
|
763
764
|
]);
|
|
765
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
766
|
+
nextLine.close();
|
|
764
767
|
if (!/^y/i.test(String(ans || ''))) {
|
|
765
768
|
console.log(paint(C.yellow, '已取消(可用 a2a update-skills --yes 跳过确认)'));
|
|
766
769
|
return;
|
|
@@ -802,6 +805,7 @@ async function cmdUpdateSkills(opts) {
|
|
|
802
805
|
/** `a2a update`:一键更新 CLI + Skills(平台由运维执行 docker 命令) */
|
|
803
806
|
async function cmdUpdate(opts) {
|
|
804
807
|
console.log(hl('===== a2a update ====='));
|
|
808
|
+
let updated = false;
|
|
805
809
|
if (VERSION) {
|
|
806
810
|
// 先检查 CLI 版本
|
|
807
811
|
const pkg = await fetchJson('https://registry.npmjs.org/' + NPM_PACKAGE + '/latest');
|
|
@@ -810,6 +814,7 @@ async function cmdUpdate(opts) {
|
|
|
810
814
|
try {
|
|
811
815
|
execSync(`npm install -g ${NPM_PACKAGE}@latest`, { stdio: 'inherit' });
|
|
812
816
|
console.log(paint(C.green, '[CLI] 更新完成 ✅'));
|
|
817
|
+
updated = true;
|
|
813
818
|
} catch (e) {
|
|
814
819
|
console.log(paint(C.red, `[CLI] 更新失败,请手动:npm install -g ${NPM_PACKAGE}@latest`));
|
|
815
820
|
}
|
|
@@ -819,9 +824,15 @@ async function cmdUpdate(opts) {
|
|
|
819
824
|
} else {
|
|
820
825
|
console.log(paint(C.yellow, '[CLI] 单文件安装无版本信息,建议:npm install -g ' + NPM_PACKAGE));
|
|
821
826
|
}
|
|
827
|
+
if (updated) {
|
|
828
|
+
// 本进程仍是旧代码:让用户退出后重跑,以用新版本完成其余更新
|
|
829
|
+
const pkg2 = await fetchJson('https://registry.npmjs.org/' + NPM_PACKAGE + '/latest').catch(() => null);
|
|
830
|
+
console.log(paint(C.yellow, 'CLI 已更新到 v' + (pkg2 ? pkg2.version : '最新') + '。请退出后重新运行 a2a update(或直接运行 a2a update-skills)完成 skills 更新。'));
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
822
833
|
await cmdUpdateSkills({ yes: opts.yes || opts.y });
|
|
823
834
|
console.log('');
|
|
824
|
-
console.log('平台更新(在部署服务器执行):cd <仓库> && git pull && docker compose up -d --build');
|
|
835
|
+
console.log('平台更新(在部署服务器执行):cd <仓库> && git pull && docker compose up -d --build(或 docker compose -f docker-compose.pull.yml up -d)');
|
|
825
836
|
}
|
|
826
837
|
|
|
827
838
|
/* ------------------------------------------------------------------------- *
|
|
@@ -829,14 +840,15 @@ async function cmdUpdate(opts) {
|
|
|
829
840
|
* ------------------------------------------------------------------------- */
|
|
830
841
|
|
|
831
842
|
/** 交互式提问:依次向用户询问缺失的字段(仅 TTY 下启用) */
|
|
843
|
+
/** 交互式 stdin 行读取器(供 init 向导 / update-skills 确认使用) */
|
|
832
844
|
function createLineReader() {
|
|
833
845
|
// 自研逐行读取:输入提前到达时缓存到队列,等待者按序消费(兼容人机/伪终端/管道)
|
|
834
846
|
let buffer = '';
|
|
835
847
|
const queue = [];
|
|
836
848
|
const waiters = [];
|
|
849
|
+
let closed = false;
|
|
837
850
|
process.stdin.setEncoding('utf8');
|
|
838
|
-
|
|
839
|
-
process.stdin.on('data', (chunk) => {
|
|
851
|
+
const handler = (chunk) => {
|
|
840
852
|
buffer += chunk;
|
|
841
853
|
let i;
|
|
842
854
|
while ((i = buffer.indexOf('\n')) >= 0) {
|
|
@@ -846,11 +858,21 @@ function createLineReader() {
|
|
|
846
858
|
if (w) w(line);
|
|
847
859
|
else queue.push(line);
|
|
848
860
|
}
|
|
849
|
-
}
|
|
850
|
-
|
|
861
|
+
};
|
|
862
|
+
process.stdin.on('data', handler);
|
|
863
|
+
process.stdin.resume();
|
|
864
|
+
const nextLine = () => {
|
|
851
865
|
if (queue.length) return Promise.resolve(queue.shift());
|
|
852
866
|
return new Promise((resolve) => waiters.push(resolve));
|
|
853
867
|
};
|
|
868
|
+
// close:移除监听并暂停 stdin——否则 TTY(永不 EOF)会保持事件循环,命令结束后进程不退出
|
|
869
|
+
nextLine.close = () => {
|
|
870
|
+
if (closed) return;
|
|
871
|
+
closed = true;
|
|
872
|
+
process.stdin.removeListener('data', handler);
|
|
873
|
+
process.stdin.pause();
|
|
874
|
+
};
|
|
875
|
+
return nextLine;
|
|
854
876
|
}
|
|
855
877
|
|
|
856
878
|
async function promptInteractive(fields) {
|
|
@@ -868,6 +890,7 @@ async function promptInteractive(fields) {
|
|
|
868
890
|
}
|
|
869
891
|
answers[f.key] = val;
|
|
870
892
|
}
|
|
893
|
+
nextLine.close();
|
|
871
894
|
return answers;
|
|
872
895
|
}
|
|
873
896
|
|
|
@@ -1468,16 +1491,18 @@ async function main() {
|
|
|
1468
1491
|
await cmdUpdate(opts);
|
|
1469
1492
|
return;
|
|
1470
1493
|
}
|
|
1494
|
+
if (cmd === 'update-check') {
|
|
1495
|
+
// 有 .a2a.json 则附带平台版本对比;无配置只检查 CLI / skills
|
|
1496
|
+
let cfg = null;
|
|
1497
|
+
try { cfg = requireConfig(opts.config); } catch (e) { cfg = null; }
|
|
1498
|
+
await cmdUpdateCheck(cfg || { config: null });
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1471
1501
|
|
|
1472
1502
|
let ctx = null;
|
|
1473
1503
|
try {
|
|
1474
1504
|
ctx = requireConfig(opts.config);
|
|
1475
1505
|
} catch (e) {
|
|
1476
|
-
// update-check 允许无配置运行(仅检查 CLI / skills);其余命令必须配置
|
|
1477
|
-
if (cmd === 'update-check') {
|
|
1478
|
-
await cmdUpdateCheck({ config: null });
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
1506
|
process.stderr.write(String(e.message || e) + '\n');
|
|
1482
1507
|
process.exit(1);
|
|
1483
1508
|
}
|
|
@@ -1537,7 +1562,20 @@ async function main() {
|
|
|
1537
1562
|
}
|
|
1538
1563
|
}
|
|
1539
1564
|
|
|
1540
|
-
main()
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1565
|
+
main()
|
|
1566
|
+
.catch((err) => {
|
|
1567
|
+
const msg = err && err.message ? err.message : String(err);
|
|
1568
|
+
fail(msg);
|
|
1569
|
+
})
|
|
1570
|
+
.finally(() => {
|
|
1571
|
+
// 统一「跑完即退」守卫:命令逻辑完成后,若仍有残留句柄(stdin 监听、定时器等)
|
|
1572
|
+
// 阻止事件循环退出,短暂等待 stdout flush 后强制退出,回到 shell。
|
|
1573
|
+
const guard = setTimeout(() => {
|
|
1574
|
+
try {
|
|
1575
|
+
process.stdin.pause();
|
|
1576
|
+
if (process.stdin.removeAllListeners) process.stdin.removeAllListeners('data');
|
|
1577
|
+
} catch (e) { /* ignore */ }
|
|
1578
|
+
process.exit(process.exitCode || 0);
|
|
1579
|
+
}, 600);
|
|
1580
|
+
if (guard.unref) guard.unref();
|
|
1581
|
+
});
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent2agent-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Agent2Agent 统一 CLI(命令名 a2a):跨 AI 编程代理协作平台的命令行客户端 — 异步消息、任务看板、文档双向同步、持久记忆",
|
|
5
5
|
"bin": {
|
|
6
|
-
"a2a": "a2a.js"
|
|
6
|
+
"a2a": "a2a.js",
|
|
7
|
+
"a2a-mcp": "a2a-mcp.js"
|
|
7
8
|
},
|
|
8
9
|
"files": [
|
|
10
|
+
"a2a-mcp.js",
|
|
9
11
|
"a2a.js"
|
|
10
12
|
],
|
|
11
13
|
"keywords": [
|