poe-code 8.0.1 → 9.0.1

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 (46) hide show
  1. package/dist/cli/commands/harness.js +28 -3
  2. package/dist/cli/commands/harness.js.map +1 -1
  3. package/dist/index.js +1516 -711
  4. package/dist/index.js.map +4 -4
  5. package/dist/metafile.json +1 -1
  6. package/package.json +1 -1
  7. package/packages/safejs/dist/chunks/chunk-4YIDX44G.js +40 -0
  8. package/packages/safejs/dist/chunks/chunk-4YIDX44G.js.map +7 -0
  9. package/packages/safejs/dist/chunks/chunk-55ZK3CR7.js +7802 -0
  10. package/packages/safejs/dist/chunks/chunk-55ZK3CR7.js.map +7 -0
  11. package/packages/safejs/dist/chunks/{chunk-ME5BTAZT.js → chunk-GQBEEFVQ.js} +150 -19
  12. package/packages/safejs/dist/chunks/{chunk-ME5BTAZT.js.map → chunk-GQBEEFVQ.js.map} +4 -4
  13. package/packages/safejs/dist/chunks/chunk-NI4WFFTL.js +5701 -0
  14. package/packages/safejs/dist/chunks/chunk-NI4WFFTL.js.map +7 -0
  15. package/packages/safejs/dist/chunks/inMemory-RLZ4QGDZ.js +48 -0
  16. package/packages/safejs/dist/chunks/inMemory-RLZ4QGDZ.js.map +7 -0
  17. package/packages/safejs/dist/chunks/server-3SNT3RE2.js +12518 -0
  18. package/packages/safejs/dist/chunks/server-3SNT3RE2.js.map +7 -0
  19. package/packages/safejs/dist/chunks/types-BTF37PZ6.js +348 -0
  20. package/packages/safejs/dist/chunks/types-BTF37PZ6.js.map +7 -0
  21. package/packages/safejs/dist/cli.d.ts +2 -0
  22. package/packages/safejs/dist/cli.js +26 -2
  23. package/packages/safejs/dist/cli.js.map +2 -2
  24. package/packages/safejs/dist/core.js +2 -1
  25. package/packages/safejs/dist/index.d.ts +3 -0
  26. package/packages/safejs/dist/index.js +6 -287
  27. package/packages/safejs/dist/index.js.map +4 -4
  28. package/packages/safejs/dist/interp/host-bridge.d.ts +2 -0
  29. package/packages/safejs/dist/interp/host-bridge.js +20 -9
  30. package/packages/safejs/dist/interp/host-call.d.ts +5 -0
  31. package/packages/safejs/dist/interp/host-call.js +48 -3
  32. package/packages/safejs/dist/interp/resources.d.ts +7 -0
  33. package/packages/safejs/dist/interp/resources.js +36 -0
  34. package/packages/safejs/dist/modules/mcp-transport.d.ts +26 -0
  35. package/packages/safejs/dist/modules/mcp-transport.js +259 -0
  36. package/packages/safejs/dist/modules/mcp.d.ts +15 -1
  37. package/packages/safejs/dist/modules/mcp.js +107 -0
  38. package/packages/safejs/dist/run.js +6 -3
  39. package/packages/safejs/dist/snapshot/dump-format.d.ts +1 -1
  40. package/packages/safejs/dist/snapshot/dump-format.js +1 -1
  41. package/packages/safejs/dist/snapshot/replay-data.d.ts +1 -0
  42. package/packages/safejs/dist/snapshot/replay-data.js +3 -1
  43. package/packages/safejs/dist/snapshot/replay-inputs.d.ts +2 -2
  44. package/packages/safejs/dist/snapshot/replay-inputs.js +2 -1
  45. package/packages/safejs/dist/chunks/chunk-AVDRLE3Y.js +0 -3461
  46. package/packages/safejs/dist/chunks/chunk-AVDRLE3Y.js.map +0 -7
@@ -29,18 +29,19 @@ export function readHostOperationPolicy(operation) {
29
29
  return hostOperationPolicies.get(operation);
30
30
  }
