@bolloon/bolloon-agent 0.3.27 → 0.3.28

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.
@@ -4,15 +4,16 @@
4
4
  * Bridges MCP (Model Context Protocol) servers with Bolloon's tool system.
5
5
  * Based on the pi-mcp-adapter philosophy: on-demand tool loading with minimal token overhead.
6
6
  *
7
- * Key differences from Claude Code MCP:
8
- * - White-box design: every schema, parameter, and return value is visible
9
- * - Minimal primitives: only read, write, edit, bash under the hood
10
- * - No schema validation black boxes - interfaces are simple by design
7
+ * 2026-08-03 (验证修复): sendMcpRequest simulated 占位 → 真实 stdio JSON-RPC 通信.
8
+ * - 协议: initialize notifications/initialized tools/list tools/call
9
+ * - 请求/响应按 id 配对, 30s 超时, server 崩溃时 pending 全部 reject
10
+ * - discoverMcpServers 修复重复读 mcpServers (同一个键被读两遍)
11
11
  */
12
12
  import * as fs from 'fs/promises';
13
13
  import * as path from 'path';
14
14
  import { spawn } from 'child_process';
15
15
  import { EventEmitter } from 'events';
16
+ import * as readline from 'readline';
16
17
  // MCP adapter state
17
18
  let tools = new Map();
18
19
  let servers = new Map();
@@ -22,6 +23,8 @@ let toolCallLog = [];
22
23
  class McpEventEmitter extends EventEmitter {
23
24
  }
24
25
  const mcpEvents = new McpEventEmitter();
26
+ const MCP_REQUEST_TIMEOUT_MS = 30_000;
27
+ let mcpRequestSeq = 1;
25
28
  /**
26
29
  * Discover MCP servers from standard config locations
27
30
  */
