agent2agent-cli 0.3.0 → 0.3.2
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 +47 -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.2';
|
|
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
|
@@ -689,10 +689,19 @@ const SKILL_FILES = [
|
|
|
689
689
|
'rules/cursor.mdc',
|
|
690
690
|
];
|
|
691
691
|
|
|
692
|
-
/**
|
|
692
|
+
/** 下载单个文件到目标(带超时,避免网络挂起;失败抛明确错误) */
|
|
693
693
|
async function downloadTo(relPath, destDir) {
|
|
694
694
|
const url = `${SKILLS_RAW_BASE}/${relPath}`;
|
|
695
|
-
const
|
|
695
|
+
const ctrl = new AbortController();
|
|
696
|
+
const timer = setTimeout(() => ctrl.abort(), 20000); // 20s 超时
|
|
697
|
+
let res;
|
|
698
|
+
try {
|
|
699
|
+
res = await fetch(url, { headers: { 'User-Agent': 'a2a-cli' }, signal: ctrl.signal });
|
|
700
|
+
} catch (e) {
|
|
701
|
+
clearTimeout(timer);
|
|
702
|
+
throw new Error(`连接超时/失败(${SKILLS_RAW_BASE.replace(/^https?:\/\//, '')}),无法访问 GitHub 下载源;可稍后重试,或设置镜像源:A2A_SKILLS_URL=<可访问的镜像地址>`);
|
|
703
|
+
}
|
|
704
|
+
clearTimeout(timer);
|
|
696
705
|
if (!res.ok) throw new Error(`下载失败 ${relPath} (HTTP ${res.status})`);
|
|
697
706
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
698
707
|
const dest = path.join(destDir, ...relPath.split('/'));
|
|
@@ -743,24 +752,34 @@ async function cmdUpdateSkills(opts) {
|
|
|
743
752
|
|
|
744
753
|
console.log(hl('===== a2a update-skills ====='));
|
|
745
754
|
for (const t of targets) console.log(` 将更新: [${t.label}]`);
|
|
755
|
+
|
|
746
756
|
if (!autoYes && process.stdin.isTTY) {
|
|
747
757
|
console.log('');
|
|
748
|
-
process.stdout.write('确认更新?(y/N) ');
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
758
|
+
process.stdout.write(paint(C.dim, '确认更新?(y/N,30 秒无输入自动取消) '));
|
|
759
|
+
const nextLine = createLineReader();
|
|
760
|
+
let confirmTimer = null;
|
|
761
|
+
const ans = await Promise.race([
|
|
762
|
+
nextLine(),
|
|
763
|
+
new Promise((r) => { confirmTimer = setTimeout(() => r(''), 30000); }),
|
|
764
|
+
]);
|
|
765
|
+
if (confirmTimer) clearTimeout(confirmTimer);
|
|
766
|
+
nextLine.close();
|
|
767
|
+
if (!/^y/i.test(String(ans || ''))) {
|
|
768
|
+
console.log(paint(C.yellow, '已取消(可用 a2a update-skills --yes 跳过确认)'));
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
754
771
|
} else if (!autoYes && !process.stdin.isTTY) {
|
|
755
772
|
console.log(paint(C.yellow, '(非交互环境:加 --yes 跳过确认)'));
|
|
756
773
|
if (!process.env.A2A_AUTO_UPDATE) return;
|
|
757
774
|
}
|
|
758
775
|
|
|
776
|
+
console.log(paint(C.dim, `下载源: ${SKILLS_RAW_BASE}(网络较慢时可用 A2A_SKILLS_URL 指定镜像)`));
|
|
759
777
|
let okCount = 0;
|
|
760
778
|
for (const t of targets) {
|
|
761
779
|
try {
|
|
762
780
|
if (t.type === 'cursor') {
|
|
763
781
|
// 规则目录:写入 a2a.mdc,删除旧文件名
|
|
782
|
+
process.stdout.write(` ⏳ ${t.label} 下载中...`);
|
|
764
783
|
const dest = await downloadTo('rules/cursor.mdc', t.dir);
|
|
765
784
|
const renamed = path.join(t.dir, 'a2a.mdc');
|
|
766
785
|
fs.renameSync(dest, renamed);
|
|
@@ -768,14 +787,15 @@ async function cmdUpdateSkills(opts) {
|
|
|
768
787
|
const p = path.join(t.dir, old);
|
|
769
788
|
if (fs.existsSync(p)) { try { fs.unlinkSync(p); } catch (e) { /* ignore */ } }
|
|
770
789
|
}
|
|
771
|
-
console.log(
|
|
790
|
+
console.log(`\r ✓ ${t.label} → ${renamed}`);
|
|
772
791
|
} else {
|
|
792
|
+
process.stdout.write(` ⏳ ${t.label} 下载中...`);
|
|
773
793
|
for (const rel of SKILL_FILES) await downloadTo(rel, t.dir);
|
|
774
|
-
console.log(
|
|
794
|
+
console.log(`\r ✓ ${t.label} → ${t.dir}(${SKILL_FILES.length} 个文件)`);
|
|
775
795
|
}
|
|
776
796
|
okCount++;
|
|
777
797
|
} catch (e) {
|
|
778
|
-
console.log(
|
|
798
|
+
console.log(`\r ✗ ${t.label} 更新失败: ${paint(C.red, e.message)}`);
|
|
779
799
|
}
|
|
780
800
|
}
|
|
781
801
|
console.log(hl(`更新完成(成功 ${okCount}/${targets.length})`));
|
|
@@ -812,14 +832,15 @@ async function cmdUpdate(opts) {
|
|
|
812
832
|
* ------------------------------------------------------------------------- */
|
|
813
833
|
|
|
814
834
|
/** 交互式提问:依次向用户询问缺失的字段(仅 TTY 下启用) */
|
|
835
|
+
/** 交互式 stdin 行读取器(供 init 向导 / update-skills 确认使用) */
|
|
815
836
|
function createLineReader() {
|
|
816
837
|
// 自研逐行读取:输入提前到达时缓存到队列,等待者按序消费(兼容人机/伪终端/管道)
|
|
817
838
|
let buffer = '';
|
|
818
839
|
const queue = [];
|
|
819
840
|
const waiters = [];
|
|
841
|
+
let closed = false;
|
|
820
842
|
process.stdin.setEncoding('utf8');
|
|
821
|
-
|
|
822
|
-
process.stdin.on('data', (chunk) => {
|
|
843
|
+
const handler = (chunk) => {
|
|
823
844
|
buffer += chunk;
|
|
824
845
|
let i;
|
|
825
846
|
while ((i = buffer.indexOf('\n')) >= 0) {
|
|
@@ -829,11 +850,21 @@ function createLineReader() {
|
|
|
829
850
|
if (w) w(line);
|
|
830
851
|
else queue.push(line);
|
|
831
852
|
}
|
|
832
|
-
}
|
|
833
|
-
|
|
853
|
+
};
|
|
854
|
+
process.stdin.on('data', handler);
|
|
855
|
+
process.stdin.resume();
|
|
856
|
+
const nextLine = () => {
|
|
834
857
|
if (queue.length) return Promise.resolve(queue.shift());
|
|
835
858
|
return new Promise((resolve) => waiters.push(resolve));
|
|
836
859
|
};
|
|
860
|
+
// close:移除监听并暂停 stdin——否则 TTY(永不 EOF)会保持事件循环,命令结束后进程不退出
|
|
861
|
+
nextLine.close = () => {
|
|
862
|
+
if (closed) return;
|
|
863
|
+
closed = true;
|
|
864
|
+
process.stdin.removeListener('data', handler);
|
|
865
|
+
process.stdin.pause();
|
|
866
|
+
};
|
|
867
|
+
return nextLine;
|
|
837
868
|
}
|
|
838
869
|
|
|
839
870
|
async function promptInteractive(fields) {
|
|
@@ -851,6 +882,7 @@ async function promptInteractive(fields) {
|
|
|
851
882
|
}
|
|
852
883
|
answers[f.key] = val;
|
|
853
884
|
}
|
|
885
|
+
nextLine.close();
|
|
854
886
|
return answers;
|
|
855
887
|
}
|
|
856
888
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent2agent-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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": [
|