31
31
  export function wrapCallerInjectedBindings(bindings, options) {
32
+ options = { ...options, registerCapabilities: true };
32
33
  const state = { seen: new WeakMap() };
33
34
  const copied = Object.fromEntries(Object.entries(bindings).map(([name, value]) => [
34
35
  name,
35
36
  typeof value === "function"
36
- ? wrapCallerInjectedFunction(name, value, options, state)
37
- : copyHostValueToSandbox(value, [], { ...options, operation: name }, state, "<root>")
37
+ ? wrapCallerInjectedFunction(name, value, { ...options, capabilityPath: [name] }, state)
38
+ : copyHostValueToSandbox(value, [], { ...options, operation: name, capabilityPath: [name] }, state, "<root>")
38
39
  ]));
39
40
  options.budget.provisionDataUsage(measureSandboxData(Object.values(copied)));
40
41
  return copied;
41
42
  }
42
43
  function wrapCallerInjectedFunction(name, value, options, state) {
43
- const existing = state.seen.get(value);
44
+ const existing = state.seen.get(value) ?? options.hostCalls?.nativeClosures.get(value);
44
45
  if (existing !== undefined)
45
46
  return existing;
46
47
  const bindingName = name === "default" && value.name.length > 0 ? value.name : name;
@@ -132,6 +133,12 @@ function wrapCallerInjectedFunction(name, value, options, state) {
132
133
  name: bindingName,
133
134
  properties: (closure) => {
134
135
  state.seen.set(value, closure);
136
+ if (options.registerCapabilities) {
137
+ options.hostCalls?.registerHostCapability(JSON.stringify([
138
+ options.moduleId ?? "<bindings>",
139
+ ...(options.capabilityPath ?? [bindingName])
140
+ ]), closure, value);
141
+ }
135
142
  return (copyFunctionProperties(callable, [], options, state, options.operation ?? bindingName) ?? {});
136
143
  }
137
144
  });
@@ -316,7 +323,7 @@ function isAbortReason(reason) {
316
323
  ("code" in reason && reason.code === "aborted")));
317
324
  }
318
325
  function copyHostResultToSandbox(result, stackFrames, options) {
319
- const value = copyHostValueToSandbox(result, stackFrames, options, {
326
+ const value = copyHostValueToSandbox(result, stackFrames, { ...options, registerCapabilities: false }, {
320
327
  seen: new WeakMap()
321
328
  }, "<root>");
322
329
  options.budget.provisionDataUsage(measureSandboxData([value]));
@@ -603,7 +610,7 @@ function copyHostValueToSandbox(value, stackFrames, options, state, path) {
603
610
  if (!("value" in descriptor)) {
604
611
  throw new TypeError(`Unsupported sandbox value at ${path}[${index}]: accessor property`);
605
612
  }
606
- copy[index] = copyHostValueToSandbox(descriptor.value, stackFrames, options, state, `${path}[${index}]`);
613
+ copy[index] = copyHostValueToSandbox(descriptor.value, stackFrames, { ...options, capabilityPath: [...(options.capabilityPath ?? []), String(index)] }, state, `${path}[${index}]`);
607
614
  }
608
615
  return copy;
609
616
  }
@@ -616,7 +623,8 @@ function copyHostValueToSandbox(value, stackFrames, options, state, path) {
616
623
  state.seen.set(value, copy);
617
624
  budget.allocateCollectionEntries(value.size);
618
625
  for (const [key, entry] of value) {
619
- copy.entries.set(copyHostValueToSandbox(key, stackFrames, options, state, `${path}.<key>`), copyHostValueToSandbox(entry, stackFrames, options, state, `${path}.<value>`));
626
+ const ordinal = copy.entries.size;
627
+ copy.entries.set(copyHostValueToSandbox(key, stackFrames, { ...options, capabilityPath: [...(options.capabilityPath ?? []), `key:${ordinal}`] }, state, `${path}.<key>`), copyHostValueToSandbox(entry, stackFrames, { ...options, capabilityPath: [...(options.capabilityPath ?? []), `value:${ordinal}`] }, state, `${path}.<value>`));
620
628
  }
621
629
  return copy;
622
630
  }
@@ -629,7 +637,10 @@ function copyHostValueToSandbox(value, stackFrames, options, state, path) {
629
637
  state.seen.set(value, copy);
630
638
  budget.allocateCollectionEntries(value.size);
631
639
  for (const entry of value) {
632
- copy.values.add(copyHostValueToSandbox(entry, stackFrames, options, state, `${path}.<value>`));
640
+ copy.values.add(copyHostValueToSandbox(entry, stackFrames, {
641
+ ...options,
642
+ capabilityPath: [...(options.capabilityPath ?? []), String(copy.values.size)]
643
+ }, state, `${path}.<value>`));
633
644
  }
634
645
  return copy;
635
646
  }
@@ -653,7 +664,7 @@ function copyHostValueToSandbox(value, stackFrames, options, state, path) {
653
664
  enumerable: true,
654
665
  configurable: true,
655
666
  writable: true,
656
- value: copyHostValueToSandbox(descriptor.value, stackFrames, options, state, joinPath(path, key))
667
+ value: copyHostValueToSandbox(descriptor.value, stackFrames, { ...options, capabilityPath: [...(options.capabilityPath ?? []), key] }, state, joinPath(path, key))
657
668
  });
658
669
  }
