zidane 4.0.2 → 4.1.4

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 (77) hide show
  1. package/README.md +196 -614
  2. package/dist/agent-BoV5Twdl.d.ts +2347 -0
  3. package/dist/agent-BoV5Twdl.d.ts.map +1 -0
  4. package/dist/contexts-3Arvn7yR.js +321 -0
  5. package/dist/contexts-3Arvn7yR.js.map +1 -0
  6. package/dist/contexts.d.ts +2 -25
  7. package/dist/contexts.js +2 -10
  8. package/dist/errors-D1lhd6mX.js +118 -0
  9. package/dist/errors-D1lhd6mX.js.map +1 -0
  10. package/dist/index-28otmfLX.d.ts +400 -0
  11. package/dist/index-28otmfLX.d.ts.map +1 -0
  12. package/dist/index-BfSdALzk.d.ts +113 -0
  13. package/dist/index-BfSdALzk.d.ts.map +1 -0
  14. package/dist/index-DPsd0qwm.d.ts +254 -0
  15. package/dist/index-DPsd0qwm.d.ts.map +1 -0
  16. package/dist/index.d.ts +5 -95
  17. package/dist/index.js +141 -271
  18. package/dist/index.js.map +1 -0
  19. package/dist/interpolate-CukJwP2G.js +887 -0
  20. package/dist/interpolate-CukJwP2G.js.map +1 -0
  21. package/dist/mcp-8wClKY-3.js +771 -0
  22. package/dist/mcp-8wClKY-3.js.map +1 -0
  23. package/dist/mcp.d.ts +2 -4
  24. package/dist/mcp.js +2 -13
  25. package/dist/messages-z5Pq20p7.js +1020 -0
  26. package/dist/messages-z5Pq20p7.js.map +1 -0
  27. package/dist/presets-Cs7_CsMk.js +39 -0
  28. package/dist/presets-Cs7_CsMk.js.map +1 -0
  29. package/dist/presets.d.ts +2 -43
  30. package/dist/presets.js +2 -17
  31. package/dist/providers-CX-R-Oy-.js +969 -0
  32. package/dist/providers-CX-R-Oy-.js.map +1 -0
  33. package/dist/providers.d.ts +2 -4
  34. package/dist/providers.js +3 -23
  35. package/dist/session/sqlite.d.ts +7 -12
  36. package/dist/session/sqlite.d.ts.map +1 -0
  37. package/dist/session/sqlite.js +67 -79
  38. package/dist/session/sqlite.js.map +1 -0
  39. package/dist/session-Cn68UASv.js +440 -0
  40. package/dist/session-Cn68UASv.js.map +1 -0
  41. package/dist/session.d.ts +2 -4
  42. package/dist/session.js +3 -27
  43. package/dist/skills.d.ts +3 -322
  44. package/dist/skills.js +24 -47
  45. package/dist/skills.js.map +1 -0
  46. package/dist/stats-DoKUtF5T.js +58 -0
  47. package/dist/stats-DoKUtF5T.js.map +1 -0
  48. package/dist/tools-DpeWKzP1.js +3941 -0
  49. package/dist/tools-DpeWKzP1.js.map +1 -0
  50. package/dist/tools.d.ts +3 -95
  51. package/dist/tools.js +2 -40
  52. package/dist/tui.d.ts +533 -0
  53. package/dist/tui.d.ts.map +1 -0
  54. package/dist/tui.js +2004 -0
  55. package/dist/tui.js.map +1 -0
  56. package/dist/types-Bx_F8jet.js +39 -0
  57. package/dist/types-Bx_F8jet.js.map +1 -0
  58. package/dist/types.d.ts +4 -55
  59. package/dist/types.js +4 -28
  60. package/package.json +38 -4
  61. package/dist/agent-BAHrGtqu.d.ts +0 -2425
  62. package/dist/chunk-4ILGBQ23.js +0 -803
  63. package/dist/chunk-4LPBN547.js +0 -3540
  64. package/dist/chunk-64LLNY7F.js +0 -28
  65. package/dist/chunk-6STZTA4N.js +0 -830
  66. package/dist/chunk-7GQ7P6DM.js +0 -566
  67. package/dist/chunk-IC7FT4OD.js +0 -37
  68. package/dist/chunk-JCOB6IYO.js +0 -22
  69. package/dist/chunk-JH6IAAFA.js +0 -28
  70. package/dist/chunk-LNN5UTS2.js +0 -97
  71. package/dist/chunk-PMCQOMV4.js +0 -490
  72. package/dist/chunk-UD25QF3H.js +0 -304
  73. package/dist/chunk-W57VY6DJ.js +0 -834
  74. package/dist/sandbox-D7v6Wy62.d.ts +0 -28
  75. package/dist/skills-use-DwZrNmcw.d.ts +0 -80
  76. package/dist/types-Bai5rKpa.d.ts +0 -89
  77. package/dist/validation-Pm--dQEU.d.ts +0 -185
