ppxans-harness 2.4.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +218 -265
  3. package/bin/ppx-channels.js +2 -2
  4. package/bin/ppx-serve.js +5 -5
  5. package/bin/ppx-setup.js +124 -0
  6. package/bin/ppx-web.js +140 -0
  7. package/bin/ppx.js +2 -2
  8. package/config/identity.md +6 -6
  9. package/config/ishiki.md +16 -16
  10. package/config/ppx.json +15 -151
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +17 -10
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/brainstorm/SKILL.md +24 -0
  16. package/skills/cupid-lover-comms/SKILL.md +37 -0
  17. package/skills/debug/SKILL.md +26 -0
  18. package/skills/plan/SKILL.md +25 -0
  19. package/skills/ponytail/SKILL.md +25 -0
  20. package/skills/ppx-memory/SKILL.md +91 -0
  21. package/skills/ppx-memory/scripts/cli.js +192 -0
  22. package/skills/ppx-memory/scripts/experience.js +133 -0
  23. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  24. package/skills/ppx-memory/scripts/l0.js +52 -0
  25. package/skills/ppx-memory/scripts/l2.js +146 -0
  26. package/skills/ppx-memory/scripts/l3.js +112 -0
  27. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  28. package/skills/ppx-memory/scripts/pii.js +42 -0
  29. package/skills/ppx-memory/scripts/schema.js +80 -0
  30. package/skills/ppx-memory/scripts/session.js +398 -0
  31. package/skills/ppx-memory/scripts/similarity.js +43 -0
  32. package/skills/ppx-memory/scripts/store.js +116 -0
  33. package/skills/ppx-memory/scripts/wal.js +38 -0
  34. package/skills/ppx-selfheal/SKILL.md +24 -0
  35. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  36. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  37. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  38. package/skills/ppx-selfheal/scripts/store.js +116 -0
  39. package/skills/prompt-depth-kit/SKILL.md +28 -0
  40. package/skills/session-naming/SKILL.md +36 -0
  41. package/skills/verify/SKILL.md +25 -0
  42. package/src/agent/index.js +1340 -717
  43. package/src/agent/prompts.js +47 -3
  44. package/src/aml-server.js +197 -151
  45. package/src/ans/eviction.js +123 -143
  46. package/src/ans/guard.js +159 -120
  47. package/src/ans/lifecycle.js +96 -93
  48. package/src/ans/proactive.js +112 -129
  49. package/src/ans/reward.js +95 -111
  50. package/src/ans/values.js +15 -15
  51. package/src/audit/audit-chain.js +43 -7
  52. package/src/audit/verifier.js +157 -120
  53. package/src/bus/circuit-breaker.js +9 -1
  54. package/src/bus/runtime-bus.js +107 -93
  55. package/src/channels/base.js +57 -34
  56. package/src/channels/feishu.js +118 -126
  57. package/src/channels/http.js +1090 -592
  58. package/src/channels/index.js +111 -110
  59. package/src/channels/log.js +29 -29
  60. package/src/channels/wechat-crypto.js +73 -74
  61. package/src/channels/wechat.js +191 -197
  62. package/src/channels/workspace.js +94 -0
  63. package/src/channels-cli.js +126 -124
  64. package/src/cli.js +129 -120
  65. package/src/commands/index.js +142 -0
  66. package/src/config/channels.js +137 -170
  67. package/src/config/index.js +277 -224
  68. package/src/config/placeholder.js +31 -0
  69. package/src/config/providers.js +148 -188
  70. package/src/config/settings.js +156 -182
  71. package/src/core/policy.js +69 -18
  72. package/src/core/trace.js +8 -6
  73. package/src/edit/editblock.js +266 -0
  74. package/src/edit/snapshot.js +67 -0
  75. package/src/evidence/index.js +153 -0
  76. package/src/evolve/playbook.js +9 -10
  77. package/src/hooks/index.js +113 -0
  78. package/src/llm/client.js +188 -446
  79. package/src/llm/dsml.js +74 -74
  80. package/src/llm/embedder.js +41 -35
  81. package/src/llm/fence.js +52 -105
  82. package/src/llm/index.js +4 -4
  83. package/src/llm/local-embedder.js +94 -0
  84. package/src/llm/presets.js +113 -0
  85. package/src/llm/pricing.js +93 -0
  86. package/src/llm/retry.js +73 -73
  87. package/src/llm/router.js +92 -97
  88. package/src/mcp/admin.js +326 -0
  89. package/src/mcp/client.js +487 -375
  90. package/src/mcp/http.js +203 -0
  91. package/src/mcp/index.js +116 -116
  92. package/src/mcp/server.js +392 -0
  93. package/src/mcp/tasks.js +133 -0
  94. package/src/memory/asset-hub.js +6 -11
  95. package/src/memory/canvas.js +2 -5
  96. package/src/memory/compaction.js +28 -28
  97. package/src/memory/experience.js +133 -122
  98. package/src/memory/fact-store.js +914 -698
  99. package/src/memory/failure-episode.js +20 -11
  100. package/src/memory/fork.js +17 -8
  101. package/src/memory/index.js +8 -6
  102. package/src/memory/l0.js +53 -52
  103. package/src/memory/l2.js +145 -130
  104. package/src/memory/l3.js +111 -111
  105. package/src/memory/legion-board.js +71 -0
  106. package/src/memory/memory-ticker.js +239 -240
  107. package/src/memory/session.js +398 -397
  108. package/src/memory/sqlite-store.js +581 -0
  109. package/src/mode/blackboard.js +49 -49
  110. package/src/mode/graph.js +42 -41
  111. package/src/mode/index.js +64 -64
  112. package/src/mode/legion.js +54 -51
  113. package/src/mode/plan-exec.js +50 -50
  114. package/src/mode/router.js +28 -40
  115. package/src/orchestrator/agent-worker.js +69 -69
  116. package/src/orchestrator/dag.js +90 -83
  117. package/src/orchestrator/experts.js +76 -0
  118. package/src/orchestrator/index.js +1 -1
  119. package/src/orchestrator/legion.js +179 -187
  120. package/src/orchestrator/supervisor.js +6 -8
  121. package/src/permissions/index.js +378 -0
  122. package/src/persona/index.js +28 -29
  123. package/src/plugin/builtin.js +313 -212
  124. package/src/plugin/context.js +80 -79
  125. package/src/plugin/index.js +62 -62
  126. package/src/plugin/v3.js +73 -0
  127. package/src/protocol/index.js +148 -0
  128. package/src/repomap/index.js +309 -0
  129. package/src/review/index.js +393 -0
  130. package/src/seam/registry.js +3 -0
  131. package/src/seam/shell.js +55 -55
  132. package/src/security/injection.js +79 -0
  133. package/src/selfheal/evolve.js +67 -67
  134. package/src/selfheal/healer.js +184 -167
  135. package/src/selfheal/run.js +9 -9
  136. package/src/server.js +63 -60
  137. package/src/services/diagnose.js +180 -0
  138. package/src/services/learning-service.js +9 -0
  139. package/src/services/memory-health.js +34 -6
  140. package/src/services/memory-service.js +42 -9
  141. package/src/services/triage.js +138 -0
  142. package/src/session/parts.js +76 -0
  143. package/src/session/projection.js +73 -0
  144. package/src/session/rollout.js +54 -0
  145. package/src/session/turn.js +137 -0
  146. package/src/skills/lint.js +72 -0
  147. package/src/skills/loader.js +231 -150
  148. package/src/skills/search.js +58 -0
  149. package/src/skills/verify.js +95 -100
  150. package/src/tools/advanced.js +388 -352
  151. package/src/tools/builtin.js +384 -297
  152. package/src/tools/catalog.js +283 -159
  153. package/src/tools/command-guard.js +112 -112
  154. package/src/tools/custom.js +47 -47
  155. package/src/tools/delegate.js +383 -297
  156. package/src/tools/document.js +254 -253
  157. package/src/tools/git.js +151 -0
  158. package/src/tools/governance.js +47 -20
  159. package/src/tools/index.js +16 -11
  160. package/src/tools/methods.js +178 -178
  161. package/src/tools/ocr.js +59 -59
  162. package/src/tools/sandbox-worker.js +40 -0
  163. package/src/tools/sandbox.js +92 -0
  164. package/src/tools/seam.js +162 -125
  165. package/src/tools/selfmod.js +196 -176
  166. package/src/tools/v3.js +225 -0
  167. package/src/tools/vad.js +176 -0
  168. package/src/tools/voice.js +238 -0
  169. package/src/utils/async.js +14 -0
  170. package/src/utils/config-file.js +53 -0
  171. package/src/utils/crashguard.js +88 -0
  172. package/src/utils/http.js +53 -0
  173. package/src/utils/id.js +8 -0
  174. package/src/utils/json-state.js +33 -0
  175. package/src/utils/logger.js +17 -17
  176. package/src/utils/ndjson.js +25 -0
  177. package/src/utils/pii.js +42 -42
  178. package/src/utils/rate-limit.js +50 -0
  179. package/src/utils/schema.js +80 -0
  180. package/src/utils/similarity.js +43 -0
  181. package/src/utils/store.js +170 -108
  182. package/src/utils/text.js +15 -15
  183. package/src/utils/trace.js +153 -153
  184. package/src/utils/wal.js +39 -0
  185. package/src/utils/winutf8.js +16 -15
  186. package/src/wiki/index.js +170 -0