659
670
  return copy;
@@ -725,7 +736,7 @@ function copyFunctionProperties(callable, stackFrames, options, state, path) {
725
736
  if (typeof descriptor.value !== "function") {
726
737
  continue;
727
738
  }
728
- properties[key] = copyHostValueToSandbox(descriptor.value, stackFrames, options, state, joinPath(path, key));
739
+ properties[key] = copyHostValueToSandbox(descriptor.value, stackFrames, { ...options, capabilityPath: [...(options.capabilityPath ?? []), key] }, state, joinPath(path, key));
729
740
  }
730
741
  return Object.keys(properties).length > 0 ? properties : undefined;
731
742
  }
@@ -83,8 +83,10 @@ export declare class HostCallJournal {
83
83
  private retainedSize;
84
84
  private readonly outcomeSizes;
85
85
  private readonly capabilities;
86
+ private readonly requiredHostCapabilities;
86
87
  private readonly capabilityIds;
87
88
  readonly nativeClosures: WeakMap<object, SandboxClosure>;
89
+ private readonly hostSources;
88
90
  private readonly encodedOutcomes;
89
91
  private readonly callbackSizes;
90
92
  private readonly completedCallbackOwners;
@@ -110,6 +112,9 @@ export declare class HostCallJournal {
110
112
  snapshot(): HostCallRecord[];
111
113
  dispose(): void;
112
114
  registerCallbackFunction(record: HostCallRecord, id: number, closure: SandboxClosure, native: object): void;
115
+ registerHostCapability(path: string, closure: SandboxClosure, native: object): void;
116
+ rebindHostCapability(original: SandboxClosure, restored: SandboxClosure): void;
117
+ validateHostCapabilities(): void;
113
118
  waitForCapability(id: string): Promise<void>;
114
119
  trackCallbackCompletion(record: HostCallRecord, callbacks: readonly Promise<unknown>[]): void;
115
120
  recordCallback(record: HostCallRecord, id: number, args: SandboxValue[], step: number): string;
@@ -36,8 +36,10 @@ export class HostCallJournal {
36
36
  retainedSize = 0;
37
37
  outcomeSizes = new Map();
38
38
  capabilities = new Map();
39
+ requiredHostCapabilities = new Set();
39
40
  capabilityIds = new WeakMap();
40
41
  nativeClosures = new WeakMap();
42
+ hostSources = new WeakMap();
41
43
  encodedOutcomes = new Map();
42
44
  callbackSizes = new Map();
43
45
  completedCallbackOwners = new Set();
@@ -55,7 +57,7 @@ export class HostCallJournal {
55
57
  this.budget = budget;
56
58
  this.recordedReplay = replay !== undefined;
57
59
  if (replay !== undefined) {
58
- const replayRecords = restoreReplayCalls(replay, this.encodedOutcomes, this.callbackSizes);
60
+ const replayRecords = restoreReplayCalls(replay, this.encodedOutcomes, this.callbackSizes, this.requiredHostCapabilities);
59
61
  const restoredRunId = replayRecords[0]?.runId ?? records[0]?.runId ?? randomUUID();
60
62
  validateRestoredRecords(records, restoredRunId, sourceHash);
61
63
  for (const record of records) {
@@ -221,6 +223,34 @@ export class HostCallJournal {
221
223
  this.capabilityIds.set(closure, identity);
222
224
  this.nativeClosures.set(native, closure);
223
225
  }
226
+ registerHostCapability(path, closure, native) {
227
+ const identity = `host:${path}`;
228
+ const existing = this.capabilities.get(identity);
229
+ if (existing !== undefined && existing !== closure)
230
+ throw new TypeError(`Conflicting replay capability '${identity}'.`);
231
+ this.capabilities.set(identity, closure);
232
+ if (!this.capabilityIds.has(closure))
233
+ this.capabilityIds.set(closure, identity);
234
+ if (!this.nativeClosures.has(native))
235
+ this.nativeClosures.set(native, closure);
236
+ this.hostSources.set(closure, native);
237
+ }
238
+ rebindHostCapability(original, restored) {
239
+ const identity = this.capabilityIds.get(original);
240
+ const source = this.hostSources.get(original);
241
+ if (identity === undefined || source === undefined)
242
+ return;
243
+ this.capabilities.set(identity, restored);
244
+ this.capabilityIds.set(restored, identity);
245
+ this.nativeClosures.set(source, restored);
246
+ this.hostSources.set(restored, source);
247
+ }
248
+ validateHostCapabilities() {
249
+ for (const identity of this.requiredHostCapabilities) {
250
+ if (!this.capabilities.has(identity))
251
+ throw new UnresolvedReplayCapabilityError(identity);
252
+ }
253
+ }
224
254
  waitForCapability(id) {
225
255
  if (this.capabilities.has(id))
226
256
  return Promise.resolve();
@@ -338,7 +368,7 @@ function copyOutcome(outcome) {
338
368
  ? { status: "fulfilled", value: deepCopyToSandbox(outcome.value) }
339
369
  : { status: "rejected", reason: deepCopyToSandbox(outcome.reason) };
340
370
  }
341
- function restoreReplayCalls(input, encodedOutcomes, callbackSizes) {
371
+ function restoreReplayCalls(input, encodedOutcomes, callbackSizes, requiredHostCapabilities = new Set()) {
342
372
  validateSnapshotData(input);
343
373
  if (input === null ||
344
374
  typeof input !== "object" ||
@@ -364,7 +394,22 @@ function restoreReplayCalls(input, encodedOutcomes, callbackSizes) {
364
394
  }));
365
395
  }
366
396
  }
367
- const resolveCapability = (id) => capabilities.get(id);
397
+ const resolveCapability = (id) => {
398
+ if (!capabilities.has(id) && id.startsWith("host:")) {
399
+ const path = JSON.parse(id.slice(5));
400
+ if (!Array.isArray(path) ||
401
+ path.length < 2 ||
402
+ path.some((value) => typeof value !== "string"))
403
+ throw new TypeError("Invalid host capability identity.");
404
+ requiredHostCapabilities.add(id);
405
+ capabilities.set(id, createSandboxClosure({
406
+ call: () => {
407
+ throw new TypeError("Host capability has not been reconstructed.");
408
+ }
409
+ }));
410
+ }
411
+ return capabilities.get(id);
412
+ };
368
413
  return input.calls.map((entry, index) => {
369
414
  if (entry === null || typeof entry !== "object" || Array.isArray(entry))
370
415
  throw new TypeError("Invalid replay call.");
@@ -0,0 +1,7 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export type RunResources = {
3
+ signal: AbortSignal;
4
+ add(close: () => Promise<void>): void;
5
+ };
6
+ export declare const runResources: AsyncLocalStorage<RunResources>;
7
+ export declare function withRunResources<Result>(signal: AbortSignal | undefined, execute: () => Promise<Result>): Promise<Result>;
@@ -0,0 +1,36 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export const runResources = new AsyncLocalStorage();
3
+ export async function withRunResources(signal, execute) {
4
+ const controller = new AbortController();
5
+ const cancel = () => controller.abort(signal?.reason);
6
+ const cleanups = new Set();
7
+ const resources = {
8
+ signal: controller.signal,
9
+ add(close) {
10
+ cleanups.add(close);
11
+ }
12
+ };
13
+ signal?.addEventListener("abort", cancel, { once: true });
14
+ if (signal?.aborted)
15
+ cancel();
16
+ let result;
17
+ let failure;
18
+ let errors = [];
19
+ try {
20
+ result = await runResources.run(resources, execute);
21
+ }
22
+ catch (error) {
23
+ failure = { reason: error };
24
+ }
25
+ finally {
26
+ signal?.removeEventListener("abort", cancel);
27
+ controller.abort(new Error("SafeJS run finished."));
28
+ const outcomes = await Promise.allSettled([...cleanups].map((close) => Promise.resolve().then(close)));
29
+ errors = outcomes.flatMap((outcome) => (outcome.status === "rejected" ? [outcome.reason] : []));
30
+ }
31
+ if (failure !== undefined)
32
+ throw failure.reason;
33
+ if (errors.length > 0)
34
+ throw new AggregateError(errors, "SafeJS resource cleanup failed.");
35
+ return result;
36
+ }
@@ -0,0 +1,26 @@
1
+ import { McpClient, type HttpTransportFetch, type StdioSpawn } from "../../../tiny-mcp-client/dist/index.js";
2
+ export type McpServerConfig = {
3
+ command: string;
4
+ args?: string[];
5
+ cwd?: string;
6
+ env?: Record<string, string>;
7
+ } | {
8
+ url: string;
9
+ headers?: Record<string, string>;
10
+ };
11
+ export type McpModuleOptions = {
12
+ servers: Record<string, McpServerConfig>;
13
+ requestTimeoutMs?: number;
14
+ closeTimeoutMs?: number;
15
+ maxToolPages?: number;
16
+ signal?: AbortSignal;
17
+ fetch?: HttpTransportFetch;
18
+ spawn?: StdioSpawn;
19
+ };
20
+ export type ManagedMcpConnection = {
21
+ ready: Promise<McpClient>;
22
+ close(): Promise<void>;
23
+ };
24
+ export declare function parseMcpConfig(source: string, directory: string): McpModuleOptions;
25
+ export declare function normalizeMcpOptions(value: unknown, keys?: string[]): McpModuleOptions;
26
+ export declare function connectMcpTransport(server: McpServerConfig, options: McpModuleOptions, signal?: AbortSignal): ManagedMcpConnection;
@@ -0,0 +1,259 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { HttpTransport, McpClient, StdioTransport } from "tiny-mcp-client";
4
+ export function parseMcpConfig(source, directory) {
5
+ const parsed = JSON.parse(source);
6
+ const options = normalizeMcpOptions(parsed, [
7
+ "servers",
8
+ "requestTimeoutMs",
9
+ "closeTimeoutMs",
10
+ "maxToolPages"
11
+ ]);
12
+ for (const server of Object.values(options.servers)) {
13
+ if ("command" in server) {
14
+ server.cwd = path.resolve(directory, server.cwd ?? ".");
15
+ if (server.command.includes("/") || server.command.includes("\\")) {
16
+ server.command = path.resolve(directory, server.command);
17
+ }
18
+ }
19
+ }
20
+ return options;
21
+ }
22
+ export function normalizeMcpOptions(value, keys = [
23
+ "servers",
24
+ "requestTimeoutMs",
25
+ "closeTimeoutMs",
26
+ "maxToolPages",
27
+ "signal",
28
+ "fetch",
29
+ "spawn"
30
+ ]) {
31
+ const options = readRecord(value, "MCP options", keys);
32
+ const servers = readRecord(options.servers, "MCP servers");
33
+ const normalized = Object.create(null);
34
+ for (const [name, input] of Object.entries(servers)) {
35
+ if (name.length === 0 || name.trim() !== name)
36
+ throw new TypeError("MCP server names must be non-empty without surrounding whitespace.");
37
+ const server = readRecord(input, `MCP server ${name}`);
38
+ if (Object.hasOwn(server, "command")) {
39
+ readRecord(server, `MCP server ${name}`, ["command", "args", "cwd", "env"]);
40
+ const command = readText(server.command, "MCP command");
41
+ if (server.args !== undefined &&
42
+ (!Array.isArray(server.args) ||
43
+ server.args.some((argument) => typeof argument !== "string")))
44
+ throw new TypeError("MCP args must be an array of strings.");
45
+ normalized[name] = {
46
+ command,
47
+ ...(server.args === undefined ? {} : { args: [...server.args] }),
48
+ ...(server.cwd === undefined ? {} : { cwd: readText(server.cwd, "MCP cwd") }),
49
+ env: readStrings(server.env ?? {}, "MCP environment")
50
+ };
51
+ }
52
+ else {
53
+ readRecord(server, `MCP server ${name}`, ["url", "headers"]);
54
+ const url = new URL(readText(server.url, "MCP URL"));
55
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
56
+ throw new TypeError("MCP URL must use HTTP or HTTPS without credentials or a fragment.");
57
+ const headers = readStrings(server.headers ?? {}, "MCP headers");
58
+ new Headers(headers);
59
+ normalized[name] = { url: url.href, headers };
60
+ }
61
+ }
62
+ for (const name of ["fetch", "spawn"])
63
+ if (options[name] !== undefined && typeof options[name] !== "function")
64
+ throw new TypeError(`MCP ${name} must be a function.`);
65
+ if (options.signal !== undefined && !(options.signal instanceof AbortSignal))
66
+ throw new TypeError("MCP signal must be an AbortSignal.");
67
+ return {
68
+ servers: normalized,
69
+ requestTimeoutMs: readTimeout(options.requestTimeoutMs, 30_000),
70
+ closeTimeoutMs: readTimeout(options.closeTimeoutMs, 1_000),
71
+ maxToolPages: readTimeout(options.maxToolPages, 100),
72
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
73
+ ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
74
+ ...(options.spawn === undefined ? {} : { spawn: options.spawn })
75
+ };
76
+ }
77
+ function readRecord(value, label, keys) {
78
+ if (typeof value !== "object" ||
79
+ value === null ||
80
+ Array.isArray(value) ||
81
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
82
+ throw new TypeError(`${label} must be a plain object.`);
83
+ const result = Object.create(null);
84
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
85
+ if (!("value" in descriptor) || (keys !== undefined && !keys.includes(key)))
86
+ throw new TypeError(`${label} contains unsupported field ${key}.`);
87
+ result[key] = descriptor.value;
88
+ }
89
+ return result;
90
+ }
91
+ function readText(value, label) {
92
+ if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0"))
93
+ throw new TypeError(`${label} must be a non-empty string without NUL characters.`);
94
+ return value;
95
+ }
96
+ function readStrings(value, label) {
97
+ const entries = readRecord(value, label);
98
+ for (const [name, entry] of Object.entries(entries))
99
+ if (typeof entry !== "string" || name.includes("\0") || entry.includes("\0"))
100
+ throw new TypeError(`${label} must contain strings without NUL characters.`);
101
+ return entries;
102
+ }
103
+ function readTimeout(value, fallback) {
104
+ if (value === undefined)
105
+ return fallback;
106
+ if (typeof value !== "number" ||
107
+ !Number.isSafeInteger(value) ||
108
+ value <= 0 ||
109
+ value > 2_147_483_647)
110
+ throw new TypeError("MCP timeouts must be positive 32-bit integers.");
111
+ return value;
112
+ }
113
+ export function connectMcpTransport(server, options, signal) {
114
+ signal?.throwIfAborted();
115
+ options.signal?.throwIfAborted();
116
+ const client = new McpClient({
117
+ clientInfo: { name: "safejs", version: "1" },
118
+ requestTimeoutMs: options.requestTimeoutMs
119
+ });
120
+ let child;
121
+ const activeRequests = new Set();
122
+ const transport = "command" in server
123
+ ? new StdioTransport({
124
+ ...server,
125
+ spawn(command, args, spawnOptions) {
126
+ child =
127
+ options.spawn === undefined
128
+ ? spawn(command, args, spawnOptions)
129
+ : options.spawn(command, args, spawnOptions);
130
+ return child;
131
+ }
132
+ })
133
+ : new HttpTransport({
134
+ ...server,
135
+ async fetch(input, init) {
136
+ const controller = new AbortController();
137
+ activeRequests.add(controller);
138
+ const cancel = () => controller.abort(init?.signal?.reason);
139
+ init?.signal?.addEventListener("abort", cancel, { once: true });
140
+ if (init?.signal?.aborted)
141
+ cancel();
142
+ const timeout = setTimeout(() => controller.abort(new Error("MCP HTTP request timed out.")), init?.method === "DELETE" ? options.closeTimeoutMs : options.requestTimeoutMs);
143
+ const finish = () => {
144
+ clearTimeout(timeout);
145
+ activeRequests.delete(controller);
146
+ init?.signal?.removeEventListener("abort", cancel);
147
+ };
148
+ try {
149
+ const response = await (options.fetch ?? fetch)(input, {
150
+ ...init,
151
+ signal: controller.signal,
152
+ redirect: "error"
153
+ });
154
+ if (response.body === null) {
155
+ finish();
156
+ return response;
157
+ }
158
+ const reader = response.body.getReader();
159
+ let stopReading;
160
+ const release = () => {
161
+ finish();
162
+ if (stopReading !== undefined)
163
+ controller.signal.removeEventListener("abort", stopReading);
164
+ };
165
+ const body = new ReadableStream({
166
+ start(stream) {
167
+ stopReading = () => {
168
+ void reader.cancel(controller.signal.reason).catch(() => undefined);
169
+ stream.error(controller.signal.reason);
170
+ release();
171
+ };
172
+ controller.signal.addEventListener("abort", stopReading, { once: true });
173
+ if (controller.signal.aborted)
174
+ stopReading();
175
+ },
176
+ async pull(stream) {
177
+ try {
178
+ const result = await reader.read();
179
+ if (controller.signal.aborted)
180
+ return;
181
+ if (result.done) {
182
+ release();
183
+ stream.close();
184
+ }
185
+ else
186
+ stream.enqueue(result.value);
187
+ }
188
+ catch (error) {
189
+ release();
190
+ stream.error(error);
191
+ }
192
+ },
193
+ async cancel(reason) {
194
+ release();
195
+ await reader.cancel(reason);
196
+ }
197
+ });
198
+ return new Response(body, {
199
+ status: response.status,
200
+ statusText: response.statusText,
201
+ headers: response.headers
202
+ });
203
+ }
204
+ catch (error) {
205
+ finish();
206
+ throw error;
207
+ }
208
+ }
209
+ });
210
+ let closing;
211
+ const cancel = () => {
212
+ void close().catch(() => undefined);
213
+ };
214
+ function close() {
215
+ closing ??= (async () => {
216
+ signal?.removeEventListener("abort", cancel);
217
+ options.signal?.removeEventListener("abort", cancel);
218
+ for (const request of activeRequests)
219
+ request.abort(new Error("MCP connection closed."));
220
+ await client.close();
221
+ transport.dispose();
222
+ await new Promise((resolve, reject) => {
223
+ let forced;
224
+ const timeout = setTimeout(() => {
225
+ if (child !== undefined && child.exitCode === null && child.signalCode === null)
226
+ child.kill("SIGKILL");
227
+ forced = setTimeout(() => reject(new Error("MCP transport did not close.")), options.closeTimeoutMs);
228
+ }, options.closeTimeoutMs);
229
+ void transport.closed.then(() => {
230
+ clearTimeout(timeout);
231
+ clearTimeout(forced);
232
+ resolve();
233
+ }, (error) => {
234
+ clearTimeout(timeout);
235
+ clearTimeout(forced);
236
+ reject(error);
237
+ });
238
+ });
239
+ })();
240
+ return closing;
241
+ }
242
+ signal?.addEventListener("abort", cancel, { once: true });
243
+ options.signal?.addEventListener("abort", cancel, { once: true });
244
+ const aborted = signal?.aborted || options.signal?.aborted;
245
+ if (aborted)
246
+ cancel();
247
+ const connecting = aborted
248
+ ? Promise.reject(signal?.aborted ? signal.reason : options.signal?.reason)
249
+ : client.connect(transport);
250
+ const ready = connecting.then(() => client, async (error) => {
251
+ await close().catch(() => undefined);
252
+ throw signal?.aborted
253
+ ? signal.reason
254
+ : options.signal?.aborted
255
+ ? options.signal.reason
256
+ : error;
257
+ });
258
+ return { ready, close };
259
+ }
@@ -1,3 +1,4 @@
1
+ import { type McpModuleOptions } from "./mcp-transport.js";
1
2
  export type McpModuleServerHandle = {
2
3
  command: string;
3
4
  args?: string[];
@@ -42,8 +43,21 @@ type McpConnection = {
42
43
  }>): Promise<McpModuleToolBatchResult[]>;
43
44
  };
44
45
  export type ConnectMcp = (server: McpModuleServerHandle) => Promise<McpConnection> | McpConnection;
45
- export declare function makeMcpModule(connectMcp: ConnectMcp): {
46
+ export type McpNamedServerHandle = {
47
+ name: string;
48
+ };
49
+ export type ManagedMcpClient = McpModuleClient & {
50
+ close(): Promise<void>;
51
+ };
52
+ export type ManagedMcpModule = {
53
+ servers: Record<string, ManagedMcpClient>;
54
+ server(name: string): McpNamedServerHandle;
55
+ client(handle: McpNamedServerHandle | string): Promise<ManagedMcpClient>;
56
+ };
57
+ type CustomMcpModule = {
46
58
  server(server: McpModuleServerHandle): McpModuleServerHandle;
47
59
  client(handle: McpModuleServerHandle): Promise<McpModuleClient>;
48
60
  };
61
+ export declare function makeMcpModule(options: McpModuleOptions): ManagedMcpModule;
62
+ export declare function makeMcpModule(connectMcp: ConnectMcp): CustomMcpModule;
49
63
  export {};
@@ -1,5 +1,112 @@
1
+ import { McpError } from "tiny-mcp-client";
2
+ import { hostErrorData } from "../error/shape.js";
3
+ import { declareHostOperation } from "../interp/host-bridge.js";
4
+ import { runResources } from "../interp/resources.js";
5
+ import { connectMcpTransport, normalizeMcpOptions } from "./mcp-transport.js";
1
6
  const DEFAULT_TOOL_BATCH_CONCURRENCY = 4;
2
7
  export function makeMcpModule(connectMcp) {
8
+ if (typeof connectMcp !== "function") {
9
+ const options = normalizeMcpOptions(connectMcp);
10
+ const servers = Object.create(null);
11
+ for (const [name, config] of Object.entries(options.servers)) {
12
+ const connections = new WeakMap();
13
+ const directScope = {};
14
+ const connection = () => {
15
+ const scope = runResources.getStore();
16
+ scope?.signal.throwIfAborted();
17
+ options.signal?.throwIfAborted();
18
+ const owner = scope ?? directScope;
19
+ let current = connections.get(owner);
20
+ if (current === undefined) {
21
+ current = connectMcpTransport(config, options, scope?.signal);
22
+ connections.set(owner, current);
23
+ scope?.add(current.close);
24
+ }
25
+ return current.ready;
26
+ };
27
+ const invoke = async (operation) => {
28
+ try {
29
+ const result = await operation(await connection());
30
+ runResources.getStore()?.signal.throwIfAborted();
31
+ options.signal?.throwIfAborted();
32
+ return result;
33
+ }
34
+ catch (error) {
35
+ runResources.getStore()?.signal.throwIfAborted();
36
+ options.signal?.throwIfAborted();
37
+ if (error instanceof McpError) {
38
+ hostErrorData.set(error, {
39
+ code: error.code,
40
+ ...(error.data === undefined ? {} : { data: error.data })
41
+ });
42
+ }
43
+ throw error;
44
+ }
45
+ };
46
+ servers[name] = {
47
+ tools: declareHostOperation(async () => invoke(async (client) => {
48
+ const tools = [];
49
+ const cursors = new Set();
50
+ let pages = 0;
51
+ let cursor;
52
+ do {
53
+ const result = await client.listTools(cursor === undefined ? undefined : { cursor });
54
+ pages += 1;
55
+ tools.push(...normalizeToolsResult(result));
56
+ cursor = result.nextCursor;
57
+ if (cursor !== undefined) {
58
+ if (typeof cursor !== "string" || cursors.has(cursor))
59
+ throw new Error("MCP tools pagination did not advance.");
60
+ cursors.add(cursor);
61
+ if (pages >= options.maxToolPages)
62
+ throw new Error("MCP tools page limit exceeded.");
63
+ }
64
+ } while (cursor !== undefined);
65
+ return tools;
66
+ }), "re-issue"),
67
+ tool: declareHostOperation(async (toolName, args) => {
68
+ const request = createNullRecord({
69
+ name: readNonEmptyTrimmedString(toolName, "MCP tool name"),
70
+ ...(args === undefined ? {} : { arguments: readToolArguments(args) })
71
+ });
72
+ return invoke((client) => client.callTool(request));
73
+ }, "read-side-effect"),
74
+ toolBatch: declareHostOperation(async (calls) => {
75
+ const normalized = readToolBatchCalls(calls);
76
+ if (normalized.length === 0)
77
+ return [];
78
+ return invoke((client) => executeToolBatch(client, normalized));
79
+ }, "read-side-effect"),
80
+ close: declareHostOperation(async () => {
81
+ const owner = runResources.getStore() ?? directScope;
82
+ const current = connections.get(owner);
83
+ if (current !== undefined) {
84
+ await current.close();
85
+ connections.delete(owner);
86
+ }
87
+ }, "re-issue")
88
+ };
89
+ }
90
+ const readName = (value) => {
91
+ const name = readNonEmptyString(value, "MCP server name");
92
+ if (!Object.hasOwn(servers, name))
93
+ throw new Error(`MCP server '${name}' is not configured.`);
94
+ return name;
95
+ };
96
+ return {
97
+ servers,
98
+ server(name) {
99
+ return createNullRecord({ name: readName(name) });
100
+ },
101
+ async client(handle) {
102
+ if (typeof handle === "string")
103
+ return servers[readName(handle)];
104
+ if (!isRecord(handle) || Object.keys(handle).length !== 1 || !Object.hasOwn(handle, "name"))
105
+ throw new TypeError("MCP client requires a named server handle.");
106
+ return servers[readName(getOwnProperty(handle, "name"))];
107
+ }
108
+ };
109
+ }
3
110
  return {
4
111
  server(server) {
5
112
  return normalizeServerHandle(server);