@@ -0,0 +1,771 @@
1
+ import { t as toolOutputByteLength } from "./types-Bx_F8jet.js";
2
+ import { Buffer } from "node:buffer";
3
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4
+ import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
5
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { Protocol } from "@modelcontextprotocol/sdk/shared/protocol.js";
8
+ import { InitializeResultSchema, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/sdk/types.js";
9
+ //#region src/mcp/sse-to-json-fetch.ts
10
+ /**
11
+ * `fetch` shim that converts streamable-http POST responses with
12
+ * `Content-Type: text/event-stream` into synthetic `application/json`
13
+ * responses, preserving every original header (notably `mcp-session-id`).
14
+ *
15
+ * Why this exists
16
+ * ----------------
17
+ * The MCP SDK's streamable-http transport handles POST responses two ways
18
+ * depending on `content-type`:
19
+ *
20
+ * - `application/json` → `await response.json()` (works on every runtime).
21
+ * - `text/event-stream` → `body.pipeThrough(TextDecoderStream)
22
+ * .pipeThrough(EventSourceParserStream)`
23
+ * and forward each parsed event to `onmessage`.
24
+ *
25
+ * On Bun + MCP SDK 1.29.x the second pipeline silently drops the parsed
26
+ * event: the full `event: message\ndata: {...}\n\n` arrives intact, the
27
+ * stream closes, but `onmessage` is never called. Bootstrap then waits the
28
+ * full `bootstrapTimeout` (10s default) and the agent loses every tool the
29
+ * server would have exposed.
30
+ *
31
+ * The MCP spec lets clients accept either content type
32
+ * (`Accept: application/json, text/event-stream`), so flipping the stream
33
+ * to JSON at the boundary is a transparent, server-agnostic workaround.
34
+ *
35
+ * Scope of the shim
36
+ * -----------------
37
+ * - POST + `text/event-stream` → drain, parse SSE message events, return
38
+ * a synthetic `application/json` response. Single event becomes a JSON
39
+ * object (matching the shape the SDK expects from non-streaming
40
+ * servers); multiple events become a JSON array (the SDK already
41
+ * iterates `Array.isArray(data)` from a JSON response).
42
+ * - GET (long-lived `_startOrAuthSse` listener) → untouched. Per-event
43
+ * latency matters there and the SSE pipeline isn't always broken on
44
+ * GET in the same way (different code path inside Bun's stream impl).
45
+ * - 202 / non-SSE / malformed SSE / no body → passthrough.
46
+ *
47
+ * Runtime gating
48
+ * --------------
49
+ * `sseToJsonFetchIfNeeded()` only returns a wrapper on Bun. Node + browser
50
+ * runtimes get `undefined` and the SDK uses global `fetch` directly — no
51
+ * extra buffer-and-redrain on the happy path, and no risk of collapsing a
52
+ * future progress-streaming response into a single batched array. If you
53
+ * need to apply the shim unconditionally (testing, custom hosts), call
54
+ * `sseToJsonFetch()` directly.
55
+ *
56
+ * Cleanup
57
+ * -------
58
+ * Remove this file and its `fetch:` injection in `createTransport` once
59
+ * either Bun fixes the `pipeThrough` chain or the SDK switches off the
60
+ * streaming pipeline by default.
61
+ */
62
+ /**
63
+ * Detect whether we're running on Bun. The `Bun` global is set by the
64
+ * runtime itself and isn't faked by Bun's Node-compat layer.
65
+ */
66
+ function isBunRuntime() {
67
+ return typeof globalThis.Bun !== "undefined";
68
+ }
69
+ /**
70
+ * Returns the `sseToJsonFetch` wrapper only on runtimes that need it
71
+ * (currently Bun). Returns `undefined` everywhere else, which makes the
72
+ * SDK fall back to the global `fetch` and keeps the streaming pipeline
73
+ * intact on Node where it works correctly.
74
+ *
75
+ * Designed as the value to pass directly to `StreamableHTTPClientTransport`'s
76
+ * `fetch` option:
77
+ *
78
+ * new StreamableHTTPClientTransport(url, {
79
+ * fetch: sseToJsonFetchIfNeeded(),
80
+ * })
81
+ */
82
+ function sseToJsonFetchIfNeeded() {
83
+ return isBunRuntime() ? sseToJsonFetch() : void 0;
84
+ }
85
+ /**
86
+ * Wrap a `fetch` implementation so streamable-http POST responses that come
87
+ * back as `text/event-stream` are converted to JSON. Pass the result as
88
+ * `opts.fetch` to `StreamableHTTPClientTransport`.
89
+ *
90
+ * Always-on (i.e. unconditional) — for the runtime-gated entry point,
91
+ * use {@link sseToJsonFetchIfNeeded} instead.
92
+ */
93
+ function sseToJsonFetch(baseFetch = fetch) {
94
+ return async function sseToJsonWrappedFetch(input, init) {
95
+ const response = await baseFetch(input, init);
96
+ if ((init?.method ?? "GET").toString().toUpperCase() !== "POST") return response;
97
+ const contentType = response.headers.get("content-type");
98
+ if (!contentType || !contentType.includes("text/event-stream")) return response;
99
+ if (!response.body) return response;
100
+ let raw;
101
+ try {
102
+ raw = await response.text();
103
+ } catch {
104
+ return response;
105
+ }
106
+ const events = parseSseDataEvents(raw);
107
+ return synthesizeJsonResponse(response, events.length === 1 ? events[0] : events);
108
+ };
109
+ }
110
+ /**
111
+ * Parse a buffered SSE body into the JSON payloads of its `message` events.
112
+ *
113
+ * Skips:
114
+ * - SSE comments (lines starting with `:`).
115
+ * - Non-default event types (`event: foo` ≠ `message`). The MCP server
116
+ * only ever emits `message` events for JSON-RPC; anything else is out of
117
+ * band and the SDK wouldn't have surfaced it to `onmessage` either.
118
+ * - Malformed `data:` payloads (anything that fails `JSON.parse`).
119
+ */
120
+ function parseSseDataEvents(raw) {
121
+ const events = [];
122
+ for (const block of raw.split(/\r?\n\r?\n/)) {
123
+ if (!block.trim()) continue;
124
+ const dataLines = [];
125
+ let isMessageEvent = true;
126
+ for (const line of block.split(/\r?\n/)) {
127
+ if (line.startsWith(":")) continue;
128
+ if (line.startsWith("event:")) {
129
+ const eventType = line.slice(6).trim();
130
+ if (eventType && eventType !== "message") isMessageEvent = false;
131
+ } else if (line.startsWith("data:")) {
132
+ const value = line.slice(5);
133
+ dataLines.push(value.startsWith(" ") ? value.slice(1) : value);
134
+ }
135
+ }
136
+ if (!isMessageEvent || dataLines.length === 0) continue;
137
+ try {
138
+ events.push(JSON.parse(dataLines.join("\n")));
139
+ } catch {}
140
+ }
141
+ return events;
142
+ }
143
+ /**
144
+ * Build a `Response` mirroring the original's status / statusText / headers
145
+ * but with a JSON body and `content-type: application/json`. Header
146
+ * preservation is the whole point — `mcp-session-id` is set by the server
147
+ * on the initialize POST and must round-trip into the SDK's
148
+ * `_sessionId` capture (`response.headers.get('mcp-session-id')`).
149
+ */
150
+ function synthesizeJsonResponse(original, payload) {
151
+ const headers = new Headers(original.headers);
152
+ headers.set("content-type", "application/json");
153
+ return new Response(JSON.stringify(payload), {
154
+ status: original.status,
155
+ statusText: original.statusText,
156
+ headers
157
+ });
158
+ }
159
+ //#endregion
160
+ //#region src/mcp/tolerant-client.ts
161
+ /**
162
+ * Drop-in `Client` subclass whose `connect()` mirrors the upstream MCP SDK
163
+ * sequence but treats `notifications/initialized` as best-effort.
164
+ *
165
+ * The MCP spec marks that notification fire-and-forget, but the SDK awaits
166
+ * the underlying transport `send()` and rethrows on any HTTP 4xx. Several
167
+ * real-world streamable-http servers (e.g. browser-codemode, custom MCPs
168
+ * that gate non-initialize routes on a session id that didn't yet exist when
169
+ * the notification posted) reject the notification with a 4xx and still
170
+ * accept every subsequent request. The base `Client.connect()` then closes
171
+ * the transport, the bootstrap fails, and the agent silently loses every
172
+ * tool the server would have exposed.
173
+ *
174
+ * This subclass changes only that single step — log + continue if the
175
+ * notification throws. Initialize, capability/version capture,
176
+ * `setProtocolVersion`, listChanged handler wiring, and the close-on-failure
177
+ * path for everything else are preserved verbatim from the SDK.
178
+ *
179
+ * Should be removed when the SDK lands an opt-in tolerance flag upstream.
180
+ */
181
+ var TolerantMcpClient = class extends Client {
182
+ async connect(transport, options) {
183
+ await Protocol.prototype.connect.call(this, transport);
184
+ if (transport.sessionId !== void 0) return;
185
+ const self = this;
186
+ try {
187
+ const result = await this.request({
188
+ method: "initialize",
189
+ params: {
190
+ protocolVersion: LATEST_PROTOCOL_VERSION,
191
+ capabilities: self._capabilities,
192
+ clientInfo: self._clientInfo
193
+ }
194
+ }, InitializeResultSchema, options);
195
+ if (result === void 0) throw new Error(`Server sent invalid initialize result: ${result}`);
196
+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
197
+ self._serverCapabilities = result.capabilities;
198
+ self._serverVersion = result.serverInfo;
199
+ const setProtocolVersion = transport.setProtocolVersion;
200
+ if (setProtocolVersion) setProtocolVersion.call(transport, result.protocolVersion);
201
+ self._instructions = result.instructions;
202
+ try {
203
+ await this.notification({ method: "notifications/initialized" });
204
+ } catch (notifyError) {
205
+ const msg = notifyError instanceof Error ? notifyError.message : String(notifyError);
206
+ console.warn(`[zidane:mcp] server rejected notifications/initialized (continuing): ${msg}`);
207
+ }
208
+ if (self._pendingListChangedConfig) {
209
+ self._setupListChangedHandlers(self._pendingListChangedConfig);
210
+ self._pendingListChangedConfig = void 0;
211
+ }
212
+ } catch (error) {
213
+ this.close();
214
+ throw error;
215
+ }
216
+ }
217
+ };
218
+ //#endregion
219
+ //#region src/mcp/index.ts
220
+ const DEFAULT_MCP_BOOTSTRAP_TIMEOUT_MS = 1e4;
221
+ function inferTransport(raw) {
222
+ if (raw.transport === "stdio" || raw.transport === "sse" || raw.transport === "streamable-http") return raw.transport;
223
+ if (raw.type === "stdio" || raw.type === "sse" || raw.type === "streamable-http" || raw.type === "http") return raw.type === "http" ? "streamable-http" : raw.type;
224
+ if (raw.command) return "stdio";
225
+ if (raw.httpUrl) return "streamable-http";
226
+ if (raw.sseUrl) return "sse";
227
+ if (raw.url) return "streamable-http";
228
+ throw new Error(`Cannot infer MCP transport from config: ${JSON.stringify(raw)}`);
229
+ }
230
+ function normalizeOne(name, raw) {
231
+ const transport = inferTransport(raw);
232
+ const url = raw.url ?? raw.httpUrl ?? raw.sseUrl;
233
+ const config = {
234
+ name,
235
+ transport
236
+ };
237
+ if (raw.command) config.command = raw.command;
238
+ if (raw.args) config.args = raw.args;
239
+ if (raw.env) config.env = raw.env;
240
+ if (raw.strictEnv === true) config.strictEnv = true;
241
+ if (url) config.url = url;
242
+ if (raw.headers) config.headers = raw.headers;
243
+ if (typeof raw.bootstrapTimeout === "number") config.bootstrapTimeout = raw.bootstrapTimeout;
244
+ if (typeof raw.toolTimeout === "number") config.toolTimeout = raw.toolTimeout;
245
+ if (Array.isArray(raw.enabledTools)) config.enabledTools = raw.enabledTools;
246
+ if (Array.isArray(raw.disabledTools)) config.disabledTools = raw.disabledTools;
247
+ if (typeof raw.toolFilter === "function") config.toolFilter = raw.toolFilter;
248
+ return config;
249
+ }
250
+ /**
251
+ * True when the input looks like a single config object (flat fields like `command`,
252
+ * `transport`, `url`) rather than a record whose values are configs.
253
+ */
254
+ function looksLikeSingleConfig(obj) {
255
+ return [
256
+ "transport",
257
+ "type",
258
+ "command",
259
+ "url",
260
+ "httpUrl",
261
+ "sseUrl"
262
+ ].some((key) => typeof obj[key] === "string");
263
+ }
264
+ /**
265
+ * Normalize MCP server configs from any common shape to `McpServerConfig[]`.
266
+ *
267
+ * Accepts:
268
+ * - `McpServerConfig[]` — zidane native (pass-through).
269
+ * - `McpServerConfig` — a single config object (wrapped to a 1-element array).
270
+ * - `Record<string, RawShape>` — name-keyed map (common in host-SDK configs), where the key is the server name.
271
+ * - Mixed shapes with `type` vs `transport`, `httpUrl`/`sseUrl` vs `url`.
272
+ *
273
+ * Returns `[]` when `input` is nullish. Throws a descriptive error when the transport
274
+ * cannot be inferred from a given entry, or when the input shape is unsupported.
275
+ */
276
+ function normalizeMcpServers(input) {
277
+ if (input == null) return [];
278
+ if (Array.isArray(input)) return input.map((raw, idx) => {
279
+ const obj = raw;
280
+ return normalizeOne(obj.name ?? `mcp_${idx}`, obj);
281
+ });
282
+ if (typeof input === "object") {
283
+ const obj = input;
284
+ if (looksLikeSingleConfig(obj)) {
285
+ const raw = obj;
286
+ return [normalizeOne(raw.name ?? "mcp_0", raw)];
287
+ }
288
+ return Object.entries(obj).map(([name, raw]) => normalizeOne(name, raw ?? {}));
289
+ }
290
+ throw new Error(`Unsupported MCP server config shape: ${typeof input}`);
291
+ }
292
+ /**
293
+ * Lossy flattener — converts MCP `CallToolResult.content` blocks to a single
294
+ * string. Text blocks are extracted; non-text blocks are JSON-stringified.
295
+ *
296
+ * Use this only at UI / log boundaries that require a string. The agent
297
+ * loop itself routes through {@link normalizeMcpBlocks} so image blocks
298
+ * survive into provider-native tool_result content (Anthropic blocks,
299
+ * OpenAI companion-user-message).
300
+ */
301
+ function resultToString(content) {
302
+ if (!content || !Array.isArray(content)) return "";
303
+ return content.map((block) => {
304
+ if (block && typeof block === "object" && block.type === "text") {
305
+ const text = block.text;
306
+ if (typeof text === "string") return text;
307
+ }
308
+ return JSON.stringify(block);
309
+ }).join("\n");
310
+ }
311
+ /**
312
+ * Normalize MCP `CallToolResult.content` to zidane's {@link ToolResultContent[]} shape.
313
+ *
314
+ * Handles the four MCP content block types:
315
+ * - `text` → preserved as `{type:'text', text}`
316
+ * - `image` → preserved as `{type:'image', mediaType, data}` (MCP uses `mimeType`)
317
+ * - `resource` with embedded text → flattened to a text block
318
+ * - `resource` with embedded blob whose `mimeType` is `image/*` → flattened to an image block
319
+ * - Any unrecognized block → JSON-stringified fallback text block (lossy but safe)
320
+ *
321
+ * Returns `null` when the input is not an array — callers should fall back to an empty
322
+ * result in that case.
323
+ */
324
+ function normalizeMcpBlocks(content) {
325
+ if (!Array.isArray(content)) return null;
326
+ const out = [];
327
+ for (const raw of content) {
328
+ if (!raw || typeof raw !== "object") continue;
329
+ const block = raw;
330
+ if (block.type === "text" && typeof block.text === "string") {
331
+ out.push({
332
+ type: "text",
333
+ text: block.text
334
+ });
335
+ continue;
336
+ }
337
+ if (block.type === "image" && typeof block.data === "string") {
338
+ const mediaType = typeof block.mimeType === "string" ? block.mimeType : typeof block.mediaType === "string" ? block.mediaType : "image/png";
339
+ out.push({
340
+ type: "image",
341
+ mediaType,
342
+ data: block.data
343
+ });
344
+ continue;
345
+ }
346
+ if (block.type === "resource" && block.resource && typeof block.resource === "object") {
347
+ const res = block.resource;
348
+ if (typeof res.text === "string") {
349
+ out.push({
350
+ type: "text",
351
+ text: res.text
352
+ });
353
+ continue;
354
+ }
355
+ if (typeof res.blob === "string" && typeof res.mimeType === "string" && res.mimeType.startsWith("image/")) {
356
+ out.push({
357
+ type: "image",
358
+ mediaType: res.mimeType,
359
+ data: res.blob
360
+ });
361
+ continue;
362
+ }
363
+ }
364
+ out.push({
365
+ type: "text",
366
+ text: JSON.stringify(block)
367
+ });
368
+ }
369
+ return out;
370
+ }
371
+ /**
372
+ * Route the MCP result content through the narrowest appropriate zidane shape:
373
+ *
374
+ * - All blocks are `text` → return a joined string (smaller wire payload,
375
+ * string-friendly for hook consumers that don't need structured access).
376
+ * - Any block is non-text → return a structured `ToolResultContent[]`.
377
+ * - Empty / non-array input → return `''`.
378
+ */
379
+ function packMcpResult(content) {
380
+ const normalized = normalizeMcpBlocks(content);
381
+ if (!normalized || normalized.length === 0) return "";
382
+ const parts = [];
383
+ for (const block of normalized) {
384
+ if (block.type !== "text") return normalized;
385
+ parts.push(block.text);
386
+ }
387
+ return parts.join("\n");
388
+ }
389
+ /**
390
+ * Create the appropriate MCP transport for a server config.
391
+ *
392
+ * For stdio: when `config.env` is provided, it is merged on top of the MCP SDK's
393
+ * `getDefaultEnvironment()` whitelist (`PATH`, `HOME`, `LANG`, `SHELL`, `USER` on
394
+ * POSIX; `APPDATA`, `PATH`, ... on Win32). Without this defensive merge, older
395
+ * MCP SDK versions strip `PATH` the moment a consumer sets any env, breaking
396
+ * `spawn('node', ...)` with ENOENT. Pass `strictEnv: true` to opt out and send
397
+ * `env` verbatim.
398
+ */
399
+ function createTransport(config) {
400
+ switch (config.transport) {
401
+ case "stdio": {
402
+ const mergedEnv = config.env && !config.strictEnv ? {
403
+ ...getDefaultEnvironment(),
404
+ ...config.env
405
+ } : config.env;
406
+ return new StdioClientTransport({
407
+ command: config.command,
408
+ args: config.args,
409
+ env: mergedEnv
410
+ });
411
+ }
412
+ case "sse": return new SSEClientTransport(new URL(config.url), { requestInit: config.headers ? { headers: config.headers } : void 0 });
413
+ case "streamable-http": return new StreamableHTTPClientTransport(new URL(config.url), {
414
+ requestInit: config.headers ? { headers: config.headers } : void 0,
415
+ fetch: sseToJsonFetchIfNeeded()
416
+ });
417
+ default: throw new Error(`Unknown MCP transport: ${config.transport}`);
418
+ }
419
+ }
420
+ /**
421
+ * Connect to MCP servers and discover their tools.
422
+ *
423
+ * Each tool is namespaced as `mcp_{serverName}_{toolName}` to avoid
424
+ * collisions with agent tools or tools from other servers.
425
+ *
426
+ * @param configs - Array of MCP server configurations
427
+ * @param _clientFactory - Internal: override client construction for testing
428
+ * @param hooks - Optional agent hooks for firing mcp:connect, mcp:error, mcp:close events
429
+ */
430
+ async function connectMcpServers(configs, _clientFactory, hooks) {
431
+ const connections = [];
432
+ const tools = {};
433
+ const errors = [];
434
+ let closed = false;
435
+ const bootstrapResults = await Promise.all(configs.map((config) => bootstrapServer(config, _clientFactory, hooks)));
436
+ for (const result of bootstrapResults) {
437
+ if (!result.ok) {
438
+ errors.push({
439
+ name: result.name,
440
+ error: result.error
441
+ });
442
+ await hooks?.callHook("mcp:error", {
443
+ name: result.name,
444
+ error: result.error
445
+ });
446
+ continue;
447
+ }
448
+ connections.push({
449
+ name: result.name,
450
+ client: result.client
451
+ });
452
+ const toolNames = [];
453
+ for (const tool of result.tools) {
454
+ const namespacedName = `mcp_${result.config.name}_${tool.name}`;
455
+ toolNames.push(namespacedName);
456
+ tools[namespacedName] = buildMcpToolDef(result.config, result.client, tool, namespacedName, hooks);
457
+ }
458
+ await hooks?.callHook("mcp:connect", {
459
+ name: result.name,
460
+ transport: result.config.transport,
461
+ tools: toolNames
462
+ });
463
+ }
464
+ if (errors.length > 0 && connections.length === 0) {
465
+ const messages = errors.map((e) => `${e.name}: ${e.error.message}`).join(", ");
466
+ throw new Error(`All MCP servers failed to connect: ${messages}`);
467
+ }
468
+ return {
469
+ tools,
470
+ close: async () => {
471
+ if (closed) return;
472
+ closed = true;
473
+ await Promise.allSettled(connections.map(async ({ name, client }) => {
474
+ await hooks?.callHook("mcp:close", { name });
475
+ await client.close();
476
+ }));
477
+ }
478
+ };
479
+ }
480
+ /**
481
+ * Connect one MCP server and list its tools, wrapped in a single race against
482
+ * `config.bootstrapTimeout` (default 10s). Always emits `mcp:bootstrap:start`
483
+ * before network I/O and `mcp:bootstrap:end` with `durationMs` + outcome, so a
484
+ * host can build a timing view even when every server succeeds.
485
+ *
486
+ * Errors are captured into the `{ ok: false }` result rather than thrown — the
487
+ * parent uses `Promise.all` across every server and we can't let one rejection
488
+ * short-circuit the batch.
489
+ */
490
+ async function bootstrapServer(config, _clientFactory, hooks) {
491
+ const start = Date.now();
492
+ if (config.enabledTools && config.disabledTools) {
493
+ const error = /* @__PURE__ */ new Error(`MCP server "${config.name}": enabledTools and disabledTools are mutually exclusive — set one or the other, not both.`);
494
+ await hooks?.callHook("mcp:bootstrap:start", {
495
+ name: config.name,
496
+ transport: config.transport
497
+ });
498
+ await hooks?.callHook("mcp:bootstrap:end", {
499
+ name: config.name,
500
+ transport: config.transport,
501
+ durationMs: 0,
502
+ ok: false,
503
+ error
504
+ });
505
+ return {
506
+ ok: false,
507
+ name: config.name,
508
+ error
509
+ };
510
+ }
511
+ await hooks?.callHook("mcp:bootstrap:start", {
512
+ name: config.name,
513
+ transport: config.transport
514
+ });
515
+ let client = null;
516
+ try {
517
+ client = _clientFactory ? _clientFactory() : new TolerantMcpClient({
518
+ name: "zidane",
519
+ version: "1.0.0"
520
+ });
521
+ const currentClient = client;
522
+ const transport = createTransport(config);
523
+ const bootstrapTimeout = config.bootstrapTimeout ?? DEFAULT_MCP_BOOTSTRAP_TIMEOUT_MS;
524
+ const { tools: mcpTools } = await raceWithTimeout(async () => {
525
+ await currentClient.connect(transport);
526
+ return await currentClient.listTools();
527
+ }, bootstrapTimeout, `MCP server "${config.name}" bootstrap timed out after ${bootstrapTimeout}ms`);
528
+ const filteredTools = await applyMcpToolFilters(config, mcpTools, hooks);
529
+ const durationMs = Date.now() - start;
530
+ await hooks?.callHook("mcp:bootstrap:end", {
531
+ name: config.name,
532
+ transport: config.transport,
533
+ durationMs,
534
+ ok: true,
535
+ toolCount: filteredTools.length
536
+ });
537
+ return {
538
+ ok: true,
539
+ name: config.name,
540
+ config,
541
+ client: currentClient,
542
+ tools: filteredTools
543
+ };
544
+ } catch (err) {
545
+ const error = err instanceof Error ? err : new Error(String(err));
546
+ await closeClientQuietly(client);
547
+ const durationMs = Date.now() - start;
548
+ await hooks?.callHook("mcp:bootstrap:end", {
549
+ name: config.name,
550
+ transport: config.transport,
551
+ durationMs,
552
+ ok: false,
553
+ error
554
+ });
555
+ return {
556
+ ok: false,
557
+ name: config.name,
558
+ error
559
+ };
560
+ }
561
+ }
562
+ /**
563
+ * Apply config-side filters (`enabledTools` / `disabledTools` / `toolFilter`)
564
+ * and then the `mcp:tools:filter` hook to the upstream tool list.
565
+ *
566
+ * Composition order — narrowest-to-widest:
567
+ * 1. Allow-list (`enabledTools`) — keep only listed tools.
568
+ * 2. Deny-list (`disabledTools`) — drop listed tools.
569
+ * 3. Predicate (`toolFilter`) — fine-grained metadata filtering.
570
+ * 4. Hook (`mcp:tools:filter`) — runtime / per-host decisions.
571
+ *
572
+ * The mutual exclusion of `enabledTools` and `disabledTools` is checked in
573
+ * `bootstrapServer` so this stays a pure data transform.
574
+ */
575
+ async function applyMcpToolFilters(config, tools, hooks) {
576
+ let filtered = tools;
577
+ if (config.enabledTools && config.enabledTools.length > 0) {
578
+ const allow = new Set(config.enabledTools);
579
+ filtered = filtered.filter((t) => allow.has(t.name));
580
+ }
581
+ if (config.disabledTools && config.disabledTools.length > 0) {
582
+ const deny = new Set(config.disabledTools);
583
+ filtered = filtered.filter((t) => !deny.has(t.name));
584
+ }
585
+ if (config.toolFilter) {
586
+ const predicate = config.toolFilter;
587
+ filtered = filtered.filter((t) => predicate(t));
588
+ }
589
+ if (hooks) {
590
+ const ctx = {
591
+ server: config.name,
592
+ transport: config.transport,
593
+ tools: [...filtered]
594
+ };
595
+ await hooks.callHook("mcp:tools:filter", ctx);
596
+ filtered = ctx.tools;
597
+ }
598
+ return filtered;
599
+ }
600
+ /**
601
+ * Build the zidane `ToolDef` that wraps a single MCP tool. Extracted so the
602
+ * parallel bootstrap path can assemble tools from a results array without
603
+ * inlining a 70-line closure inside the collector loop.
604
+ *
605
+ * The returned `execute` closes over the bootstrapped `client` — reconnects /
606
+ * swap-outs must go through a fresh `connectMcpServers` call rather than
607
+ * rewiring the tool in place.
608
+ */
609
+ function buildMcpToolDef(config, client, tool, namespacedName, hooks) {
610
+ return {
611
+ spec: {
612
+ name: namespacedName,
613
+ description: tool.description || "",
614
+ inputSchema: tool.inputSchema ?? {
615
+ type: "object",
616
+ properties: {}
617
+ }
618
+ },
619
+ execute: async (input, ctx) => {
620
+ const { turnId, callId, signal } = ctx;
621
+ const displayName = ctx.toolAliases?.[namespacedName] ?? namespacedName;
622
+ const gateCtx = {
623
+ turnId,
624
+ callId,
625
+ server: config.name,
626
+ tool: tool.name,
627
+ displayName,
628
+ input,
629
+ block: false,
630
+ reason: "MCP tool execution was blocked"
631
+ };
632
+ await hooks?.callHook("mcp:tool:gate", gateCtx);
633
+ if (gateCtx.block) return `Blocked: ${gateCtx.reason}`;
634
+ const effectiveInput = gateCtx.input;
635
+ if (gateCtx.result !== void 0) {
636
+ let substitute = gateCtx.result;
637
+ const transformCtx = {
638
+ turnId,
639
+ callId,
640
+ server: config.name,
641
+ tool: tool.name,
642
+ displayName,
643
+ input: effectiveInput,
644
+ result: substitute,
645
+ outputBytes: toolOutputByteLength(substitute)
646
+ };
647
+ await hooks?.callHook("mcp:tool:transform", transformCtx);
648
+ substitute = transformCtx.result;
649
+ await hooks?.callHook("mcp:tool:after", {
650
+ turnId,
651
+ callId,
652
+ server: config.name,
653
+ tool: tool.name,
654
+ displayName,
655
+ input: effectiveInput,
656
+ result: substitute,
657
+ outputBytes: toolOutputByteLength(substitute)
658
+ });
659
+ return substitute;
660
+ }
661
+ await hooks?.callHook("mcp:tool:before", {
662
+ turnId,
663
+ callId,
664
+ server: config.name,
665
+ tool: tool.name,
666
+ displayName,
667
+ input: effectiveInput
668
+ });
669
+ const timeout = config.toolTimeout ?? 3e4;
670
+ try {
671
+ let output = packMcpResult((await raceWithTimeoutAndSignal(() => client.callTool({
672
+ name: tool.name,
673
+ arguments: effectiveInput
674
+ }), timeout, `MCP tool "${tool.name}" on server "${config.name}" timed out after ${timeout}ms`, signal)).content);
675
+ const transformCtx = {
676
+ turnId,
677
+ callId,
678
+ server: config.name,
679
+ tool: tool.name,
680
+ displayName,
681
+ input: effectiveInput,
682
+ result: output,
683
+ outputBytes: toolOutputByteLength(output)
684
+ };
685
+ await hooks?.callHook("mcp:tool:transform", transformCtx);
686
+ output = transformCtx.result;
687
+ await hooks?.callHook("mcp:tool:after", {
688
+ turnId,
689
+ callId,
690
+ server: config.name,
691
+ tool: tool.name,
692
+ displayName,
693
+ input: effectiveInput,
694
+ result: output,
695
+ outputBytes: toolOutputByteLength(output)
696
+ });
697
+ return output;
698
+ } catch (err) {
699
+ const error = err instanceof Error ? err : new Error(String(err));
700
+ await hooks?.callHook("mcp:tool:error", {
701
+ turnId,
702
+ callId,
703
+ server: config.name,
704
+ tool: tool.name,
705
+ displayName,
706
+ input: effectiveInput,
707
+ error
708
+ });
709
+ await hooks?.callHook("mcp:tool:after", {
710
+ turnId,
711
+ callId,
712
+ server: config.name,
713
+ tool: tool.name,
714
+ displayName,
715
+ input: effectiveInput,
716
+ result: error.message,
717
+ outputBytes: Buffer.byteLength(error.message)
718
+ });
719
+ throw error;
720
+ }
721
+ }
722
+ };
723
+ }
724
+ async function closeClientQuietly(client) {
725
+ if (!client) return;
726
+ try {
727
+ await client.close();
728
+ } catch {}
729
+ }
730
+ async function raceWithTimeout(task, timeoutMs, timeoutMessage) {
731
+ let timer;
732
+ try {
733
+ return await new Promise((resolvePromise, rejectPromise) => {
734
+ timer = setTimeout(() => rejectPromise(new Error(timeoutMessage)), timeoutMs);
735
+ task().then(resolvePromise, rejectPromise);
736
+ });
737
+ } finally {
738
+ if (timer) clearTimeout(timer);
739
+ }
740
+ }
741
+ /**
742
+ * Race a promise-returning task against (a) a timeout and (b) an optional
743
+ * abort signal. Cleans up its timer and abort listener on every exit path.
744
+ *
745
+ * The task itself isn't cancellable (the MCP SDK doesn't take a signal), so
746
+ * the rejection unblocks the tool call while the underlying RPC completes in
747
+ * the background. When the agent subsequently calls `connection.close()` the
748
+ * stdio transport kills the subprocess, which clears any stuck in-flight work.
749
+ */
750
+ async function raceWithTimeoutAndSignal(task, timeoutMs, timeoutMessage, signal) {
751
+ if (signal?.aborted) throw new Error("MCP tool call aborted");
752
+ let timer;
753
+ let onAbort;
754
+ try {
755
+ return await new Promise((resolvePromise, rejectPromise) => {
756
+ timer = setTimeout(() => rejectPromise(new Error(timeoutMessage)), timeoutMs);
757
+ if (signal) {
758
+ onAbort = () => rejectPromise(/* @__PURE__ */ new Error("MCP tool call aborted"));
759
+ signal.addEventListener("abort", onAbort, { once: true });
760
+ }
761
+ task().then(resolvePromise, rejectPromise);
762
+ });
763
+ } finally {
764
+ if (timer) clearTimeout(timer);
765
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
766
+ }
767
+ }
768
+ //#endregion
769
+ export { resultToString as i, normalizeMcpBlocks as n, normalizeMcpServers as r, connectMcpServers as t };
770
+
771
+ //# sourceMappingURL=mcp-8wClKY-3.js.map