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
package/src/mcp/client.js CHANGED
@@ -1,375 +1,487 @@
1
- // src/mcp/client.js - MCP (Model Context Protocol) 客户端 (零依赖)
2
- // 传输可插拔: stdio (本机 npx/uvx 服务器) + HTTP (Streamable HTTP, 远程托管服务器)。
3
- // 协议: JSON-RPC 2.0 (换行分隔 JSON / HTTP body)。协议版本 2024-11-05 (stdio) / 2025-03-26 (HTTP)。
4
- // 能力: initialize 握手 / tools/list / tools/call / resources/list / resources/read。
5
- // 可靠性: 请求超时 + 断连时拒绝所有 pending 请求 (不再永久挂起)。
6
- import { spawn, spawnSync } from "node:child_process";
7
- import { warn } from "../utils/logger.js";
8
-
9
- const PROTOCOL_VERSION = "2024-11-05"; // stdio 本地服务器广泛兼容的版本
10
- const HTTP_PROTOCOL_VERSION = "2025-03-26"; // Streamable HTTP 传输对应的协议版本
11
- const DEFAULT_TIMEOUT = 60000; // 单请求默认超时 (ms)
12
- const MAX_STDIO_BUF = 1024 * 1024; // stdout 缓冲上限: 防恶意/异常服务器发超长行耗尽内存 (v1.0.8)
13
-
14
- // ---- SSE (Server-Sent Events) 解析 ----
15
- // Streamable HTTP 服务器可用 text/event-stream 响应。抽 event -> data 里的 JSON-RPC 消息。
16
- export function parseSSE(text) {
17
- const out = [];
18
- const events = String(text).split(/\r?\n\r?\n/);
19
- for (const ev of events) {
20
- let name = "message";
21
- const data = [];
22
- for (const line of ev.split(/\r?\n/)) {
23
- if (!line || line.startsWith(":")) continue;
24
- if (line.startsWith("event:")) name = line.slice(6).trim();
25
- else if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
26
- }
27
- if (!data.length) continue;
28
- if (name !== "message" && name !== "endpoint") continue;
29
- const payload = data.join("\n").trim();
30
- if (!payload) continue;
31
- try {
32
- const j = JSON.parse(payload);
33
- if (Array.isArray(j)) out.push(...j);
34
- else out.push(j);
35
- } catch { /* 忽略非 JSON 负载 */ }
36
- }
37
- return out;
38
- }
39
-
40
- // 解析 HTTP 响应体 -> JSON-RPC 消息数组 (SSE 或 JSON 单条/批量)
41
- function parseHttpBody(text, contentType) {
42
- const ct = (contentType || "").split(";")[0].trim().toLowerCase();
43
- const trimmed = text.trim();
44
- if (ct === "text/event-stream" || /^(event|data):/m.test(trimmed)) {
45
- return parseSSE(trimmed);
46
- }
47
- try {
48
- const j = JSON.parse(trimmed);
49
- return Array.isArray(j) ? j : [j];
50
- } catch { return []; }
51
- }
52
-
53
- // ---- stdio 传输 ----
54
- // 启动本地服务器子进程, 从 stdout 读换行分隔 JSON, 请求写 stdin。
55
- class StdioTransport {
56
- constructor({ command, args = [], env = {}, cwd = null }) {
57
- this.command = command;
58
- this.args = args;
59
- this.env = env;
60
- this.cwd = cwd;
61
- this.proc = null;
62
- this._buf = "";
63
- this._spawned = false;
64
- }
65
-
66
- start() {
67
- return new Promise((resolve, reject) => {
68
- const proc = spawn(this.command, this.args, {
69
- cwd: this.cwd || undefined,
70
- env: { ...process.env, ...this.env },
71
- stdio: ["pipe", "pipe", "pipe"],
72
- });
73
- this.proc = proc;
74
- proc.stdout.setEncoding("utf8");
75
- proc.stdout.on("data", (chunk) => this._onData(chunk));
76
- proc.stderr.on("data", (d) => warn(`[mcp] ${this.command} stderr: ${String(d).slice(0, 300)}`));
77
- // v1.0.8: stdin error 监听 (子进程退出后 write 触发 EPIPE 会 unhandled 崩溃)
78
- if (proc.stdin) proc.stdin.on("error", (e) => warn(`[mcp] ${this.command} stdin: ${e.message}`));
79
- proc.once("spawn", () => { this._spawned = true; resolve(); });
80
- proc.once("error", (e) => {
81
- if (!this._spawned) reject(new Error(`无法启动 MCP 服务器 ${this.command}: ${e.message}`));
82
- else this.onClose?.(e);
83
- });
84
- proc.on("exit", (code) => {
85
- if (this._spawned) this.onClose?.(new Error(`MCP 服务器进程退出 (code ${code})`));
86
- });
87
- });
88
- }
89
-
90
- send(msg) {
91
- if (!this.proc || !this.proc.stdin || this.proc.killed) {
92
- throw new Error("MCP 服务器未连接");
93
- }
94
- try {
95
- this.proc.stdin.write(JSON.stringify(msg) + "\n");
96
- } catch (e) {
97
- throw new Error(`MCP 写入失败: ${e.message}`);
98
- }
99
- }
100
-
101
- close() {
102
- if (this.proc) {
103
- const pid = this.proc.pid;
104
- // v1.0.8: 杀进程树 (npx/uvx 会派生子进程, 只 kill 直接子进程会泄漏)
105
- if (process.platform === "win32") {
106
- try { spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", timeout: 5000 }); } catch {}
107
- } else {
108
- try { process.kill(-pid, "SIGTERM"); } catch {} // 负 pid = 整个进程组
109
- }
110
- try { this.proc.kill(); } catch {}
111
- }
112
- this.proc = null;
113
- }
114
-
115
- _onData(chunk) {
116
- this._buf += chunk;
117
- // v1.0.8: 缓冲上限保护 (防内存耗尽)
118
- if (this._buf.length > MAX_STDIO_BUF) {
119
- warn(`[mcp] ${this.command} stdout 缓冲超限 (${MAX_STDIO_BUF}), 丢弃多余数据`);
120
- this._buf = this._buf.slice(-4096);
121
- }
122
- let idx;
123
- while ((idx = this._buf.indexOf("\n")) !== -1) {
124
- const line = this._buf.slice(0, idx).trim();
125
- this._buf = this._buf.slice(idx + 1);
126
- if (!line) continue;
127
- let msg;
128
- try { msg = JSON.parse(line); } catch { continue; }
129
- this.onMessage?.(msg);
130
- }
131
- }
132
- }
133
-
134
- // ---- HTTP 传输 (Streamable HTTP) ----
135
- // 每次请求 POST 到 url, 响应可为 JSON 或 SSE; 维护 Mcp-Session-Id 会话头。
136
- class HttpTransport {
137
- constructor({ url, headers = {}, timeout = DEFAULT_TIMEOUT }) {
138
- this.url = url;
139
- this.headers = headers;
140
- this.timeout = timeout;
141
- this.sessionId = null;
142
- this.closed = false;
143
- }
144
-
145
- async start() { /* 单次 POST 流即可, 无需预开连接 */ }
146
-
147
- async send(msg) {
148
- const headers = {
149
- "content-type": "application/json",
150
- "accept": "application/json, text/event-stream",
151
- ...this.headers,
152
- };
153
- if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
154
-
155
- const ctrl = new AbortController();
156
- const timer = setTimeout(() => ctrl.abort(), this.timeout);
157
- timer.unref?.(); // 不阻塞事件循环退出
158
-
159
- let res;
160
- try {
161
- res = await fetch(this.url, { method: "POST", headers, body: JSON.stringify(msg), signal: ctrl.signal });
162
- } catch (e) {
163
- const err = e.name === "AbortError"
164
- ? new Error(`MCP HTTP 请求超时 (${this.timeout}ms)`)
165
- : new Error(`MCP HTTP 请求失败: ${e.message}`);
166
- this._error(msg, err);
167
- return;
168
- } finally {
169
- clearTimeout(timer);
170
- }
171
-
172
- const sid = res.headers.get("mcp-session-id");
173
- if (sid) this.sessionId = sid;
174
-
175
- if (!res.ok) {
176
- const t = await res.text().catch(() => "");
177
- this._error(msg, new Error(`MCP HTTP ${res.status}: ${t.slice(0, 200)}`));
178
- return;
179
- }
180
- const text = await res.text();
181
- if (!text) return; // 202/204 空响应 (通知), 无需分发
182
- for (const m of parseHttpBody(text, res.headers.get("content-type") || "")) {
183
- this.onMessage?.(m);
184
- }
185
- }
186
-
187
- _error(msg, e) {
188
- if (msg && msg.id != null) {
189
- this.onMessage?.({ jsonrpc: "2.0", id: msg.id, error: { code: -32603, message: e.message } });
190
- } else {
191
- warn(`[mcp] HTTP 通知失败: ${e.message}`);
192
- }
193
- }
194
-
195
- close() { this.closed = true; }
196
- }
197
-
198
- // ---- MCP 客户端核心 (传输无关) ----
199
- export class McpClient {
200
- constructor(config = {}) {
201
- this.config = config;
202
- this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
203
- const isHttp = Boolean(config.url) || config.transport === "http";
204
- if (isHttp) {
205
- if (!config.url) throw new Error("HTTP MCP 服务器需 url");
206
- this.transport = new HttpTransport(config);
207
- this.protocolVersion = config.protocolVersion || HTTP_PROTOCOL_VERSION;
208
- } else {
209
- if (!config.command) throw new Error("stdio MCP 服务器需 command");
210
- this.transport = new StdioTransport(config);
211
- this.protocolVersion = config.protocolVersion || PROTOCOL_VERSION;
212
- }
213
- this._pending = new Map(); // id -> { resolve, reject, timer }
214
- this._nextId = 0;
215
- this._dead = false;
216
- this.capabilities = {}; // initialize 握手后填充
217
- this.transport.onMessage = (m) => this._onMessage(m);
218
- this.transport.onClose = (e) => this._failAll(e || new Error("MCP 连接已断开"));
219
- }
220
-
221
- // 启动 + initialize 握手 + initialized 通知
222
- async connect() {
223
- await this.transport.start();
224
- const init = await this._request("initialize", {
225
- protocolVersion: this.protocolVersion,
226
- capabilities: {},
227
- clientInfo: { name: "ppx-agent", version: "0.1.0" },
228
- });
229
- if (init && init.protocolVersion) this.protocolVersion = init.protocolVersion;
230
- this.capabilities = (init && init.capabilities) || {};
231
- await this._notify("notifications/initialized", {});
232
- return this;
233
- }
234
-
235
- async listTools() {
236
- const r = await this._request("tools/list", {});
237
- return (r && r.tools) || [];
238
- }
239
-
240
- // 调用工具 -> 提取纯文本结果 (向后兼容)
241
- async callTool(name, args = {}) {
242
- return extractToolText(await this.callToolRaw(name, args));
243
- }
244
-
245
- // 调用工具 -> 返回原始 result (含 content/isError, 供上层判断)
246
- async callToolRaw(name, args = {}) {
247
- return this._request("tools/call", { name, arguments: args });
248
- }
249
-
250
- async listResources() {
251
- const r = await this._request("resources/list", {});
252
- return (r && r.resources) || [];
253
- }
254
-
255
- async readResource(uri) {
256
- return this._request("resources/read", { uri });
257
- }
258
-
259
- async listPrompts() {
260
- const r = await this._request("prompts/list", {});
261
- return (r && r.prompts) || [];
262
- }
263
-
264
- // 获取提示模板并填充参数 -> 返回消息数组 [{ role, content }]
265
- async getPrompt(name, args = {}) {
266
- const r = await this._request("prompts/get", { name, arguments: args });
267
- return (r && r.messages) || [];
268
- }
269
-
270
- close() {
271
- if (this._dead) return;
272
- this._dead = true;
273
- this.transport.close?.();
274
- this._failAll(new Error("MCP 连接已关闭"));
275
- }
276
-
277
- // ---- 内部 ----
278
- _request(method, params = {}) {
279
- if (this._dead) return Promise.reject(new Error("MCP 连接已关闭"));
280
- const id = ++this._nextId;
281
- const msg = { jsonrpc: "2.0", id, method, params };
282
- return new Promise((resolve, reject) => {
283
- const timer = setTimeout(() => {
284
- this._pending.delete(id);
285
- reject(new Error(`MCP 请求超时 (${this.timeout}ms): ${method}`));
286
- }, this.timeout);
287
- this._pending.set(id, { resolve, reject, timer });
288
- Promise.resolve()
289
- .then(() => this.transport.send(msg))
290
- .catch((e) => this._settle(id, null, e));
291
- });
292
- }
293
-
294
- _notify(method, params = {}) {
295
- if (this._dead) return Promise.resolve();
296
- return Promise.resolve()
297
- .then(() => this.transport.send({ jsonrpc: "2.0", method, params }))
298
- .catch((e) => warn(`[mcp] 通知发送失败 ${method}: ${e.message}`));
299
- }
300
-
301
- _onMessage(msg) {
302
- if (!msg || msg.id == null) return; // 忽略服务器通知/无 id 消息
303
- const p = this._pending.get(msg.id);
304
- if (!p) return;
305
- if (msg.error) this._settle(msg.id, null, new Error(msg.error.message || "MCP 调用失败"));
306
- else this._settle(msg.id, msg.result, null);
307
- }
308
-
309
- _settle(id, result, err) {
310
- const p = this._pending.get(id);
311
- if (!p) return;
312
- this._pending.delete(id);
313
- clearTimeout(p.timer);
314
- err ? p.reject(err) : p.resolve(result);
315
- }
316
-
317
- _failAll(err) {
318
- for (const [id, p] of [...this._pending]) {
319
- clearTimeout(p.timer);
320
- this._pending.delete(id);
321
- p.reject(err);
322
- }
323
- }
324
- }
325
-
326
- // 提取 tools/call 结果的文本 (MCP 返回 content 数组, 每项 type=text 含 text)
327
- export function extractToolText(result) {
328
- if (result == null) return "";
329
- const content = result.content;
330
- if (Array.isArray(content)) {
331
- const texts = content
332
- .filter((c) => c && c.type === "text" && typeof c.text === "string")
333
- .map((c) => c.text);
334
- if (texts.length) return texts.join("\n");
335
- // 非文本内容 (image/resource) 回退为 JSON
336
- return JSON.stringify(content);
337
- }
338
- if (typeof result === "string") return result;
339
- return JSON.stringify(result);
340
- }
341
-
342
- // 提取 tools/call 结果的 { text, isError } (识别 MCP 工具级报错, 供自愈重试)
343
- export function extractToolResult(result) { if (result == null) return { text: "", isError: false };
344
- const content = result.content;
345
- let isError = Boolean(result.isError);
346
- if (Array.isArray(content)) {
347
- const texts = [];
348
- for (const c of content) {
349
- if (!c) continue;
350
- if (c.isError) isError = true;
351
- if (c.type === "text" && typeof c.text === "string") texts.push(c.text);
352
- else if (c.type === "image" || c.type === "resource" || c.type === "audio") {
353
- texts.push(`[${c.type}: ${c.mimeType || c.uri || ""}]`);
354
- }
355
- }
356
- if (texts.length) return { text: texts.join("\n"), isError };
357
- return { text: JSON.stringify(content), isError };
358
- }
359
- if (typeof result === "string") return { text: result, isError };
360
- return { text: JSON.stringify(result), isError };
361
- }
362
-
363
- // 提取 resources/read 结果的文本 (MCP 返回 { contents: [{ uri, mimeType, text }] })
364
- export function extractResourceText(result) {
365
- if (result == null) return "";
366
- const contents = result.contents;
367
- if (Array.isArray(contents)) {
368
- const texts = contents
369
- .filter((c) => c && typeof c.text === "string")
370
- .map((c) => `[${c.uri || "resource"}]\n${c.text}`);
371
- return texts.join("\n\n") || JSON.stringify(contents);
372
- }
373
- if (typeof result === "string") return result;
374
- return JSON.stringify(result);
375
- }
1
+ // src/mcp/client.js - MCP (Model Context Protocol) 客户端 (零依赖)
2
+ // 传输可插拔: stdio (本机 npx/uvx 服务器) + HTTP (Streamable HTTP, 远程托管服务器)。
3
+ // 协议: JSON-RPC 2.0 (换行分隔 JSON / HTTP body)。协议版本 2024-11-05 (stdio) / 2025-03-26 (HTTP)。
4
+ // 能力: initialize 握手 / tools/list / tools/call / resources/list / resources/read。
5
+ // 可靠性: 请求超时 + 断连时拒绝所有 pending 请求 (不再永久挂起)。
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { createRequire } from "node:module";
8
+ import { warn, debug } from "../utils/logger.js";
9
+
10
+ const require = createRequire(import.meta.url);
11
+ // 客户端版本随包走, 不硬编码 (外部体检: 曾写死 0.1.0 与 package.json 漂移, server.js 已修, 此处补齐)
12
+ let PKG_VERSION = "0.0.0";
13
+ try { PKG_VERSION = require("../../package.json").version || "0.0.0"; } catch (e) { debug(`[mcp/client] 已忽略异常: ${e && e.message ? e.message : e}`); }
14
+
15
+ const PROTOCOL_VERSION = "2024-11-05"; // stdio 本地服务器广泛兼容的版本
16
+ const HTTP_PROTOCOL_VERSION = "2025-03-26"; // Streamable HTTP 传输对应的协议版本
17
+ const DEFAULT_TIMEOUT = 60000; // 单请求默认超时 (ms)
18
+ const MAX_STDIO_BUF = 1024 * 1024; // stdout 缓冲上限: 防恶意/异常服务器发超长行耗尽内存 (v1.0.8)
19
+
20
+ // ---- SSE (Server-Sent Events) 解析 ----
21
+ // Streamable HTTP 服务器可用 text/event-stream 响应。抽 event -> data 里的 JSON-RPC 消息。
22
+ export function parseSSE(text) {
23
+ const out = [];
24
+ const events = String(text).split(/\r?\n\r?\n/);
25
+ for (const ev of events) {
26
+ let name = "message";
27
+ const data = [];
28
+ for (const line of ev.split(/\r?\n/)) {
29
+ if (!line || line.startsWith(":")) continue;
30
+ if (line.startsWith("event:")) name = line.slice(6).trim();
31
+ else if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
32
+ }
33
+ if (!data.length) continue;
34
+ if (name !== "message" && name !== "endpoint") continue;
35
+ const payload = data.join("\n").trim();
36
+ if (!payload) continue;
37
+ try {
38
+ const j = JSON.parse(payload);
39
+ if (Array.isArray(j)) out.push(...j);
40
+ else out.push(j);
41
+ } catch { /* 忽略非 JSON 负载 */ }
42
+ }
43
+ return out;
44
+ }
45
+
46
+ // 解析 HTTP 响应体 -> JSON-RPC 消息数组 (SSE 或 JSON 单条/批量)
47
+ function parseHttpBody(text, contentType) {
48
+ const ct = (contentType || "").split(";")[0].trim().toLowerCase();
49
+ const trimmed = text.trim();
50
+ if (ct === "text/event-stream" || /^(event|data):/m.test(trimmed)) {
51
+ return parseSSE(trimmed);
52
+ }
53
+ try {
54
+ const j = JSON.parse(trimmed);
55
+ return Array.isArray(j) ? j : [j];
56
+ } catch { return []; }
57
+ }
58
+
59
+ // ---- x-mcp-header 标注解析 (MCP 2026-07-28) ----
60
+ // 从工具 inputSchema 提取 x-mcp-header 标注: 参数路径 (纯 properties 链) -> header 名。
61
+ // 约束校验 (违反任一 → 返回 null 表示该工具定义非法, 应排除):
62
+ // - header 值非空, 匹配 HTTP token 语法 (RFC 9110 1*tchar)
63
+ // - header 名在 schema 内大小写不敏感唯一
64
+ // - 只允许 primitive 类型 (integer/string/boolean; number 不允许)
65
+ // - 静态可达: 路径只能经过 properties, 不得穿过 items/composition/conditional/$ref
66
+ // - 未经数组/组合/条件/$ref 嵌套的纯 properties 链
67
+ const HTTP_TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
68
+
69
+ function parseXMcpHeaders(toolName, schema) {
70
+ const found = []; // { path, header }
71
+ const seen = new Set(); // header (小写去重)
72
+ const walk = (node, path, depth) => {
73
+ if (!node || typeof node !== "object" || depth > 8) return;
74
+ // 检查当前属性是否有 x-mcp-header
75
+ if (node["x-mcp-header"] !== undefined) {
76
+ const hv = String(node["x-mcp-header"]);
77
+ // 校验: 非空 + HTTP token
78
+ if (!hv || !HTTP_TOKEN.test(hv)) throw new Error(`header 非法: ${JSON.stringify(hv)}`);
79
+ // 校验: 大小写不敏感唯一
80
+ const lk = hv.toLowerCase();
81
+ if (seen.has(lk)) throw new Error(`header 重复: ${hv}`);
82
+ seen.add(lk);
83
+ // 校验: 类型必须 primitive 且非 number
84
+ const type = node.type;
85
+ if (!["integer", "string", "boolean"].includes(type)) throw new Error(`类型不允许: ${type || "(无)"}`);
86
+ found.push({ path: [...path], header: hv });
87
+ }
88
+ // 递归 properties (纯链)
89
+ if (node.properties && typeof node.properties === "object") {
90
+ for (const [k, v] of Object.entries(node.properties)) {
91
+ if (v && typeof v === "object") walk(v, [...path, k], depth + 1);
92
+ }
93
+ }
94
+ // 非法穿透: items/oneOf/anyOf/allOf/not/if/then/else/$ref → x-mcp-header 出现在其中 = 非法
95
+ for (const bad of ["items", "oneOf", "anyOf", "allOf", "not", "if", "then", "else", "$ref"]) {
96
+ if (node[bad] !== undefined && hasXMcpHeader(node[bad])) {
97
+ throw new Error(`x-mcp-header 出现在 ${bad} 内 (非法)`);
98
+ }
99
+ }
100
+ };
101
+ try {
102
+ walk(schema, [], 0);
103
+ return found;
104
+ } catch (e) {
105
+ warn(`[mcp] 工具 ${toolName} x-mcp-header 标注非法: ${e.message}`);
106
+ return null;
107
+ }
108
+ }
109
+
110
+ function hasXMcpHeader(node) {
111
+ if (!node || typeof node !== "object") return false;
112
+ if (node["x-mcp-header"] !== undefined) return true;
113
+ return Object.values(node).some((v) => v && typeof v === "object" && hasXMcpHeader(v));
114
+ }
115
+
116
+ // ---- stdio 传输 ----
117
+ // 启动本地服务器子进程, 从 stdout 读换行分隔 JSON, 请求写 stdin。
118
+ class StdioTransport {
119
+ constructor({ command, args = [], env = {}, cwd = null }) {
120
+ this.command = command;
121
+ this.args = args;
122
+ this.env = env;
123
+ this.cwd = cwd;
124
+ this.proc = null;
125
+ this._buf = "";
126
+ this._spawned = false;
127
+ }
128
+
129
+ start() {
130
+ return new Promise((resolve, reject) => {
131
+ const proc = spawn(this.command, this.args, {
132
+ cwd: this.cwd || undefined,
133
+ env: { ...process.env, ...this.env },
134
+ stdio: ["pipe", "pipe", "pipe"],
135
+ // 2026-09-18 修复 (仅非 Windows): close() 用 process.kill(-pid) 杀进程组,
136
+ // 但原 spawn 未 detached, 子进程与父进程同组 → 负 pid kill 必抛 ESRCH 被吞,
137
+ // npx/uvx 派生的孙进程在 Unix 上泄漏。Windows 走 taskkill /T 不受影响。
138
+ detached: process.platform !== "win32",
139
+ });
140
+ this.proc = proc;
141
+ proc.stdout.setEncoding("utf8");
142
+ proc.stdout.on("data", (chunk) => this._onData(chunk));
143
+ proc.stderr.on("data", (d) => warn(`[mcp] ${this.command} stderr: ${String(d).slice(0, 300)}`));
144
+ // v1.0.8: stdin error 监听 (子进程退出后 write 触发 EPIPE 会 unhandled 崩溃)
145
+ if (proc.stdin) proc.stdin.on("error", (e) => warn(`[mcp] ${this.command} stdin: ${e.message}`));
146
+ proc.once("spawn", () => { this._spawned = true; resolve(); });
147
+ proc.once("error", (e) => {
148
+ if (!this._spawned) reject(new Error(`无法启动 MCP 服务器 ${this.command}: ${e.message}`));
149
+ else this.onClose?.(e);
150
+ });
151
+ proc.on("exit", (code) => {
152
+ if (this._spawned) this.onClose?.(new Error(`MCP 服务器进程退出 (code ${code})`));
153
+ });
154
+ });
155
+ }
156
+
157
+ send(msg) {
158
+ if (!this.proc || !this.proc.stdin || this.proc.killed) {
159
+ throw new Error("MCP 服务器未连接");
160
+ }
161
+ try {
162
+ this.proc.stdin.write(JSON.stringify(msg) + "\n");
163
+ } catch (e) {
164
+ throw new Error(`MCP 写入失败: ${e.message}`);
165
+ }
166
+ }
167
+
168
+ close() {
169
+ if (this.proc) {
170
+ const pid = this.proc.pid;
171
+ // v1.0.8: 杀进程树 (npx/uvx 会派生子进程, 只 kill 直接子进程会泄漏)
172
+ if (process.platform === "win32") {
173
+ try { spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", timeout: 5000 }); } catch (e) { debug(`[mcp/client] 已忽略异常: ${e && e.message ? e.message : e}`); }
174
+ } else {
175
+ try { process.kill(-pid, "SIGTERM"); } catch (e) { debug(`[mcp/client] 已忽略异常: ${e && e.message ? e.message : e}`); } // 负 pid = 整个进程组
176
+ }
177
+ try { this.proc.kill(); } catch (e) { debug(`[mcp/client] 已忽略异常: ${e && e.message ? e.message : e}`); }
178
+ }
179
+ this.proc = null;
180
+ }
181
+
182
+ _onData(chunk) {
183
+ this._buf += chunk;
184
+ // v1.0.8: 缓冲上限保护 (防内存耗尽)
185
+ if (this._buf.length > MAX_STDIO_BUF) {
186
+ warn(`[mcp] ${this.command} stdout 缓冲超限 (${MAX_STDIO_BUF}), 丢弃多余数据`);
187
+ this._buf = this._buf.slice(-4096);
188
+ }
189
+ let idx;
190
+ while ((idx = this._buf.indexOf("\n")) !== -1) {
191
+ const line = this._buf.slice(0, idx).trim();
192
+ this._buf = this._buf.slice(idx + 1);
193
+ if (!line) continue;
194
+ let msg;
195
+ try { msg = JSON.parse(line); } catch { continue; }
196
+ this.onMessage?.(msg);
197
+ }
198
+ }
199
+ }
200
+
201
+ // ---- HTTP 传输 (Streamable HTTP) ----
202
+ // 每次请求 POST 到 url, 响应可为 JSON 或 SSE; 维护 Mcp-Session-Id 会话头。
203
+ class HttpTransport {
204
+ constructor({ url, headers = {}, timeout = DEFAULT_TIMEOUT }) {
205
+ this.url = url;
206
+ this.headers = headers;
207
+ this.timeout = timeout;
208
+ this.sessionId = null;
209
+ this.closed = false;
210
+ }
211
+
212
+ async start() { /* 单次 POST 流即可, 无需预开连接 */ }
213
+
214
+ async send(msg, extraHeaders = {}) {
215
+ const headers = {
216
+ "content-type": "application/json",
217
+ "accept": "application/json, text/event-stream",
218
+ ...this.headers,
219
+ ...extraHeaders,
220
+ };
221
+ if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
222
+
223
+ const ctrl = new AbortController();
224
+ const timer = setTimeout(() => ctrl.abort(), this.timeout);
225
+ timer.unref?.(); // 不阻塞事件循环退出
226
+
227
+ let res;
228
+ try {
229
+ res = await fetch(this.url, { method: "POST", headers, body: JSON.stringify(msg), signal: ctrl.signal });
230
+ } catch (e) {
231
+ const err = e.name === "AbortError"
232
+ ? new Error(`MCP HTTP 请求超时 (${this.timeout}ms)`)
233
+ : new Error(`MCP HTTP 请求失败: ${e.message}`);
234
+ this._error(msg, err);
235
+ return;
236
+ } finally {
237
+ clearTimeout(timer);
238
+ }
239
+
240
+ const sid = res.headers.get("mcp-session-id");
241
+ if (sid) this.sessionId = sid;
242
+
243
+ if (!res.ok) {
244
+ const t = await res.text().catch(() => "");
245
+ this._error(msg, new Error(`MCP HTTP ${res.status}: ${t.slice(0, 200)}`));
246
+ return;
247
+ }
248
+ const text = await res.text();
249
+ if (!text) return; // 202/204 空响应 (通知), 无需分发
250
+ for (const m of parseHttpBody(text, res.headers.get("content-type") || "")) {
251
+ this.onMessage?.(m);
252
+ }
253
+ }
254
+
255
+ _error(msg, e) {
256
+ if (msg && msg.id != null) {
257
+ this.onMessage?.({ jsonrpc: "2.0", id: msg.id, error: { code: -32603, message: e.message } });
258
+ } else {
259
+ warn(`[mcp] HTTP 通知失败: ${e.message}`);
260
+ }
261
+ }
262
+
263
+ close() { this.closed = true; }
264
+ }
265
+
266
+ // ---- MCP 客户端核心 (传输无关) ----
267
+ export class McpClient {
268
+ constructor(config = {}) {
269
+ this.config = config;
270
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
271
+ const isHttp = Boolean(config.url) || config.transport === "http";
272
+ if (isHttp) {
273
+ if (!config.url) throw new Error("HTTP MCP 服务器需 url");
274
+ this.transport = new HttpTransport(config);
275
+ this.protocolVersion = config.protocolVersion || HTTP_PROTOCOL_VERSION;
276
+ } else {
277
+ if (!config.command) throw new Error("stdio MCP 服务器需 command");
278
+ this.transport = new StdioTransport(config);
279
+ this.protocolVersion = config.protocolVersion || PROTOCOL_VERSION;
280
+ }
281
+ this._pending = new Map(); // id -> { resolve, reject, timer }
282
+ this._nextId = 0;
283
+ this._dead = false;
284
+ this.capabilities = {}; // initialize 握手后填充
285
+ this.transport.onMessage = (m) => this._onMessage(m);
286
+ this.transport.onClose = (e) => this._failAll(e || new Error("MCP 连接已断开"));
287
+ }
288
+
289
+ // 启动 + initialize 握手 + initialized 通知
290
+ async connect() {
291
+ await this.transport.start();
292
+ const init = await this._request("initialize", {
293
+ protocolVersion: this.protocolVersion,
294
+ capabilities: {},
295
+ clientInfo: { name: "ppx-agent", version: PKG_VERSION },
296
+ });
297
+ if (init && init.protocolVersion) this.protocolVersion = init.protocolVersion;
298
+ this.capabilities = (init && init.capabilities) || {};
299
+ await this._notify("notifications/initialized", {});
300
+ return this;
301
+ }
302
+
303
+ async listTools() {
304
+ const r = await this._request("tools/list", {});
305
+ const tools = (r && r.tools) || [];
306
+ // v2.6.0: x-mcp-header 客户端支持 — 解析每把工具的 inputSchema 里的 x-mcp-header 标注,
307
+ // 存为 { 参数路径 -> headerName }; 非法标注 (约束见 MCP 2026-07-28 规范) 的工具整体排除
308
+ this._toolHeaders = new Map(); // toolName -> [{ path: string[], header: string }]
309
+ const kept = [];
310
+ for (const t of tools) {
311
+ if (!t || !t.name || !t.inputSchema) { kept.push(t); continue; }
312
+ const parsed = parseXMcpHeaders(t.name, t.inputSchema);
313
+ if (parsed === null) {
314
+ // 非法标注: 排除该工具 (规范: 单个坏工具不影响其他有效工具)
315
+ warn(`[mcp] 工具 ${t.name} 的 x-mcp-header 标注非法, 已排除`);
316
+ continue;
317
+ }
318
+ if (parsed.length) this._toolHeaders.set(t.name, parsed);
319
+ kept.push(t);
320
+ }
321
+ return kept;
322
+ }
323
+
324
+ // 调用工具 -> 提取纯文本结果 (向后兼容)
325
+ async callTool(name, args = {}) {
326
+ return extractToolText(await this.callToolRaw(name, args));
327
+ }
328
+
329
+ // 调用工具 -> 返回原始 result (含 content/isError, 供上层判断)
330
+ async callToolRaw(name, args = {}) {
331
+ const msg = { name, arguments: args };
332
+ // x-mcp-header 镜像: 工具定义里有标注时, 把参数值转成 Mcp-Param-* 头 (仅 HTTP 传输生效)
333
+ const extraHeaders = this._resolveParamHeaders(name, args);
334
+ return this._request("tools/call", msg, extraHeaders);
335
+ }
336
+
337
+ // 解析工具参数 -> Mcp-Param-{Header} HTTP 头 (x-mcp-header 标注)
338
+ // 按规范: 仅 primitive (string/integer/boolean) 且静态可达路径 (纯 properties 链) 可镜像
339
+ _resolveParamHeaders(name, args) {
340
+ const out = {};
341
+ const anns = this._toolHeaders && this._toolHeaders.get(name);
342
+ if (!anns || !args || typeof args !== "object") return out;
343
+ for (const { path, header } of anns) {
344
+ let v = args;
345
+ for (const seg of path) {
346
+ if (v == null || typeof v !== "object") { v = undefined; break; }
347
+ v = v[seg];
348
+ }
349
+ if (v === undefined) continue; // 参数未提供 -> 省略该头
350
+ let sv;
351
+ if (typeof v === "string") sv = v;
352
+ else if (typeof v === "number" && Number.isInteger(v)) sv = String(v);
353
+ else if (typeof v === "boolean") sv = v ? "true" : "false";
354
+ else continue; // 非 primitive 不镜像
355
+ out[`Mcp-Param-${header}`] = sv;
356
+ }
357
+ return out;
358
+ }
359
+
360
+ async listResources() {
361
+ const r = await this._request("resources/list", {});
362
+ return (r && r.resources) || [];
363
+ }
364
+
365
+ async readResource(uri) {
366
+ return this._request("resources/read", { uri });
367
+ }
368
+
369
+ async listPrompts() {
370
+ const r = await this._request("prompts/list", {});
371
+ return (r && r.prompts) || [];
372
+ }
373
+
374
+ // 获取提示模板并填充参数 -> 返回消息数组 [{ role, content }]
375
+ async getPrompt(name, args = {}) {
376
+ const r = await this._request("prompts/get", { name, arguments: args });
377
+ return (r && r.messages) || [];
378
+ }
379
+
380
+ close() {
381
+ if (this._dead) return;
382
+ this._dead = true;
383
+ this.transport.close?.();
384
+ this._failAll(new Error("MCP 连接已关闭"));
385
+ }
386
+
387
+ // ---- 内部 ----
388
+ _request(method, params = {}, extraHeaders = {}) {
389
+ if (this._dead) return Promise.reject(new Error("MCP 连接已关闭"));
390
+ const id = ++this._nextId;
391
+ const msg = { jsonrpc: "2.0", id, method, params };
392
+ return new Promise((resolve, reject) => {
393
+ const timer = setTimeout(() => {
394
+ this._pending.delete(id);
395
+ reject(new Error(`MCP 请求超时 (${this.timeout}ms): ${method}`));
396
+ }, this.timeout);
397
+ this._pending.set(id, { resolve, reject, timer });
398
+ Promise.resolve()
399
+ .then(() => this.transport.send(msg, extraHeaders))
400
+ .catch((e) => this._settle(id, null, e));
401
+ });
402
+ }
403
+
404
+ _notify(method, params = {}) {
405
+ if (this._dead) return Promise.resolve();
406
+ return Promise.resolve()
407
+ .then(() => this.transport.send({ jsonrpc: "2.0", method, params }))
408
+ .catch((e) => warn(`[mcp] 通知发送失败 ${method}: ${e.message}`));
409
+ }
410
+
411
+ _onMessage(msg) {
412
+ if (!msg || msg.id == null) return; // 忽略服务器通知/无 id 消息
413
+ const p = this._pending.get(msg.id);
414
+ if (!p) return;
415
+ if (msg.error) this._settle(msg.id, null, new Error(msg.error.message || "MCP 调用失败"));
416
+ else this._settle(msg.id, msg.result, null);
417
+ }
418
+
419
+ _settle(id, result, err) {
420
+ const p = this._pending.get(id);
421
+ if (!p) return;
422
+ this._pending.delete(id);
423
+ clearTimeout(p.timer);
424
+ err ? p.reject(err) : p.resolve(result);
425
+ }
426
+
427
+ _failAll(err) {
428
+ for (const [id, p] of [...this._pending]) {
429
+ clearTimeout(p.timer);
430
+ this._pending.delete(id);
431
+ p.reject(err);
432
+ }
433
+ }
434
+ }
435
+
436
+ // 提取 tools/call 结果的文本 (MCP 返回 content 数组, 每项 type=text 含 text)
437
+ export function extractToolText(result) {
438
+ if (result == null) return "";
439
+ const content = result.content;
440
+ if (Array.isArray(content)) {
441
+ const texts = content
442
+ .filter((c) => c && c.type === "text" && typeof c.text === "string")
443
+ .map((c) => c.text);
444
+ if (texts.length) return texts.join("\n");
445
+ // 非文本内容 (image/resource) 回退为 JSON
446
+ return JSON.stringify(content);
447
+ }
448
+ if (typeof result === "string") return result;
449
+ return JSON.stringify(result);
450
+ }
451
+
452
+ // 提取 tools/call 结果的 { text, isError } (识别 MCP 工具级报错, 供自愈重试)
453
+ export function extractToolResult(result) { if (result == null) return { text: "", isError: false };
454
+ const content = result.content;
455
+ let isError = Boolean(result.isError);
456
+ if (Array.isArray(content)) {
457
+ const texts = [];
458
+ for (const c of content) {
459
+ if (!c) continue;
460
+ if (c.isError) isError = true;
461
+ if (c.type === "text" && typeof c.text === "string") texts.push(c.text);
462
+ else if (c.type === "image" || c.type === "resource" || c.type === "audio") {
463
+ texts.push(`[${c.type}: ${c.mimeType || c.uri || ""}]`);
464
+ }
465
+ }
466
+ if (texts.length) return { text: texts.join("\n"), isError };
467
+ return { text: JSON.stringify(content), isError };
468
+ }
469
+ if (typeof result === "string") return { text: result, isError };
470
+ return { text: JSON.stringify(result), isError };
471
+ }
472
+
473
+ // 提取 resources/read 结果的文本 (MCP 返回 { contents: [{ uri, mimeType, text }] })
474
+ export function extractResourceText(result) {
475
+ if (result == null) return "";
476
+ const contents = result.contents;
477
+ if (Array.isArray(contents)) {
478
+ const texts = contents
479
+ .filter((c) => c && typeof c.text === "string")
480
+ .map((c) => `[${c.uri || "resource"}]\n${c.text}`);
481
+ return texts.join("\n\n") || JSON.stringify(contents);
482
+ }
483
+ if (typeof result === "string") return result;
484
+ return JSON.stringify(result);
485
+ }
486
+
487
+ export { parseXMcpHeaders, hasXMcpHeader };