@@ -37,24 +40,17 @@ export async function discoverMcpServers() {
37
40
  try {
38
41
  const content = await fs.readFile(loc, 'utf-8');
39
42
  const mcpJson = JSON.parse(content);
40
- if (mcpJson.mcpServers) {
41
- for (const [name, config] of Object.entries(mcpJson.mcpServers)) {
43
+ // 2026-08-03 fix: mcpServers 只读一次 (之前 if + if['mcpServers'] 重复读同一键)
44
+ const serversConfig = (mcpJson.mcpServers ?? mcpJson['mcpServers']);
45
+ if (serversConfig && typeof serversConfig === 'object') {
46
+ for (const [name, config] of Object.entries(serversConfig)) {
42
47
  const serverConfig = config;
48
+ if (!serverConfig || typeof serverConfig.command !== 'string')
49
+ continue;
43
50
  configs.push({
44
51
  name,
45
52
  command: serverConfig.command,
46
- args: serverConfig.args,
47
- env: serverConfig.env,
48
- });
49
- }
50
- }
51
- if (mcpJson['mcpServers']) {
52
- for (const [name, config] of Object.entries(mcpJson['mcpServers'])) {
53
- const serverConfig = config;
54
- configs.push({
55
- name,
56
- command: serverConfig.command,
57
- args: serverConfig.args,
53
+ args: Array.isArray(serverConfig.args) ? serverConfig.args : undefined,
58
54
  env: serverConfig.env,
59
55
  });
60
56
  }
@@ -64,7 +60,15 @@ export async function discoverMcpServers() {
64
60
  // File doesn't exist, skip
65
61
  }
66
62
  }
67
- return configs;
63
+ // 去重 (同 name 同 command)
64
+ const seen = new Set();
65
+ return configs.filter((c) => {
66
+ const key = `${c.name}::${c.command}`;
67
+ if (seen.has(key))
68
+ return false;
69
+ seen.add(key);
70
+ return true;
71
+ });
68
72
  }
69
73
  /**
70
74
  * Initialize the MCP adapter
@@ -76,15 +80,60 @@ export async function initializeMcpAdapter() {
76
80
  console.log(`[McpAdapter] Discovered ${discoveredServers.length} MCP servers`);
77
81
  for (const server of discoveredServers) {
78
82
  registerServer(server);
83
+ // 2026-08-03: 启动后立即握手 + 发现工具 (真实 stdio 协议)
84
+ await connectAndDiscover(server.name).catch((e) => {
85
+ console.warn(`[McpAdapter] connect ${server.name} 失败:`, e?.message?.slice(0, 120));
86
+ });
79
87
  }
80
88
  initialized = true;
81
89
  mcpEvents.emit('initialized');
82
90
  }
91
+ /**
92
+ * 连接 MCP server + 握手 + 发现并注册工具 (2026-08-03)
93
+ * 协议序: startServer → initialize → notifications/initialized → tools/list
94
+ */
95
+ export async function connectAndDiscover(serverName) {
96
+ const server = servers.get(serverName);
97
+ if (!server)
98
+ return [];
99
+ if (!server.running || !server.process) {
100
+ const started = await startServer(serverName);
101
+ if (!started)
102
+ return [];
103
+ // 等 server 就绪
104
+ await new Promise((r) => setTimeout(r, 400));
105
+ }
106
+ await sendMcpRequest(serverName, 'initialize', {
107
+ protocolVersion: '2024-11-05',
108
+ capabilities: {},
109
+ clientInfo: { name: 'bolloon', version: '0.3.27' },
110
+ });
111
+ await sendMcpRequest(serverName, 'notifications/initialized');
112
+ const result = await sendMcpRequest(serverName, 'tools/list');
113
+ const toolsList = Array.isArray(result?.tools) ? result.tools : [];
114
+ const discovered = [];
115
+ for (const t of toolsList) {
116
+ const tool = {
117
+ name: String(t.name || ''),
118
+ description: String(t.description || ''),
119
+ inputSchema: t.inputSchema || {},
120
+ serverName,
121
+ };
122
+ if (!tool.name)
123
+ continue;
124
+ discovered.push(tool);
125
+ registerTool(tool);
126
+ }
127
+ console.log(`[McpAdapter] ${serverName}: 发现 ${discovered.length} 个工具 (${discovered.map((t) => t.name).join(', ')})`);
128
+ return discovered;
129
+ }
83
130
  /**
84
131
  * Register an MCP server configuration
85
132
  */
86
133
  export function registerServer(config) {
87
- servers.set(config.name, { config, process: null, running: false });
134
+ if (servers.has(config.name))
135
+ return;
136
+ servers.set(config.name, { config, process: null, running: false, pending: new Map() });
88
137
  console.log(`[McpAdapter] Registered server: ${config.name}`);
89
138
  }
90
139
  /**
@@ -156,10 +205,12 @@ export async function executeTool(name, args) {
156
205
  toolCallLog = toolCallLog.slice(-50);
157
206
  }
158
207
  if (result && typeof result === 'object' && 'content' in result) {
159
- return {
160
- success: true,
161
- content: result.content,
162
- };
208
+ // 2026-08-03: 提取 content 数组里的文本 (agent 直接可用)
209
+ const content = result.content;
210
+ const text = Array.isArray(content)
211
+ ? content.map((c) => c?.text ?? '').filter(Boolean).join('\n')
212
+ : JSON.stringify(result);
213
+ return { success: true, content: [{ type: 'text', text }] };
163
214
  }
164
215
  return { success: true, content: [{ type: 'text', text: JSON.stringify(result) }] };
165
216
  }
@@ -170,25 +221,78 @@ export async function executeTool(name, args) {
170
221
  }
171
222
  }
172
223
  /**
173
- * Send MCP request to a server (simplified - uses stdin/stdout JSON-RPC)
224
+ * Send MCP request to a server via real stdio JSON-RPC (2026-08-03).
225
+ * - 写 JSON-RPC 行到 server stdin, 从 stdout 按 id 配对响应
226
+ * - 30s 超时; server 进程退出时挂起请求全部 reject
174
227
  */
175
228
  async function sendMcpRequest(serverName, method, params) {
176
229
  const server = servers.get(serverName);
177
230
  if (!server) {
178
231
  throw new Error(`Server not registered: ${serverName}`);
179
232
  }
180
- // For now, return a placeholder - real implementation would use MCP protocol
181
- // The actual protocol typically uses stdio or HTTP+streamableHTTP
182
- console.log(`[McpAdapter] Would send ${method} to ${serverName}:`, params);
183
- // Simulate successful response for development
184
- return {
185
- content: [
186
- {
187
- type: 'text',
188
- text: `[Simulated] Tool ${method} executed with params: ${JSON.stringify(params)}`,
189
- },
190
- ],
191
- };
233
+ // 确保 server 进程在跑
234
+ if (!server.running || !server.process || !server.process.stdin?.writable) {
235
+ const started = await startServer(serverName);
236
+ if (!started)
237
+ throw new Error(`无法启动 MCP server: ${serverName}`);
238
+ // 等 500ms 让 server 就绪
239
+ await new Promise((r) => setTimeout(r, 500));
240
+ }
241
+ const child = server.process;
242
+ if (!child.stdin?.writable)
243
+ throw new Error(`MCP server stdin 不可写: ${serverName}`);
244
+ const id = mcpRequestSeq++;
245
+ const isNotification = method.startsWith('notifications/');
246
+ // 通知 (notifications/*) 是 fire-and-forget: 无 id, server 不响应
247
+ const line = JSON.stringify(isNotification
248
+ ? { jsonrpc: '2.0', method, params: params ?? {} }
249
+ : { jsonrpc: '2.0', id, method, params: params ?? {} });
250
+ if (isNotification) {
251
+ child.stdin.write(line + '\n');
252
+ return undefined;
253
+ }
254
+ return new Promise((resolve, reject) => {
255
+ const timer = setTimeout(() => {
256
+ server.pending.delete(id);
257
+ reject(new Error(`MCP request timeout (${MCP_REQUEST_TIMEOUT_MS}ms): ${method}`));
258
+ }, MCP_REQUEST_TIMEOUT_MS);
259
+ server.pending.set(id, { resolve, reject, timer });
260
+ child.stdin.write(line + '\n');
261
+ });
262
+ }
263
+ /** 挂上 stdout 行读取器 (按 id 分发响应) */
264
+ function attachStdoutReader(serverName, child) {
265
+ const server = servers.get(serverName);
266
+ if (!server)
267
+ return;
268
+ const rl = readline.createInterface({ input: child.stdout });
269
+ rl.on('line', (line) => {
270
+ const trimmed = line.trim();
271
+ if (!trimmed)
272
+ return;
273
+ let msg;
274
+ try {
275
+ msg = JSON.parse(trimmed);
276
+ }
277
+ catch {
278
+ console.log(`[McpAdapter][${serverName}] non-JSON stdout:`, trimmed.slice(0, 200));
279
+ return;
280
+ }
281
+ // 服务端主动推送 (无 id) → 忽略
282
+ if (msg.id === undefined || msg.id === null)
283
+ return;
284
+ const entry = server.pending.get(msg.id);
285
+ if (!entry)
286
+ return;
287
+ clearTimeout(entry.timer);
288
+ server.pending.delete(msg.id);
289
+ if (msg.error) {
290
+ entry.reject(new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
291
+ }
292
+ else {
293
+ entry.resolve(msg.result);
294
+ }
295
+ });
192
296
  }
193
297
  /**
194
298
  * Start an MCP server process
@@ -215,16 +319,20 @@ export async function startServer(serverName) {
215
319
  });
216
320
  child.on('error', (err) => {
217
321
  console.error(`[McpAdapter][${serverName}] error:`, err);
322
+ rejectAllPending(server, `MCP server process error: ${err.message}`);
218
323
  server.running = false;
219
324
  server.process = null;
220
325
  });
221
326
  child.on('exit', (code) => {
222
327
  console.log(`[McpAdapter][${serverName}] exited with code:`, code);
328
+ rejectAllPending(server, `MCP server exited with code ${code}`);
223
329
  server.running = false;
224
330
  server.process = null;
225
331
  });
226
332
  server.process = child;
227
333
  server.running = true;
334
+ // 2026-08-03: 挂 stdout 行读取器, 按 id 分发 JSON-RPC 响应
335
+ attachStdoutReader(serverName, child);
228
336
  console.log(`[McpAdapter] Started server: ${serverName}`);
229
337
  return true;
230
338
  }
@@ -233,6 +341,14 @@ export async function startServer(serverName) {
233
341
  return false;
234
342
  }
235
343
  }
344
+ /** server 退出时把挂起请求全部 reject, 避免调用方永远等待 */
345
+ function rejectAllPending(server, reason) {
346
+ for (const [, entry] of server.pending) {
347
+ clearTimeout(entry.timer);
348
+ entry.reject(new Error(reason));
349
+ }
350
+ server.pending.clear();
351
+ }
236
352
  /**
237
353
  * Stop an MCP server
238
354
  */
@@ -46,6 +46,14 @@ const TOOL_WHITELIST = new Set([
46
46
  'create_skill', 'update_skill', 'list_skill_candidates', 'promote_skill',
47
47
  // 2026-08-02: plan/todo/review 工具 (plan-store.ts)
48
48
  'create_plan', 'update_plan', 'review_plan', 'list_plans',
49
+ // 2026-08-03: 决策协议工具 (decision-store.ts, Context OS §7 九要素)
50
+ 'create_decision', 'decide_decision', 'rollback_decision', 'list_decisions',
51
+ // 2026-08-03: Context OS 资产层工具 (context-os.ts, 12+3 层文件夹体系)
52
+ 'list_context_layers', 'write_context_asset', 'read_context_assets',
53
+ // 2026-08-03: MCP 工具 (pi-ecosystem-mcp, 真实 stdio JSON-RPC)
54
+ 'mcp_list_tools', 'mcp_tool',
55
+ // 2026-08-03: DID 发布到 IPFS+IPNS (自动安装 Kubo)
56
+ 'publish_did',
49
57
  // 2026-08-02: 远端 channel 工具 (本地智能体 @ 远程交流)
50
58
  'list_remote_channels', 'send_to_remote_channel',
51
59
  ]);
@@ -1614,6 +1614,47 @@ export async function createWebServer(port = 3000, options = {}) {
1614
1614
  catch (err) {
1615
1615
  console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
1616
1616
  }
1617
+ // 2026-08-03 (Context OS P5): 初始化资产层 12+3 目录 (幂等, 每层 README 声明职责边界)
1618
+ try {
1619
+ const { ensureContextOsDirs } = await import('../bootstrap/context-os.js');
1620
+ await ensureContextOsDirs();
1621
+ console.log('[createWebServer] Context OS 资产层就绪 (~/.bolloon/context-os/, 12+3 层)');
1622
+ }
1623
+ catch (err) {
1624
+ console.warn('[createWebServer] Context OS 资产层初始化失败 (非致命):', err);
1625
+ }
1626
+ // 2026-08-03: 后台自动安装/启动本地 Kubo (IPFS+IPNS 发布用, 全自动零手动).
1627
+ // fire-and-forget, 不阻塞 server 启动; 首次下载 ~100MB 需 1-2 分钟.
1628
+ // 装好后 publish_did / DID 发布自动走 IPFS+IPNS; 没装好则降级本地模式.
1629
+ (async () => {
1630
+ try {
1631
+ const sdk = await import('@diap/sdk');
1632
+ const checkKuboSetup = sdk.checkKuboSetup;
1633
+ if (typeof checkKuboSetup !== 'function')
1634
+ return;
1635
+ const setup = await checkKuboSetup(true, true);
1636
+ if (setup?.ready && setup?.daemonRunning) {
1637
+ console.log('[ipfs] 本地 Kubo 就绪 (自动安装/启动) → DID 发布支持 IPFS+IPNS');
1638
+ }
1639
+ else {
1640
+ console.log('[ipfs] Kubo 不可用, DID 发布降级本地模式 (可用 publish_did 工具重试)');
1641
+ }
1642
+ }
1643
+ catch (e) {
1644
+ console.warn('[ipfs] Kubo 自动安装失败 (非致命):', e?.message?.slice(0, 120));
1645
+ }
1646
+ })();
1647
+ // 2026-08-03: 初始化 MCP 适配器 (读 ~/.mcp.json, 自动握手发现工具).
1648
+ // 后台异步: 不阻塞启动, 失败静默 (agent 调 mcp_list_tools 时再触发).
1649
+ (async () => {
1650
+ try {
1651
+ const { initializeMcpAdapter } = await import('../pi-ecosystem-mcp/index.js');
1652
+ await initializeMcpAdapter();
1653
+ }
1654
+ catch (e) {
1655
+ console.warn('[mcp] 初始化失败 (非致命):', e?.message?.slice(0, 120));
1656
+ }
1657
+ })();
1617
1658
  // 重置旧的 agent session,确保使用新的 LLM 配置
1618
1659
  const { resetAgentSession } = await import('../agents/pi-sdk.js');
1619
1660
  resetAgentSession();
@@ -3303,7 +3344,7 @@ ${goalDesc}
3303
3344
  }
3304
3345
  if (summariesToRead.length > 0) {
3305
3346
  const memBlock = summariesToRead.map(s => s.trim().slice(-1500)).join('\n\n---\n\n');
3306
- contextHint += `[系统上下文] 本 channel 的历史记忆 (来自 memory-compressor 摘要, 帮助你延续之前的对话, 引用而非复述):\n${memBlock.slice(-2500)}\n\n`;
3347
+ contextHint += `[系统上下文] 动态状态层 · chat-worksite (上次做到哪 — 本 channel 历史记忆, 来自 memory-compressor 摘要, 引用而非复述):\n${memBlock.slice(-2500)}\n\n`;
3307
3348
  }
3308
3349
  }
3309
3350
  }
@@ -3318,12 +3359,25 @@ ${goalDesc}
3318
3359
  const plans = await listActivePlans();
3319
3360
  if (plans.length > 0) {
3320
3361
  const plansBlock = plans.slice(0, 3).map(p => planToContext(p)).join('\n\n');
3321
- contextHint += `[系统上下文] 进行中的计划 (来自 plan-store, 执行中每完成一步调 update_plan 勾选):\n${plansBlock}\n\n`;
3362
+ contextHint += `[系统上下文] 动态状态层 · focus (此刻优先级与时间边界 — 进行中的计划, 来自 plan-store, 执行中每完成一步调 update_plan 勾选):\n${plansBlock}\n\n`;
3322
3363
  }
3323
3364
  }
3324
3365
  catch (planErr) {
3325
3366
  // 静默失败
3326
3367
  }
3368
+ // 2026-08-03 (Context OS P5): 资产层目录注入 — LLM 知道有 12+3 层资产,
3369
+ // 需要细节时用 read_context_assets 按层路由读取, 不全仓扫描 (Context OS §4).
3370
+ try {
3371
+ const { readContextAssets, formatLayerListing } = await import('../bootstrap/context-os.js');
3372
+ const listings = await readContextAssets();
3373
+ const total = listings.reduce((s, l) => s + l.fileCount, 0);
3374
+ if (total > 0) {
3375
+ contextHint += `[系统上下文] 资产层 (Context OS 12+3 层, ${total} 篇资产 — 任务需要细节时用 read_context_assets 按层读取, 不要假装知道没读过的内容):\n${formatLayerListing(listings)}`;
3376
+ }
3377
+ }
3378
+ catch (ctxErr) {
3379
+ // 静默失败
3380
+ }
3327
3381
  const linkedIds = channelForJudgment?.linkedDocumentIds;
3328
3382
  if (Array.isArray(linkedIds) && linkedIds.length > 0) {
3329
3383
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.27",
3
+ "version": "0.3.28",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",