@@ -0,0 +1,203 @@
1
+ // src/mcp/http.js - MCP Streamable HTTP 传输层 (零依赖)
2
+ // 2026-07-28 语义:
3
+ // - 单端点只收 POST; 每个 JSON-RPC 请求/通知一次 POST
4
+ // - 必带头: MCP-Protocol-Version (现代 era, 与 body _meta 校验一致)
5
+ // - 响应: application/json 单对象 或 text/event-stream (SSE 流, 请求级)
6
+ // - 通知 (无 id) -> 202 Accepted 无 body
7
+ // - 取消 = 关闭 SSE 流 (无需 notifications/cancelled)
8
+ // - Origin 校验防 DNS rebinding; 本地默认只绑 127.0.0.1
9
+ import { McpServer, McpError, MCP_ERROR, MODERN_PROTOCOL_VERSION, SERVER_INFO_META_KEY } from "./server.js";
10
+ import { readBody, sendJson, SSE_HEADERS } from "../utils/http.js";
11
+ import { debug } from "../utils/logger.js";
12
+
13
+ const MAX_BODY = 1024 * 1024; // 1MB
14
+
15
+ // ---- SSE 序列化 ----
16
+ function sseMessage(obj) {
17
+ return `event: message\ndata: ${JSON.stringify(obj)}\n\n`;
18
+ }
19
+
20
+ // (readBody 收敛到 utils/http.js: 超限返回 null, 由下方改为抛 McpError 保持原语义)
21
+
22
+ // ---- 传输错误 -> HTTP 状态码映射 (2026-07-28) ----
23
+ function statusFor(err) {
24
+ switch (err.code) {
25
+ case MCP_ERROR.UNSUPPORTED_PROTOCOL_VERSION: return 400; // Bad Request
26
+ case MCP_ERROR.HEADER_MISMATCH: return 400;
27
+ case MCP_ERROR.MISSING_REQUIRED_CLIENT_CAPABILITY: return 400;
28
+ case MCP_ERROR.METHOD_NOT_FOUND: return 404; // 未知方法
29
+ case MCP_ERROR.INVALID_PARAMS: return 400;
30
+ case MCP_ERROR.INVALID_REQUEST: return 400;
31
+ case MCP_ERROR.PARSE_ERROR: return 400;
32
+ default: return 500;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * 创建 MCP Streamable HTTP 请求处理器。
38
+ * @param {McpServer} server
39
+ * @param {object} [opts]
40
+ * @param {(req:any)=>boolean} [opts.authenticated] 鉴权钩子: 返回 false 则 401 (可选, 未提供则放行)
41
+ * @param {(req:any, res:any)=>boolean} [opts.rateLimit] 限流钩子: 返回 false 则已响应 429 (可选)
42
+ * @param {string[]} [opts.allowedOrigins] CORS/Origin 白名单; 空 = 仅无 Origin 或本地回环
43
+ * @param {boolean} [opts.skipOriginCheck] 宿主已统一处理 CORS/Origin 时跳过 (避免双重标准)
44
+ * @returns {(req:any,res:any)=>Promise<void>}
45
+ */
46
+ export function createMcpHttpHandler(server, opts = {}) {
47
+ return async (req, res) => {
48
+ try {
49
+ // 1. 方法限制: 只收 POST
50
+ if (req.method !== "POST") {
51
+ sendJson(res, 405, { error: "method not allowed" }, { headers: { "Allow": "POST" } });
52
+ return;
53
+ }
54
+
55
+ // 2. Origin 校验 (防 DNS rebinding; 宿主已统一处理时可跳过)
56
+ const origin = req.headers.origin;
57
+ if (origin && !opts.skipOriginCheck) {
58
+ const allowed = opts.allowedOrigins && opts.allowedOrigins.length
59
+ ? opts.allowedOrigins.includes(origin)
60
+ : /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin);
61
+ if (!allowed) {
62
+ sendJson(res, 403, { jsonrpc: "2.0", error: { code: -32600, message: "origin not allowed" } });
63
+ return;
64
+ }
65
+ }
66
+
67
+ // 3. 可选鉴权 / 限流 (由宿主通道注入)
68
+ if (opts.authenticated && !opts.authenticated(req, res)) return;
69
+ if (opts.rateLimit && !opts.rateLimit(req, res)) return;
70
+
71
+ // 4. 读 body + 解析
72
+ let msg;
73
+ try {
74
+ const body = await readBody(req, { maxBytes: MAX_BODY });
75
+ if (body === null) throw new McpError(MCP_ERROR.INVALID_REQUEST, "request too large");
76
+ msg = body ? JSON.parse(body) : {};
77
+ } catch (e) {
78
+ sendJson(res, 400, {
79
+ jsonrpc: "2.0",
80
+ id: null,
81
+ error: { code: MCP_ERROR.PARSE_ERROR, message: `Parse error: ${e.message}` },
82
+ });
83
+ return;
84
+ }
85
+
86
+ // 5. 版本头校验 (现代 era): MCP-Protocol-Version 必须与 body _meta 一致
87
+ const headerVersion = req.headers["mcp-protocol-version"];
88
+ const bodyVersion = msg && msg.params && msg.params._meta && msg.params._meta["io.modelcontextprotocol/protocolVersion"];
89
+ if (bodyVersion && headerVersion && headerVersion !== bodyVersion) {
90
+ sendJson(res, 400, {
91
+ jsonrpc: "2.0",
92
+ id: msg.id ?? null,
93
+ error: { code: MCP_ERROR.HEADER_MISMATCH, message: "Header MCP-Protocol-Version does not match body _meta", data: { header: headerVersion, body: bodyVersion } },
94
+ });
95
+ return;
96
+ }
97
+ // 现代请求 (有 bodyVersion) 但缺 header: 允许 (兼容未带头的老客户端), 记录即可
98
+ if (bodyVersion && !headerVersion) {
99
+ // 2026-07-28 规范要求必带; 宽松兼容: 直接按 body 版本处理
100
+ }
101
+
102
+ // 6. 通知 (无 id) -> 202
103
+ if (msg && (msg.id === undefined || msg.id === null)) {
104
+ // 通知: 只处理 notifications/* 或 ping 通知语义; 直接受理
105
+ await server.handle(msg, {}).catch((e) => {
106
+ // 通知失败仅记日志, 不影响 202 语义
107
+ if (opts.onError) opts.onError(e);
108
+ });
109
+ sendJson(res, 202);
110
+ return;
111
+ }
112
+
113
+ // 7. 请求: 是否 SSE (客户端 Accept 两者之一; 流式工具请求开 SSE 流)
114
+ const accept = String(req.headers.accept || "");
115
+ const isStreamTool = msg && msg.method === "tools/call" && /stream/i.test(String(msg.params && msg.params.name || ""));
116
+ const wantsSSE = accept.includes("text/event-stream") && isStreamTool;
117
+
118
+ if (wantsSSE) {
119
+ // SSE 响应流: 进度通知 + 最终响应, 关闭流 = 取消
120
+ res.writeHead(200, SSE_HEADERS);
121
+ res.write(": keep-alive\n\n");
122
+ const progressToken = msg.params && msg.params._meta && msg.params._meta.progressToken;
123
+ const streamCtx = {
124
+ stream: {
125
+ onDelta: (delta, extra = {}) => {
126
+ if (res.writableEnded) return;
127
+ const notif = { jsonrpc: "2.0", method: "notifications/progress", params: { progress: extra.progress, total: extra.total, message: String(delta).slice(0, 4000) } };
128
+ if (progressToken !== undefined) notif.params.progressToken = progressToken;
129
+ res.write(sseMessage(notif));
130
+ },
131
+ onMessage: (text) => {
132
+ if (res.writableEnded) return;
133
+ res.write(sseMessage({ jsonrpc: "2.0", method: "notifications/message", params: { level: "info", data: String(text).slice(0, 4000) } }));
134
+ },
135
+ // v2.6.0: 结构化工具事件透传 (web 前端渲染工具卡片): notifications/message 带 type=tool
136
+ onTool: (ev) => {
137
+ if (res.writableEnded || !ev) return;
138
+ res.write(sseMessage({ jsonrpc: "2.0", method: "notifications/message", params: { level: "info", data: { type: "tool", tool: ev.tool, status: ev.type || ev.status, ok: ev.ok, durationMs: ev.durationMs } } }));
139
+ },
140
+ // 推理轮次事件
141
+ onStep: (ev) => {
142
+ if (res.writableEnded || !ev) return;
143
+ res.write(sseMessage({ jsonrpc: "2.0", method: "notifications/message", params: { level: "info", data: { type: "step", round: ev.round, maxRounds: ev.maxRounds } } }));
144
+ },
145
+ },
146
+ };
147
+ try {
148
+ const { result } = await server.handle(msg, streamCtx);
149
+ // 流式工具: 结果即最终 delta (chat.stream 已通过 onDelta 推送过; 这里补发完整结果)
150
+ res.write(sseMessage({ jsonrpc: "2.0", id: msg.id, result: result ?? {} }));
151
+ } catch (e) {
152
+ const err = e instanceof McpError ? e : new McpError(MCP_ERROR.INTERNAL_ERROR, e.message || "internal error");
153
+ res.write(sseMessage({ jsonrpc: "2.0", id: msg.id, error: { code: err.code, message: err.message, ...(err.data ? { data: err.data } : {}) } }));
154
+ } finally {
155
+ try { res.end(); } catch (e) { debug(`[mcp/http] 已忽略异常: ${e && e.message ? e.message : e}`); }
156
+ }
157
+ return;
158
+ }
159
+
160
+ // 8. 普通 JSON 响应
161
+ try {
162
+ const { result } = await server.handle(msg, {});
163
+ sendJson(res, 200, { jsonrpc: "2.0", id: msg.id, result: result ?? {} });
164
+ } catch (e) {
165
+ const err = e instanceof McpError ? e : new McpError(MCP_ERROR.INTERNAL_ERROR, e.message || "internal error");
166
+ sendJson(res, statusFor(err), {
167
+ jsonrpc: "2.0",
168
+ id: msg.id ?? null,
169
+ error: { code: err.code, message: err.message, ...(err.data ? { data: err.data } : {}) },
170
+ });
171
+ }
172
+ } catch (e) {
173
+ // 外层兜底 (不应发生)
174
+ try {
175
+ sendJson(res, 500, { jsonrpc: "2.0", error: { code: -32603, message: e.message || "internal error" } });
176
+ } catch (e) { debug(`[mcp/http] 已忽略异常: ${e && e.message ? e.message : e}`); }
177
+ }
178
+ };
179
+ }
180
+
181
+ // ---- 便捷工厂: 从 agent 构建标准 McpServer + HTTP handler ----
182
+ export function createMcpEndpoint(agent, opts = {}) {
183
+ const server = new McpServer(agent, {
184
+ name: opts.name,
185
+ version: opts.version,
186
+ supportedVersions: opts.supportedVersions,
187
+ extraTools: opts.extraTools,
188
+ prompts: opts.prompts,
189
+ });
190
+ const handler = createMcpHttpHandler(server, {
191
+ authenticated: opts.authenticated,
192
+ rateLimit: opts.rateLimit,
193
+ allowedOrigins: opts.allowedOrigins,
194
+ // 2026-10-03 修复 (P2): 原工厂吞掉 skipOriginCheck —— channels/http.js 显式传
195
+ // skipOriginCheck: true (宿主 CORS 白名单已统一处理 Origin), 但工厂不转发 →
196
+ // handler 内部仍用默认回环正则二次校验, 契约失真 (宿主白名单形同虚设)。
197
+ skipOriginCheck: opts.skipOriginCheck,
198
+ onError: opts.onError,
199
+ });
200
+ return { server, handler };
201
+ }
202
+
203
+ export { McpServer, McpError, MCP_ERROR, MODERN_PROTOCOL_VERSION, SERVER_INFO_META_KEY };
package/src/mcp/index.js CHANGED
@@ -1,116 +1,116 @@
1
- // src/mcp/index.js - MCP 工具注册 (接入 MCP 工具生态)
2
- // 把 MCP 服务器的工具转换为皮皮虾 ToolCatalog 工具, 与内置工具同权。
3
- // servers: [
4
- // { command, args, env, prefix } // stdio (本机)
5
- // { url, headers, prefix, timeout } // HTTP Streamable (远程)
6
- // ]
7
- import { McpClient, extractToolResult, extractResourceText } from "./client.js";
8
- import { TOOL_ERROR_PREFIX } from "../tools/catalog.js";
9
- import { warn, info } from "../utils/logger.js";
10
-
11
- // v1.0.8: MCP 工具名清洗 (只留 \w.-, 截断) — 防恶意服务器注册非法工具名/注入
12
- export function sanitizeMcpName(name) {
13
- return String(name || "").replace(/[^\w.-]/g, "_").slice(0, 64);
14
- }
15
-
16
- // P1⑦ (2026-09-15): MCP 命名空间隔离 —— 防工具名碰撞 (Hermes 生产数据: 51 工具服务器上
17
- // 幻觉工具名命中真实工具; cron job 本想调 kanban_list 结果执行了 list_goals)。
18
- // 强制 serverName__toolName 前缀 + 精确匹配短路。
19
- export function serverLabel(s) {
20
- // 取服务器可读标识: 配置 name > 包名 (args 里 @scope/pkg 或 *.py) > command 首段 > url host
21
- if (s && s.name) return sanitizeMcpName(s.name);
22
- const fromArgs = (s && s.args || []).find((a) => /(?:^|\/)[@\w.-]+(?:@[\w.]+)?$/.test(a) && !a.startsWith("-"));
23
- if (fromArgs) return sanitizeMcpName(fromArgs.split("/").pop());
24
- if (s && s.command) return sanitizeMcpName(String(s.command).split(/[\\/\s]/).filter(Boolean).pop() || "mcp");
25
- if (s && s.url) {
26
- try {
27
- const host = new URL(s.url).hostname.split(".").filter(Boolean);
28
- // 取二级域: mcp.example.com → example; www.google.com → google
29
- return sanitizeMcpName(host.length >= 2 ? host[host.length - 2] : host[0] || "mcp");
30
- } catch {}
31
- }
32
- return "mcp";
33
- }
34
-
35
- export function namespacedMcpName(label, raw) {
36
- const clean = sanitizeMcpName(raw);
37
- return clean ? `${label}__${clean}` : label;
38
- }
39
-
40
- // v1.0.8: MCP 工具描述清洗 (单行化 + 截断) — 服务器描述直接进 LLM prompt, 防换行/长文本注入指令
41
- // P1⑦: 额外剔除危险 flag (--system/--dangerously 等命令行注入惯用手法)
42
- export function sanitizeMcpDescription(desc, name) {
43
- let s = String(desc || "").replace(/\s+/g, " ").trim();
44
- // 危险 flag 清洗: 描述里出现命令行注入惯用 flag 视为可疑, 剔除该片段
45
- s = s.replace(/--(?:system|dangerously[-\w]*|yes|force|no[-\w]+|y)\b/g, "[flag]");
46
- return s.slice(0, 200) || `MCP 工具: ${name}`;
47
- }
48
-
49
- // 连接所有 MCP 服务器, 列出工具并注册到 catalog。
50
- // 除 tools 外, 服务器声明 resources/prompts 能力时, 额外注册对应读写工具 (P2⑥)。
51
- // 返回 { count, clients, close } — close() 关闭全部连接 (调用方负责生命周期)
52
- export async function registerMcpTools(catalog, servers = []) {
53
- const clients = [];
54
- let count = 0;
55
- for (const s of servers) {
56
- if (!s) { warn("[mcp] 跳过无效服务器配置"); continue; }
57
- if (!s.command && !s.url) { warn("[mcp] 跳过无效服务器配置 (缺 command/url)"); continue; }
58
- const client = new McpClient(s);
59
- try {
60
- await client.connect();
61
- const tools = await client.listTools();
62
- // P1⑦: 命名空间强制前缀 (serverLabel) —— 默认开启, 可显式 prefix 覆盖/关闭
63
- const label = serverLabel(s);
64
- const ns = s.prefix != null ? s.prefix : `${label}__`;
65
- for (const t of tools) {
66
- if (!t || !t.name) continue;
67
- const name = ns + sanitizeMcpName(t.name);
68
- // P1⑦: 精确匹配短路 —— 注册时登记原始名 (供幻觉工具名防御: 无前缀名永不执行)
69
- catalog.register({
70
- name,
71
- description: sanitizeMcpDescription(t.description, t.name),
72
- parameters: t.inputSchema || { type: "object", properties: {} },
73
- category: "mcp",
74
- // isError 时加错误前缀, 让皮皮虾的自愈重试语义对 MCP 工具同样生效
75
- execute: async (args) => {
76
- const raw = await client.callToolRaw(t.name, args || {});
77
- const { text, isError } = extractToolResult(raw);
78
- return isError ? `${TOOL_ERROR_PREFIX} MCP 工具 ${t.name}: ${text}` : text;
79
- },
80
- });
81
- count += 1;
82
- }
83
- count += registerResourceTools(catalog, client, s, ns);
84
- clients.push(client);
85
- } catch (e) {
86
- warn(`[mcp] 服务器 ${s.command || s.url} 连接失败: ${e.message}`);
87
- client.close();
88
- }
89
- }
90
- if (count) info(`[mcp] 已注册 ${count} 个 MCP 工具`);
91
- return { count, clients, close: () => clients.forEach((c) => c.close()) };
92
- }
93
-
94
- // 服务器声明 resources 能力时, 注册 list_resources / read_resource 两个工具 (让 LLM 可读远程资源)
95
- function registerResourceTools(catalog, client, s, ns = "") {
96
- if (!client.capabilities?.resources) return 0;
97
- const label = s.command || s.url || "MCP";
98
- catalog.register({
99
- name: ns + "list_resources",
100
- description: `列出 MCP 服务器 ${label} 提供的资源 (URI 列表)`,
101
- parameters: { type: "object", properties: {}, required: [] },
102
- category: "mcp",
103
- execute: async () => {
104
- const res = await client.listResources();
105
- return res.map((r) => `${r.uri || "?"}${r.name ? ` — ${r.name}` : ""}`).join("\n") || "(无资源)";
106
- },
107
- });
108
- catalog.register({
109
- name: ns + "read_resource",
110
- description: `读取 MCP 服务器 ${label} 的资源内容 (按 uri)`,
111
- parameters: { type: "object", properties: { uri: { type: "string", description: "资源 URI" } }, required: ["uri"] },
112
- category: "mcp",
113
- execute: async (args) => extractResourceText(await client.readResource((args || {}).uri)),
114
- });
115
- return 2;
116
- }
1
+ // src/mcp/index.js - MCP 工具注册 (接入 MCP 工具生态)
2
+ // 把 MCP 服务器的工具转换为皮皮虾 ToolCatalog 工具, 与内置工具同权。
3
+ // servers: [
4
+ // { command, args, env, prefix } // stdio (本机)
5
+ // { url, headers, prefix, timeout } // HTTP Streamable (远程)
6
+ // ]
7
+ import { McpClient, extractToolResult, extractResourceText } from "./client.js";
8
+ import { TOOL_ERROR_PREFIX } from "../tools/catalog.js";
9
+ import { warn, info, debug } from "../utils/logger.js";
10
+
11
+ // v1.0.8: MCP 工具名清洗 (只留 \w.-, 截断) — 防恶意服务器注册非法工具名/注入
12
+ export function sanitizeMcpName(name) {
13
+ return String(name || "").replace(/[^\w.-]/g, "_").slice(0, 64);
14
+ }
15
+
16
+ // P1⑦ (2026-09-15): MCP 命名空间隔离 —— 防工具名碰撞 (Hermes 生产数据: 51 工具服务器上
17
+ // 幻觉工具名命中真实工具; cron job 本想调 kanban_list 结果执行了 list_goals)。
18
+ // 强制 serverName__toolName 前缀 + 精确匹配短路。
19
+ export function serverLabel(s) {
20
+ // 取服务器可读标识: 配置 name > 包名 (args 里 @scope/pkg 或 *.py) > command 首段 > url host
21
+ if (s && s.name) return sanitizeMcpName(s.name);
22
+ const fromArgs = (s && s.args || []).find((a) => /(?:^|\/)[@\w.-]+(?:@[\w.]+)?$/.test(a) && !a.startsWith("-"));
23
+ if (fromArgs) return sanitizeMcpName(fromArgs.split("/").pop());
24
+ if (s && s.command) return sanitizeMcpName(String(s.command).split(/[\\/\s]/).filter(Boolean).pop() || "mcp");
25
+ if (s && s.url) {
26
+ try {
27
+ const host = new URL(s.url).hostname.split(".").filter(Boolean);
28
+ // 取二级域: mcp.example.com → example; www.google.com → google
29
+ return sanitizeMcpName(host.length >= 2 ? host[host.length - 2] : host[0] || "mcp");
30
+ } catch (e) { debug(`[mcp/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
31
+ }
32
+ return "mcp";
33
+ }
34
+
35
+ export function namespacedMcpName(label, raw) {
36
+ const clean = sanitizeMcpName(raw);
37
+ return clean ? `${label}__${clean}` : label;
38
+ }
39
+
40
+ // v1.0.8: MCP 工具描述清洗 (单行化 + 截断) — 服务器描述直接进 LLM prompt, 防换行/长文本注入指令
41
+ // P1⑦: 额外剔除危险 flag (--system/--dangerously 等命令行注入惯用手法)
42
+ export function sanitizeMcpDescription(desc, name) {
43
+ let s = String(desc || "").replace(/\s+/g, " ").trim();
44
+ // 危险 flag 清洗: 描述里出现命令行注入惯用 flag 视为可疑, 剔除该片段
45
+ s = s.replace(/--(?:system|dangerously[-\w]*|yes|force|no[-\w]+|y)\b/g, "[flag]");
46
+ return s.slice(0, 200) || `MCP 工具: ${name}`;
47
+ }
48
+
49
+ // 连接所有 MCP 服务器, 列出工具并注册到 catalog。
50
+ // 除 tools 外, 服务器声明 resources/prompts 能力时, 额外注册对应读写工具 (P2⑥)。
51
+ // 返回 { count, clients, close } — close() 关闭全部连接 (调用方负责生命周期)
52
+ export async function registerMcpTools(catalog, servers = []) {
53
+ const clients = [];
54
+ let count = 0;
55
+ for (const s of servers) {
56
+ if (!s) { warn("[mcp] 跳过无效服务器配置"); continue; }
57
+ if (!s.command && !s.url) { warn("[mcp] 跳过无效服务器配置 (缺 command/url)"); continue; }
58
+ const client = new McpClient(s);
59
+ try {
60
+ await client.connect();
61
+ const tools = await client.listTools();
62
+ // P1⑦: 命名空间强制前缀 (serverLabel) —— 默认开启, 可显式 prefix 覆盖/关闭
63
+ const label = serverLabel(s);
64
+ const ns = s.prefix != null ? s.prefix : `${label}__`;
65
+ for (const t of tools) {
66
+ if (!t || !t.name) continue;
67
+ const name = ns + sanitizeMcpName(t.name);
68
+ // P1⑦: 精确匹配短路 —— 注册时登记原始名 (供幻觉工具名防御: 无前缀名永不执行)
69
+ catalog.register({
70
+ name,
71
+ description: sanitizeMcpDescription(t.description, t.name),
72
+ parameters: t.inputSchema || { type: "object", properties: {} },
73
+ category: "mcp",
74
+ // isError 时加错误前缀, 让皮皮虾的自愈重试语义对 MCP 工具同样生效
75
+ execute: async (args) => {
76
+ const raw = await client.callToolRaw(t.name, args || {});
77
+ const { text, isError } = extractToolResult(raw);
78
+ return isError ? `${TOOL_ERROR_PREFIX} MCP 工具 ${t.name}: ${text}` : text;
79
+ },
80
+ });
81
+ count += 1;
82
+ }
83
+ count += registerResourceTools(catalog, client, s, ns);
84
+ clients.push(client);
85
+ } catch (e) {
86
+ warn(`[mcp] 服务器 ${s.command || s.url} 连接失败: ${e.message}`);
87
+ client.close();
88
+ }
89
+ }
90
+ if (count) info(`[mcp] 已注册 ${count} 个 MCP 工具`);
91
+ return { count, clients, close: () => clients.forEach((c) => c.close()) };
92
+ }
93
+
94
+ // 服务器声明 resources 能力时, 注册 list_resources / read_resource 两个工具 (让 LLM 可读远程资源)
95
+ function registerResourceTools(catalog, client, s, ns = "") {
96
+ if (!client.capabilities?.resources) return 0;
97
+ const label = s.command || s.url || "MCP";
98
+ catalog.register({
99
+ name: ns + "list_resources",
100
+ description: `列出 MCP 服务器 ${label} 提供的资源 (URI 列表)`,
101
+ parameters: { type: "object", properties: {}, required: [] },
102
+ category: "mcp",
103
+ execute: async () => {
104
+ const res = await client.listResources();
105
+ return res.map((r) => `${r.uri || "?"}${r.name ? ` — ${r.name}` : ""}`).join("\n") || "(无资源)";
106
+ },
107
+ });
108
+ catalog.register({
109
+ name: ns + "read_resource",
110
+ description: `读取 MCP 服务器 ${label} 的资源内容 (按 uri)`,
111
+ parameters: { type: "object", properties: { uri: { type: "string", description: "资源 URI" } }, required: ["uri"] },
112
+ category: "mcp",
113
+ execute: async (args) => extractResourceText(await client.readResource((args || {}).uri)),
114
+ });
115
+ return 2;
116
+ }