@introspection-ai/recipes 0.13.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 (161) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +184 -0
  3. package/dist/agent-tool.d.ts +42 -0
  4. package/dist/agent-tool.d.ts.map +1 -0
  5. package/dist/agent-tool.js +159 -0
  6. package/dist/agent-tool.js.map +1 -0
  7. package/dist/child-agent-completions.d.ts +61 -0
  8. package/dist/child-agent-completions.d.ts.map +1 -0
  9. package/dist/child-agent-completions.js +135 -0
  10. package/dist/child-agent-completions.js.map +1 -0
  11. package/dist/child-agent-store.d.ts +48 -0
  12. package/dist/child-agent-store.d.ts.map +1 -0
  13. package/dist/child-agent-store.js +98 -0
  14. package/dist/child-agent-store.js.map +1 -0
  15. package/dist/child-agent.d.ts +43 -0
  16. package/dist/child-agent.d.ts.map +1 -0
  17. package/dist/child-agent.js +268 -0
  18. package/dist/child-agent.js.map +1 -0
  19. package/dist/direct-cli.d.ts +9 -0
  20. package/dist/direct-cli.d.ts.map +1 -0
  21. package/dist/direct-cli.js +22 -0
  22. package/dist/direct-cli.js.map +1 -0
  23. package/dist/index.d.ts +14 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +14 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/inspect.d.ts +26 -0
  28. package/dist/inspect.d.ts.map +1 -0
  29. package/dist/inspect.js +70 -0
  30. package/dist/inspect.js.map +1 -0
  31. package/dist/interactions.d.ts +204 -0
  32. package/dist/interactions.d.ts.map +1 -0
  33. package/dist/interactions.js +358 -0
  34. package/dist/interactions.js.map +1 -0
  35. package/dist/mcp-catalog.d.ts +8 -0
  36. package/dist/mcp-catalog.d.ts.map +1 -0
  37. package/dist/mcp-catalog.js +60 -0
  38. package/dist/mcp-catalog.js.map +1 -0
  39. package/dist/mcp-cli-core.d.ts +79 -0
  40. package/dist/mcp-cli-core.d.ts.map +1 -0
  41. package/dist/mcp-cli-core.js +1717 -0
  42. package/dist/mcp-cli-core.js.map +1 -0
  43. package/dist/mcp-cli-help.d.ts +2 -0
  44. package/dist/mcp-cli-help.d.ts.map +1 -0
  45. package/dist/mcp-cli-help.js +36 -0
  46. package/dist/mcp-cli-help.js.map +1 -0
  47. package/dist/mcp-cli-policy.d.ts +17 -0
  48. package/dist/mcp-cli-policy.d.ts.map +1 -0
  49. package/dist/mcp-cli-policy.js +230 -0
  50. package/dist/mcp-cli-policy.js.map +1 -0
  51. package/dist/mcp-cli.d.ts +3 -0
  52. package/dist/mcp-cli.d.ts.map +1 -0
  53. package/dist/mcp-cli.js +33 -0
  54. package/dist/mcp-cli.js.map +1 -0
  55. package/dist/mcp-client.d.ts +3 -0
  56. package/dist/mcp-client.d.ts.map +1 -0
  57. package/dist/mcp-client.js +86 -0
  58. package/dist/mcp-client.js.map +1 -0
  59. package/dist/mcp-command-context.d.ts +13 -0
  60. package/dist/mcp-command-context.d.ts.map +1 -0
  61. package/dist/mcp-command-context.js +29 -0
  62. package/dist/mcp-command-context.js.map +1 -0
  63. package/dist/mcp-contract.d.ts +16 -0
  64. package/dist/mcp-contract.d.ts.map +1 -0
  65. package/dist/mcp-contract.js +306 -0
  66. package/dist/mcp-contract.js.map +1 -0
  67. package/dist/mcp-daemon-client.d.ts +10 -0
  68. package/dist/mcp-daemon-client.d.ts.map +1 -0
  69. package/dist/mcp-daemon-client.js +119 -0
  70. package/dist/mcp-daemon-client.js.map +1 -0
  71. package/dist/mcp-daemon-protocol.d.ts +64 -0
  72. package/dist/mcp-daemon-protocol.d.ts.map +1 -0
  73. package/dist/mcp-daemon-protocol.js +6 -0
  74. package/dist/mcp-daemon-protocol.js.map +1 -0
  75. package/dist/mcp-daemon.d.ts +3 -0
  76. package/dist/mcp-daemon.d.ts.map +1 -0
  77. package/dist/mcp-daemon.js +76880 -0
  78. package/dist/mcp-daemon.js.map +1 -0
  79. package/dist/mcp-run-worker.d.ts +2 -0
  80. package/dist/mcp-run-worker.d.ts.map +1 -0
  81. package/dist/mcp-run-worker.js +76579 -0
  82. package/dist/mcp-run-worker.js.map +1 -0
  83. package/dist/mcp.d.ts +168 -0
  84. package/dist/mcp.d.ts.map +1 -0
  85. package/dist/mcp.js +558 -0
  86. package/dist/mcp.js.map +1 -0
  87. package/dist/pi-extension.d.ts +30 -0
  88. package/dist/pi-extension.d.ts.map +1 -0
  89. package/dist/pi-extension.js +880 -0
  90. package/dist/pi-extension.js.map +1 -0
  91. package/dist/provider-env.d.ts +2 -0
  92. package/dist/provider-env.d.ts.map +1 -0
  93. package/dist/provider-env.js +21 -0
  94. package/dist/provider-env.js.map +1 -0
  95. package/dist/recipe/resolve.d.ts +30 -0
  96. package/dist/recipe/resolve.d.ts.map +1 -0
  97. package/dist/recipe/resolve.js +131 -0
  98. package/dist/recipe/resolve.js.map +1 -0
  99. package/dist/recipe-agent.d.ts +71 -0
  100. package/dist/recipe-agent.d.ts.map +1 -0
  101. package/dist/recipe-agent.js +555 -0
  102. package/dist/recipe-agent.js.map +1 -0
  103. package/dist/recipe-extensions.d.ts +9 -0
  104. package/dist/recipe-extensions.d.ts.map +1 -0
  105. package/dist/recipe-extensions.js +81 -0
  106. package/dist/recipe-extensions.js.map +1 -0
  107. package/dist/recipe-mcp-config.d.ts +15 -0
  108. package/dist/recipe-mcp-config.d.ts.map +1 -0
  109. package/dist/recipe-mcp-config.js +82 -0
  110. package/dist/recipe-mcp-config.js.map +1 -0
  111. package/dist/recipe-model.d.ts +49 -0
  112. package/dist/recipe-model.d.ts.map +1 -0
  113. package/dist/recipe-model.js +299 -0
  114. package/dist/recipe-model.js.map +1 -0
  115. package/dist/recipe-package.d.ts +79 -0
  116. package/dist/recipe-package.d.ts.map +1 -0
  117. package/dist/recipe-package.js +479 -0
  118. package/dist/recipe-package.js.map +1 -0
  119. package/dist/recipe-skills.d.ts +7 -0
  120. package/dist/recipe-skills.d.ts.map +1 -0
  121. package/dist/recipe-skills.js +46 -0
  122. package/dist/recipe-skills.js.map +1 -0
  123. package/dist/run-controller.d.ts +32 -0
  124. package/dist/run-controller.d.ts.map +1 -0
  125. package/dist/run-controller.js +259 -0
  126. package/dist/run-controller.js.map +1 -0
  127. package/dist/run.d.ts +28 -0
  128. package/dist/run.d.ts.map +1 -0
  129. package/dist/run.js +84 -0
  130. package/dist/run.js.map +1 -0
  131. package/dist/session.d.ts +140 -0
  132. package/dist/session.d.ts.map +1 -0
  133. package/dist/session.js +363 -0
  134. package/dist/session.js.map +1 -0
  135. package/dist/test-utils.d.ts +32 -0
  136. package/dist/test-utils.d.ts.map +1 -0
  137. package/dist/test-utils.js +259 -0
  138. package/dist/test-utils.js.map +1 -0
  139. package/dist/testing.d.ts +25 -0
  140. package/dist/testing.d.ts.map +1 -0
  141. package/dist/testing.js +96 -0
  142. package/dist/testing.js.map +1 -0
  143. package/docs/agent-composition.md +196 -0
  144. package/docs/deployment-configuration.md +65 -0
  145. package/docs/index.md +49 -0
  146. package/docs/interactions.md +210 -0
  147. package/docs/mcp-auth.md +80 -0
  148. package/docs/mcp-configuration.md +126 -0
  149. package/docs/migration.md +70 -0
  150. package/docs/pi-extension.md +97 -0
  151. package/docs/python-bindings-release.md +47 -0
  152. package/docs/recipe-evals.md +70 -0
  153. package/docs/recipe-flow.md +41 -0
  154. package/docs/recipe-format.md +186 -0
  155. package/docs/recipe-judges.md +152 -0
  156. package/docs/runtime-library.md +171 -0
  157. package/package.json +106 -0
  158. package/vendor/mcp-client/darwin-arm64/mcp-client +0 -0
  159. package/vendor/mcp-client/darwin-x64/mcp-client +0 -0
  160. package/vendor/mcp-client/linux-arm64/mcp-client +0 -0
  161. package/vendor/mcp-client/linux-x64/mcp-client +0 -0
@@ -0,0 +1,1717 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { stdin as input, stderr, stdout } from "node:process";
7
+ import { isMainThread, Worker } from "node:worker_threads";
8
+ import { createCallResult, createRuntime } from "mcporter";
9
+ import { currentMcpCommandContext, runWithMcpCommandContext, } from "./mcp-command-context.js";
10
+ import { catalogOutputSchema, renderToolContract, renderToolSignature, } from "./mcp-contract.js";
11
+ import { mcpCliHelpText } from "./mcp-cli-help.js";
12
+ import { defaultMcporterConfigPath, defaultMcpSessionPath, filterMcpCatalog, mcpSessionAllowsTool, } from "./mcp.js";
13
+ import { createMcpCliSessionPolicy, validateDelegatedMcpCommand, } from "./mcp-cli-policy.js";
14
+ const DEFAULT_RUN_TIMEOUT_MS = 120_000;
15
+ const DEFAULT_TOOL_CALL_TIMEOUT_MS = 60_000;
16
+ const DEFAULT_LIST_TIMEOUT_MS = 30_000;
17
+ const DEFAULT_MAX_RUN_TOOL_CALLS = 100;
18
+ const DEFAULT_MAX_CONCURRENT_TOOL_CALLS = 16;
19
+ const MAX_SEARCH_DESCRIPTION_CHARS = 600;
20
+ const CATALOG_LOCK_POLL_MS = 50;
21
+ const CATALOG_LOCK_INCOMPLETE_GRACE_MS = 1_000;
22
+ const MCP_SESSION_ENV = "PI_RECIPES_MCP_SESSION";
23
+ async function acquireRuntime() {
24
+ const shared = currentMcpCommandContext()?.runtime;
25
+ return shared
26
+ ? { runtime: shared, owned: false }
27
+ : { runtime: await createRuntime(), owned: true };
28
+ }
29
+ async function closeOwnedRuntime(runtime, owned) {
30
+ if (owned)
31
+ await runtime.close();
32
+ }
33
+ export class McpRunUsageError extends Error {
34
+ }
35
+ export class McpRunTimeoutError extends Error {
36
+ outcome = "unknown";
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "McpRunTimeoutError";
40
+ }
41
+ }
42
+ class McpRemoteToolResultError extends Error {
43
+ details;
44
+ constructor(details) {
45
+ super(details.message);
46
+ this.details = details;
47
+ this.name = "McpRemoteToolResultError";
48
+ }
49
+ }
50
+ export class McpRunToolError extends Error {
51
+ server;
52
+ tool;
53
+ kind = "tool_execution";
54
+ details;
55
+ code;
56
+ retryable;
57
+ action;
58
+ outcome;
59
+ constructor(server, tool, message, options) {
60
+ super(message, options);
61
+ this.server = server;
62
+ this.tool = tool;
63
+ this.name = "McpRunToolError";
64
+ this.details = options?.details;
65
+ this.code = options?.details?.code;
66
+ this.retryable = options?.details?.retryable;
67
+ this.action = options?.details?.action;
68
+ this.outcome = options?.details?.outcome;
69
+ }
70
+ }
71
+ export function mcpListHelpText() {
72
+ return [
73
+ "Usage: mcp list [server | server.tool] [flags]",
74
+ "",
75
+ "Shows a compact view of tools materialized in this recipe session.",
76
+ "",
77
+ "Flags:",
78
+ " --all-parameters Include every optional parameter.",
79
+ " --schema Show a compact input/output contract for one exact tool.",
80
+ " --verbose Include full tool and parameter descriptions.",
81
+ " --status Show concise status for an exact server target.",
82
+ " --quiet, --exit-code Health checks for an exact server target.",
83
+ " --timeout <ms> Override discovery timeout for an exact target.",
84
+ " JSON is reserved for actual tool results; metadata is compact text.",
85
+ "",
86
+ "URLs, ad-hoc transports, config overrides, and persistence are unavailable in recipe sessions.",
87
+ ].join("\n");
88
+ }
89
+ export function mcpCallHelpText() {
90
+ return [
91
+ "Usage: mcp call <server>.<tool> [arguments] [flags]",
92
+ "",
93
+ "Calls exact tools materialized in this recipe session.",
94
+ "",
95
+ "Arguments:",
96
+ " key=value Named arguments with schema-aware coercion.",
97
+ " key=@path Read an exact UTF-8 string; use @@ for a literal @.",
98
+ " --json <json|-> Supply a structured JSON object directly or from stdin.",
99
+ ' Array example: mcp call server.tool --json \'{"tags":["a","b"]}\'.',
100
+ " Quote argument tokens containing shell operators such as |, <, >, &, or ;. JSON stdin avoids nested shell quoting.",
101
+ "",
102
+ "Output/runtime flags:",
103
+ " --output text|markdown|json|raw",
104
+ " --timeout <ms>",
105
+ " Machine-readable output is forwarded unchanged.",
106
+ " When parsing JSON, keep stderr separate and do not truncate stdout with head or sed.",
107
+ "",
108
+ "URLs, ad-hoc transports, config overrides, and persistence are unavailable in recipe sessions.",
109
+ ].join("\n");
110
+ }
111
+ export function mcpSearchHelpText() {
112
+ return [
113
+ 'Usage: mcp search "what you need" [--limit N] [--regex]',
114
+ "",
115
+ "Searches only MCP tools available in this session.",
116
+ "Results include the exact tool ref, description, and required fields.",
117
+ "Try broader or alternate terms when no result matches.",
118
+ "Use `mcp list <server>` only to identify exact tool names, then inspect one candidate with `mcp list <server.tool> --schema`.",
119
+ ].join("\n");
120
+ }
121
+ export function mcpRunHelpText() {
122
+ return [
123
+ "Usage: mcp run [--var KEY=value] [--json-errors] [file]",
124
+ "",
125
+ 'Runs a short JavaScript workflow with available MCP tools such as `tools["server"]["tool"]`.',
126
+ "With no file, code is read from stdin. Keep heredocs quoted and pass dynamic values with --var.",
127
+ "Scripts are killed after 120s by default (override with PI_RECIPES_MCP_RUN_TIMEOUT_MS).",
128
+ "Each tool call is capped at 60s; workflows allow at most 100 calls and run 16 at a time by default.",
129
+ "Extra calls wait in a FIFO queue and inherit the remaining workflow deadline.",
130
+ "Always await or return tool-call chains.",
131
+ "Detached .then/.catch calls fail if still pending when the script exits.",
132
+ "Structured MCP errors retain code, retryable, action, request_id, and outcome fields when supplied.",
133
+ "Calls return decoded JSON by default, so read response fields directly from the awaited value.",
134
+ "Only when a tool documents another response type, call the format on the tool itself: tool.text(args), tool.markdown(args), tool.images(args), tool.content(args), tool.structuredContent(args), or tool.raw(args).",
135
+ "Use Promise.all for independent reads. Await dependent calls and mutations in order.",
136
+ "Do not loop over mcp call in the shell. Use mcp run for repeated calls and print only the fields needed.",
137
+ "Use --var/vars for dynamic input; process.argv is intentionally unavailable inside workflows.",
138
+ "--json-errors emits a structured error object on stderr while preserving the nonzero exit code.",
139
+ "MCP calls are always headless. If authentication is required, ask the user to authenticate the connection outside the agent session, then retry.",
140
+ "A synchronous busy-loop is force-killed at the deadline.",
141
+ "Code runs with the same OS privileges as the active shell sandbox; mcp run is not a separate security boundary.",
142
+ "",
143
+ "Example — batch or compose multiple calls:",
144
+ " mcp run <<'JS'",
145
+ ' const ids = ["id-1", "id-2", "id-3"]',
146
+ ' const results = await Promise.all(ids.map(id => tools["server"]["tool"]({ id })))',
147
+ " console.log(JSON.stringify(results.map(result => ({ id: result.id, name: result.name })), null, 2))",
148
+ " JS",
149
+ ].join("\n");
150
+ }
151
+ function isHelpArg(value) {
152
+ return value === "--help" || value === "-h" || value === "help";
153
+ }
154
+ function readStdin() {
155
+ const source = currentMcpCommandContext()?.stdin ?? input;
156
+ source.setEncoding("utf8");
157
+ return new Promise((resolve, reject) => {
158
+ let data = "";
159
+ source.on("data", (chunk) => {
160
+ data += chunk;
161
+ });
162
+ source.on("error", reject);
163
+ source.on("end", () => resolve(data));
164
+ });
165
+ }
166
+ function checkedCallResult(callResult) {
167
+ if (asRecord(callResult.raw).isError === true) {
168
+ // Fail loudly in code mode so a bad call rejects instead of flowing an
169
+ // error string into downstream logic.
170
+ const parsed = callResult.json();
171
+ const parsedRecord = asRecord(parsed);
172
+ const parsedError = Object.prototype.hasOwnProperty.call(parsedRecord, "error")
173
+ ? parsedRecord.error
174
+ : parsedRecord;
175
+ const errorObject = asRecord(parsedError);
176
+ const message = typeof parsedError === "string"
177
+ ? parsedError
178
+ : typeof errorObject.message === "string"
179
+ ? errorObject.message
180
+ : callResult.text() ?? "MCP tool call failed.";
181
+ throw new McpRemoteToolResultError({ ...errorObject, message });
182
+ }
183
+ return callResult;
184
+ }
185
+ function decodeCallResult(callResult, format, ref) {
186
+ switch (format) {
187
+ case "json": {
188
+ const decoded = callResult.json();
189
+ if (decoded === null && callResult.structuredContent() == null && callResult.text() != null) {
190
+ throw new McpRunUsageError(`${ref} did not return JSON. Its documentation should name the response type; ` +
191
+ `for plain text call tools[${JSON.stringify(ref.split(".")[0])}]` +
192
+ `[${JSON.stringify(ref.slice(ref.indexOf(".") + 1))}].text(args).`);
193
+ }
194
+ return decoded;
195
+ }
196
+ case "text":
197
+ return callResult.text();
198
+ case "markdown":
199
+ return callResult.markdown();
200
+ case "images":
201
+ return callResult.images();
202
+ case "content":
203
+ return callResult.content();
204
+ case "structuredContent":
205
+ return callResult.structuredContent();
206
+ case "raw":
207
+ return callResult.raw;
208
+ }
209
+ }
210
+ function validateRunToolArgs(server, tool, args) {
211
+ const cliStyleKey = Object.keys(args).find((key) => key.includes(":=") || key.includes("="));
212
+ if (!cliStyleKey)
213
+ return;
214
+ const suggestedKey = cliStyleKey.split(/:=|=/, 1)[0];
215
+ throw new McpRunUsageError(`Invalid JavaScript argument key '${cliStyleKey}' for ${server}.${tool}. ` +
216
+ `mcp run uses normal JavaScript objects: write { ${suggestedKey}: value }, ` +
217
+ `not mcporter CLI key=value or key:=value syntax.`);
218
+ }
219
+ class ToolCallQueue {
220
+ limit;
221
+ active = 0;
222
+ waiting = [];
223
+ cancelled;
224
+ constructor(limit) {
225
+ this.limit = limit;
226
+ }
227
+ acquire() {
228
+ if (this.cancelled !== undefined)
229
+ return Promise.reject(this.cancelled);
230
+ if (this.active < this.limit) {
231
+ this.active += 1;
232
+ return Promise.resolve(this.releaseHandle());
233
+ }
234
+ return new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));
235
+ }
236
+ cancel(error) {
237
+ if (this.cancelled !== undefined)
238
+ return;
239
+ this.cancelled = error;
240
+ for (const waiter of this.waiting.splice(0))
241
+ waiter.reject(error);
242
+ }
243
+ releaseHandle() {
244
+ let released = false;
245
+ return () => {
246
+ if (released)
247
+ return;
248
+ released = true;
249
+ const next = this.waiting.shift();
250
+ if (next && this.cancelled === undefined) {
251
+ next.resolve(this.releaseHandle());
252
+ return;
253
+ }
254
+ this.active -= 1;
255
+ };
256
+ }
257
+ }
258
+ function asRecord(value) {
259
+ return value && typeof value === "object" && !Array.isArray(value)
260
+ ? value
261
+ : {};
262
+ }
263
+ function words(value) {
264
+ return value
265
+ .toLowerCase()
266
+ .split(/[^a-z0-9_/-]+/g)
267
+ .map((token) => token.trim())
268
+ .filter(Boolean);
269
+ }
270
+ function toolProperties(tool) {
271
+ const schema = asRecord(tool.input_schema);
272
+ const properties = asRecord(schema.properties);
273
+ return Object.entries(properties).map(([name, value]) => {
274
+ const property = asRecord(value);
275
+ const description = typeof property.description === "string"
276
+ ? property.description
277
+ : typeof property.title === "string"
278
+ ? property.title
279
+ : "";
280
+ return { name, description };
281
+ });
282
+ }
283
+ function toolRequired(tool) {
284
+ const schema = asRecord(tool.input_schema);
285
+ return Array.isArray(schema.required)
286
+ ? schema.required.filter((item) => typeof item === "string")
287
+ : [];
288
+ }
289
+ function includesTerm(value, term) {
290
+ return value.toLowerCase().includes(term.toLowerCase());
291
+ }
292
+ function scoreTool(opts) {
293
+ const ref = `${opts.serverId}.${opts.tool.name}`;
294
+ const description = opts.tool.description ?? "";
295
+ const properties = toolProperties(opts.tool);
296
+ const propertyNames = properties.map((property) => property.name).join(" ");
297
+ const propertyDescriptions = properties.map((property) => property.description).join(" ");
298
+ const searchable = [
299
+ ref,
300
+ opts.serverId,
301
+ opts.serverName,
302
+ opts.tool.name,
303
+ description,
304
+ propertyNames,
305
+ propertyDescriptions,
306
+ ].join("\n");
307
+ if (opts.regex)
308
+ return opts.regex.test(searchable) ? 100 : 0;
309
+ const query = opts.query.toLowerCase();
310
+ const toolName = opts.tool.name.toLowerCase();
311
+ const refName = ref.toLowerCase();
312
+ let score = 0;
313
+ if (refName === query)
314
+ score += 500;
315
+ if (toolName === query)
316
+ score += 300;
317
+ if (refName.includes(query))
318
+ score += 120;
319
+ if (toolName.includes(query))
320
+ score += 100;
321
+ if (description.toLowerCase().includes(query))
322
+ score += 60;
323
+ const toolNameTokens = new Set(words(opts.tool.name));
324
+ const serverTokens = new Set(words(`${opts.serverId} ${opts.serverName}`));
325
+ const descriptionText = description.toLowerCase();
326
+ const propertyNameTokens = new Set(words(propertyNames));
327
+ const propertyDescriptionText = propertyDescriptions.toLowerCase();
328
+ for (const term of opts.queryTerms) {
329
+ if (toolNameTokens.has(term))
330
+ score += 40;
331
+ else if (includesTerm(opts.tool.name, term))
332
+ score += 20;
333
+ if (descriptionText.includes(term))
334
+ score += 16;
335
+ if (propertyNameTokens.has(term))
336
+ score += 12;
337
+ else if (includesTerm(propertyNames, term))
338
+ score += 6;
339
+ if (propertyDescriptionText.includes(term))
340
+ score += 6;
341
+ if (serverTokens.has(term))
342
+ score += 4;
343
+ }
344
+ return score;
345
+ }
346
+ function exampleValue(name) {
347
+ if (/^(q|query|search)$/i.test(name))
348
+ return `="example query"`;
349
+ if (/limit|count|max/i.test(name))
350
+ return "=10";
351
+ if (/^(id|.*Id)$/i.test(name))
352
+ return '="<id>"';
353
+ return '="<value>"';
354
+ }
355
+ function callExample(serverId, tool) {
356
+ const required = toolRequired(tool);
357
+ const args = required.slice(0, 4).map((name) => `${name}${exampleValue(name)}`);
358
+ return ["mcp call", `${serverId}.${tool.name}`, ...args].join(" ");
359
+ }
360
+ export function searchMcpTools(session, query, opts = {}) {
361
+ const trimmed = query.trim();
362
+ if (!trimmed)
363
+ return [];
364
+ const regex = opts.regex ? new RegExp(trimmed, "i") : undefined;
365
+ const queryTerms = words(trimmed);
366
+ const matches = [];
367
+ for (const server of session.servers) {
368
+ for (const tool of server.catalog ?? []) {
369
+ const score = scoreTool({
370
+ query: trimmed,
371
+ queryTerms,
372
+ regex,
373
+ serverId: server.id,
374
+ serverName: server.name ?? "",
375
+ tool,
376
+ });
377
+ if (score <= 0)
378
+ continue;
379
+ const ref = `${server.id}.${tool.name}`;
380
+ matches.push({
381
+ ref,
382
+ server: server.id,
383
+ tool: tool.name,
384
+ description: (tool.description ?? "").length > MAX_SEARCH_DESCRIPTION_CHARS
385
+ ? `${(tool.description ?? "").slice(0, MAX_SEARCH_DESCRIPTION_CHARS - 14).trimEnd()}… [truncated]`
386
+ : (tool.description ?? ""),
387
+ required: toolRequired(tool),
388
+ score,
389
+ inspect: `mcp list ${ref} --schema`,
390
+ call: callExample(server.id, tool),
391
+ ...(tool.annotations ? { annotations: tool.annotations } : {}),
392
+ });
393
+ }
394
+ }
395
+ return matches
396
+ .sort((a, b) => b.score - a.score || a.ref.localeCompare(b.ref))
397
+ .slice(0, opts.limit ?? 8);
398
+ }
399
+ async function readSession() {
400
+ const path = sessionPolicyPath();
401
+ const data = await readFile(path, "utf8");
402
+ const parsed = JSON.parse(data);
403
+ if (parsed.version !== 1 || !Array.isArray(parsed.servers)) {
404
+ throw new Error(`Invalid MCP session policy at ${path}`);
405
+ }
406
+ return parsed;
407
+ }
408
+ function sessionPolicyPath() {
409
+ const workspacePath = defaultMcpSessionPath(sessionRoot());
410
+ return existsSync(workspacePath)
411
+ ? workspacePath
412
+ : process.env[MCP_SESSION_ENV] || workspacePath;
413
+ }
414
+ function sessionMcporterConfigPath() {
415
+ const workspacePath = defaultMcporterConfigPath(sessionRoot());
416
+ return existsSync(workspacePath)
417
+ ? workspacePath
418
+ : process.env.MCPORTER_CONFIG || workspacePath;
419
+ }
420
+ function sessionRoot() {
421
+ return process.env.PI_RECIPES_MCP_SESSION_ROOT || process.cwd();
422
+ }
423
+ function pinSessionMcporterConfig() {
424
+ process.env.MCPORTER_CONFIG = sessionMcporterConfigPath();
425
+ }
426
+ async function sessionCliPolicy() {
427
+ return createMcpCliSessionPolicy(await readSession());
428
+ }
429
+ function catalogFingerprint(server) {
430
+ return createHash("sha256")
431
+ .update(JSON.stringify({
432
+ id: server.id,
433
+ baseUrl: server.base_url,
434
+ packageTools: server.package_tools,
435
+ agentTools: server.agent_tools,
436
+ }))
437
+ .digest("hex")
438
+ .slice(0, 20);
439
+ }
440
+ function catalogCachePath(server) {
441
+ return join(sessionRoot(), ".pi", "mcp-catalogs", `${server.id}-${catalogFingerprint(server)}.json`);
442
+ }
443
+ async function readCachedCatalog(server) {
444
+ if (server.catalog)
445
+ return server.catalog;
446
+ try {
447
+ const parsed = JSON.parse(await readFile(catalogCachePath(server), "utf8"));
448
+ return Array.isArray(parsed.tools) ? parsed.tools : null;
449
+ }
450
+ catch {
451
+ return null;
452
+ }
453
+ }
454
+ async function writeCachedCatalog(server, tools) {
455
+ const directory = join(sessionRoot(), ".pi", "mcp-catalogs");
456
+ await mkdir(directory, { recursive: true });
457
+ const path = catalogCachePath(server);
458
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
459
+ await writeFile(temporary, `${JSON.stringify({ tools })}\n`, { mode: 0o600 });
460
+ await rename(temporary, path);
461
+ }
462
+ function processExists(pid) {
463
+ try {
464
+ process.kill(pid, 0);
465
+ return true;
466
+ }
467
+ catch (error) {
468
+ return error.code === "EPERM";
469
+ }
470
+ }
471
+ async function catalogLockIsStale(path, timeoutMs) {
472
+ try {
473
+ const owner = JSON.parse(await readFile(path, "utf8"));
474
+ if (typeof owner.pid === "number" &&
475
+ Number.isInteger(owner.pid) &&
476
+ owner.pid > 0 &&
477
+ typeof owner.createdAt === "number" &&
478
+ Number.isFinite(owner.createdAt)) {
479
+ return (!processExists(owner.pid) ||
480
+ Date.now() - owner.createdAt > timeoutMs + CATALOG_LOCK_INCOMPLETE_GRACE_MS);
481
+ }
482
+ }
483
+ catch (error) {
484
+ if (error.code === "ENOENT")
485
+ return false;
486
+ }
487
+ try {
488
+ const metadata = await stat(path);
489
+ return Date.now() - metadata.mtimeMs > CATALOG_LOCK_INCOMPLETE_GRACE_MS;
490
+ }
491
+ catch {
492
+ return false;
493
+ }
494
+ }
495
+ async function acquireCatalogLock(path) {
496
+ let handle;
497
+ try {
498
+ handle = await open(path, "wx", 0o600);
499
+ }
500
+ catch (error) {
501
+ if (error.code === "EEXIST")
502
+ return false;
503
+ throw error;
504
+ }
505
+ try {
506
+ await handle.writeFile(`${JSON.stringify({ pid: process.pid, createdAt: Date.now() })}\n`);
507
+ }
508
+ catch (error) {
509
+ await handle.close();
510
+ await rm(path, { force: true });
511
+ throw error;
512
+ }
513
+ await handle.close();
514
+ return true;
515
+ }
516
+ function catalogEntry(tool) {
517
+ const annotations = asRecord(tool).annotations;
518
+ return {
519
+ name: tool.name,
520
+ ...(tool.description ? { description: tool.description } : {}),
521
+ ...(tool.inputSchema && typeof tool.inputSchema === "object"
522
+ ? { input_schema: tool.inputSchema }
523
+ : {}),
524
+ ...(tool.outputSchema && typeof tool.outputSchema === "object"
525
+ ? { output_schema: tool.outputSchema }
526
+ : {}),
527
+ ...(annotations && typeof annotations === "object"
528
+ ? { annotations: annotations }
529
+ : {}),
530
+ };
531
+ }
532
+ async function discoverServerCatalog(runtime, server, timeoutMs) {
533
+ const cached = await readCachedCatalog(server);
534
+ if (cached)
535
+ return cached;
536
+ const lock = `${catalogCachePath(server)}.lock`;
537
+ await mkdir(dirname(lock), { recursive: true });
538
+ let ownsLock = false;
539
+ const deadline = Date.now() + timeoutMs;
540
+ while (!ownsLock && Date.now() < deadline) {
541
+ ownsLock = await acquireCatalogLock(lock);
542
+ if (ownsLock)
543
+ break;
544
+ const populated = await readCachedCatalog(server);
545
+ if (populated)
546
+ return populated;
547
+ if (await catalogLockIsStale(lock, timeoutMs)) {
548
+ await rm(lock, { force: true });
549
+ continue;
550
+ }
551
+ const remainingMs = deadline - Date.now();
552
+ if (remainingMs > 0) {
553
+ await delay(Math.min(CATALOG_LOCK_POLL_MS, remainingMs));
554
+ }
555
+ }
556
+ if (!ownsLock) {
557
+ const populated = await readCachedCatalog(server);
558
+ if (populated)
559
+ return populated;
560
+ throw new Error(`timed out after ${timeoutMs}ms waiting for MCP catalog discovery for '${server.id}'`);
561
+ }
562
+ try {
563
+ // The previous owner may have populated the cache immediately before its
564
+ // process exited and this caller reclaimed the lock.
565
+ const populated = await readCachedCatalog(server);
566
+ if (populated)
567
+ return populated;
568
+ const remainingMs = deadline - Date.now();
569
+ if (remainingMs <= 0) {
570
+ throw new Error(`timed out after ${timeoutMs}ms before MCP catalog discovery for '${server.id}' started`);
571
+ }
572
+ const discovered = (await withListTimeout(runtime.listTools(server.id, {
573
+ includeSchema: true,
574
+ autoAuthorize: false,
575
+ allowCachedAuth: true,
576
+ disableOAuth: true,
577
+ }), remainingMs));
578
+ const tools = filterMcpCatalog(server, discovered.map(catalogEntry));
579
+ await writeCachedCatalog(server, tools);
580
+ return tools;
581
+ }
582
+ finally {
583
+ if (ownsLock)
584
+ await rm(lock, { recursive: true, force: true });
585
+ }
586
+ }
587
+ export async function discoverMcpCatalogs(runtime, timeoutMs) {
588
+ const session = await readSession();
589
+ return await Promise.all(session.servers.map(async (server) => {
590
+ try {
591
+ return {
592
+ id: server.id,
593
+ name: server.name,
594
+ tools: await discoverServerCatalog(runtime, server, timeoutMs),
595
+ };
596
+ }
597
+ catch (error) {
598
+ return {
599
+ id: server.id,
600
+ name: server.name,
601
+ tools: [],
602
+ error: error instanceof Error ? error.message : String(error),
603
+ };
604
+ }
605
+ }));
606
+ }
607
+ function parseSearchArgs(args) {
608
+ const queryParts = [];
609
+ let limit = 8;
610
+ let regex = false;
611
+ for (let index = 0; index < args.length; index += 1) {
612
+ const arg = args[index];
613
+ if (arg === "--json")
614
+ return { error: "mcp search metadata is compact text; JSON is reserved for tool results." };
615
+ if (arg === "--regex") {
616
+ regex = true;
617
+ continue;
618
+ }
619
+ if (arg === "--limit" || arg.startsWith("--limit=")) {
620
+ const raw = arg === "--limit" ? args[++index] : arg.slice("--limit=".length);
621
+ const value = Number(raw);
622
+ if (!Number.isInteger(value) || value <= 0) {
623
+ return { error: `--limit expects a positive integer, got '${raw ?? ""}'.` };
624
+ }
625
+ limit = Math.floor(value);
626
+ continue;
627
+ }
628
+ if (arg.startsWith("-"))
629
+ return { error: `Unknown mcp search option '${arg}'.` };
630
+ queryParts.push(arg);
631
+ }
632
+ return { query: queryParts.join(" "), limit, regex };
633
+ }
634
+ async function searchCatalog(args) {
635
+ const parsed = parseSearchArgs(args);
636
+ if (parsed.error !== undefined) {
637
+ stderr.write(`${parsed.error}\n`);
638
+ return 2;
639
+ }
640
+ const { query, limit, regex } = parsed;
641
+ if (!query.trim()) {
642
+ stderr.write("Usage: mcp search \"what you need\" [--limit N] [--regex]\n");
643
+ return 2;
644
+ }
645
+ let matches;
646
+ const { runtime, owned } = await acquireRuntime();
647
+ try {
648
+ const session = await readSession();
649
+ const results = await Promise.all(session.servers.map(async (server) => {
650
+ try {
651
+ return {
652
+ catalog: await discoverServerCatalog(runtime, server, DEFAULT_LIST_TIMEOUT_MS),
653
+ };
654
+ }
655
+ catch (error) {
656
+ return { error };
657
+ }
658
+ }));
659
+ const failed = results.filter((result) => "error" in result).length;
660
+ if (failed > 0) {
661
+ stderr.write(`mcp search: ${failed} of ${results.length} server(s) unavailable; searched the remaining catalogs.\n`);
662
+ }
663
+ matches = searchMcpTools({
664
+ ...session,
665
+ servers: session.servers.map((server, index) => ({
666
+ ...server,
667
+ catalog: "catalog" in results[index] ? results[index].catalog : [],
668
+ })),
669
+ }, query, { limit, regex });
670
+ }
671
+ catch (err) {
672
+ if (regex && err instanceof SyntaxError) {
673
+ stderr.write(`Invalid --regex pattern: ${query}\n`);
674
+ return 2;
675
+ }
676
+ throw err;
677
+ }
678
+ finally {
679
+ await closeOwnedRuntime(runtime, owned);
680
+ }
681
+ if (matches.length === 0) {
682
+ stdout.write(`No matching tools found for "${query}".\n`);
683
+ stdout.write("Try broader or alternate terms.\n");
684
+ stdout.write("Use `mcp list <server>` only to identify exact tool names, then inspect one candidate with `mcp list <server.tool> --schema`.\n");
685
+ return 0;
686
+ }
687
+ for (const match of matches) {
688
+ stdout.write(`${match.ref}${match.description ? ` — ${match.description}` : ""}\n`);
689
+ if (match.required.length > 0)
690
+ stdout.write(` required: ${match.required.join(", ")}\n`);
691
+ }
692
+ return 0;
693
+ }
694
+ function exactToolTarget(value) {
695
+ if (!value)
696
+ return null;
697
+ const dot = value.indexOf(".");
698
+ if (dot < 1 || dot === value.length - 1)
699
+ return null;
700
+ return { server: value.slice(0, dot), tool: value.slice(dot + 1) };
701
+ }
702
+ function catalogTool(session, server, tool) {
703
+ return session.servers
704
+ ?.find((entry) => entry.id === server)
705
+ ?.catalog?.find((entry) => entry.name === tool);
706
+ }
707
+ function toolCount(count) {
708
+ return `${count} tool${count === 1 ? "" : "s"}`;
709
+ }
710
+ export function parseListTimeoutMs(args) {
711
+ const configured = process.env.MCPORTER_LIST_TIMEOUT;
712
+ let raw = configured && /^[1-9]\d*$/.test(configured) ? configured : String(DEFAULT_LIST_TIMEOUT_MS);
713
+ for (let index = 1; index < args.length; index += 1) {
714
+ const arg = args[index];
715
+ if (arg === "--timeout") {
716
+ const value = args[index + 1];
717
+ if (!value)
718
+ return "mcp list: --timeout requires a value.";
719
+ raw = value;
720
+ break;
721
+ }
722
+ if (arg?.startsWith("--timeout=")) {
723
+ raw = arg.slice("--timeout=".length);
724
+ break;
725
+ }
726
+ }
727
+ if (!/^[1-9]\d*$/.test(raw)) {
728
+ return "mcp list: --timeout must be a positive integer (milliseconds).";
729
+ }
730
+ const timeout = Number(raw);
731
+ return Number.isSafeInteger(timeout)
732
+ ? timeout
733
+ : "mcp list: --timeout must be a positive integer (milliseconds).";
734
+ }
735
+ export function withListTimeout(promise, timeoutMs) {
736
+ return new Promise((resolve, reject) => {
737
+ const timer = setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
738
+ promise.then((value) => {
739
+ clearTimeout(timer);
740
+ resolve(value);
741
+ }, (error) => {
742
+ clearTimeout(timer);
743
+ reject(error);
744
+ });
745
+ });
746
+ }
747
+ export function aggregateListExitCode(hadFailure, args) {
748
+ return hadFailure && (args.includes("--quiet") || args.includes("--exit-code"))
749
+ ? 1
750
+ : 0;
751
+ }
752
+ function writeCompactListError(error) {
753
+ const filter = createDelegatedErrorFilter((text) => stderr.write(text));
754
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
755
+ filter.push(`mcp list: ${message.endsWith("\n") ? message : `${message}\n`}`);
756
+ filter.flush();
757
+ }
758
+ function compactListArgumentError(args) {
759
+ const flags = new Set([
760
+ "--all-parameters",
761
+ "--schema",
762
+ "--verbose",
763
+ "--status",
764
+ "--quiet",
765
+ "--exit-code",
766
+ "--no-oauth",
767
+ ]);
768
+ const start = args[1] && !args[1].startsWith("-") ? 2 : 1;
769
+ for (let index = start; index < args.length; index += 1) {
770
+ const arg = args[index];
771
+ if (arg === "--timeout") {
772
+ index += 1;
773
+ continue;
774
+ }
775
+ if (arg.startsWith("--timeout="))
776
+ continue;
777
+ if (flags.has(arg))
778
+ continue;
779
+ return arg.startsWith("-")
780
+ ? `Unknown mcp list option '${arg}'.`
781
+ : `Unexpected mcp list argument '${arg}'.`;
782
+ }
783
+ return undefined;
784
+ }
785
+ async function compactList(args) {
786
+ if (args.includes("--json")) {
787
+ stderr.write("mcp list metadata is compact text; JSON is reserved for tool results.\n");
788
+ return 2;
789
+ }
790
+ const argumentError = compactListArgumentError(args);
791
+ if (argumentError) {
792
+ stderr.write(`${argumentError}\n`);
793
+ return 2;
794
+ }
795
+ const target = args[1] && !args[1].startsWith("-") ? args[1] : undefined;
796
+ const schema = args.includes("--schema");
797
+ const status = args.includes("--status");
798
+ const verbose = args.includes("--verbose");
799
+ const allParameters = args.includes("--all-parameters");
800
+ const quiet = args.includes("--quiet");
801
+ const timeout = parseListTimeoutMs(args);
802
+ if (typeof timeout === "string") {
803
+ stderr.write(`${timeout}\n`);
804
+ return 2;
805
+ }
806
+ if (schema && !target) {
807
+ stderr.write("mcp list --schema requires one exact tool: mcp list <server>.<tool> --schema\n");
808
+ return 2;
809
+ }
810
+ const session = await readSession();
811
+ if (!target) {
812
+ const { runtime, owned } = await acquireRuntime();
813
+ try {
814
+ const results = await Promise.all(session.servers.map(async (server) => {
815
+ try {
816
+ const tools = await discoverServerCatalog(runtime, server, timeout);
817
+ return { server, tools };
818
+ }
819
+ catch (error) {
820
+ return { server, error };
821
+ }
822
+ }));
823
+ let hadFailure = false;
824
+ for (const result of results) {
825
+ if ("error" in result) {
826
+ hadFailure = true;
827
+ if (!quiet)
828
+ stdout.write(`${result.server.id} — unavailable\n`);
829
+ }
830
+ else if (!quiet) {
831
+ stdout.write(`${result.server.id} — ${toolCount(result.tools.length)}\n`);
832
+ }
833
+ }
834
+ return aggregateListExitCode(hadFailure, args);
835
+ }
836
+ finally {
837
+ await closeOwnedRuntime(runtime, owned);
838
+ }
839
+ }
840
+ const exact = exactToolTarget(target);
841
+ const server = exact?.server ?? target;
842
+ if (schema && !exact) {
843
+ stderr.write("mcp list --schema requires one exact tool: mcp list <server>.<tool> --schema\n");
844
+ return 2;
845
+ }
846
+ const { runtime, owned } = await acquireRuntime();
847
+ try {
848
+ const sessionServer = session.servers.find((entry) => entry.id === server);
849
+ if (!sessionServer) {
850
+ stderr.write(`MCP server '${server}' is not available in this session.\n`);
851
+ return 1;
852
+ }
853
+ const catalog = await discoverServerCatalog(runtime, sessionServer, timeout);
854
+ const tools = catalog.map((tool) => ({
855
+ name: tool.name,
856
+ description: tool.description,
857
+ inputSchema: tool.input_schema,
858
+ outputSchema: tool.output_schema,
859
+ annotations: tool.annotations,
860
+ }));
861
+ if (quiet)
862
+ return 0;
863
+ if (status) {
864
+ stdout.write(`${server} ok — ${toolCount(tools.length)}\n`);
865
+ return 0;
866
+ }
867
+ const selected = exact ? tools.filter((tool) => tool.name === exact.tool) : tools;
868
+ if (exact && selected.length === 0) {
869
+ stderr.write(`Tool '${exact.tool}' is not available on server '${server}'.\n`);
870
+ return 1;
871
+ }
872
+ if (schema && exact) {
873
+ const tool = selected[0];
874
+ if (!tool)
875
+ return 1;
876
+ const outputSchema = tool.outputSchema ?? catalogOutputSchema(catalogTool({ ...session, servers: session.servers.map((entry) => entry.id === server ? { ...entry, catalog } : entry) }, server, exact.tool));
877
+ stdout.write(renderToolContract(server, { ...tool, outputSchema }, { verboseDescriptions: verbose }));
878
+ return 0;
879
+ }
880
+ for (const tool of selected) {
881
+ stdout.write(`${renderToolSignature(server, tool, { allParameters })}\n`);
882
+ if (verbose && tool.description?.trim()) {
883
+ stdout.write(` ${tool.description.trim().replace(/\s+/g, " ")}\n`);
884
+ }
885
+ }
886
+ return 0;
887
+ }
888
+ catch (error) {
889
+ if (!quiet)
890
+ writeCompactListError(error);
891
+ return 1;
892
+ }
893
+ finally {
894
+ await closeOwnedRuntime(runtime, owned);
895
+ }
896
+ }
897
+ // Property names scripts and runtimes probe on plain objects; returning
898
+ // undefined keeps `await tools`, JSON.stringify, and inspection from throwing.
899
+ const PROXY_PROBE_PROPS = new Set(["then", "toJSON", "constructor", "inspect"]);
900
+ function normalizeName(value) {
901
+ return value.replace(/[^a-z0-9]/gi, "").toLowerCase();
902
+ }
903
+ function levenshtein(a, b) {
904
+ if (a === b)
905
+ return 0;
906
+ if (a.length === 0)
907
+ return b.length;
908
+ if (b.length === 0)
909
+ return a.length;
910
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
911
+ for (let i = 1; i <= a.length; i += 1) {
912
+ const current = [i];
913
+ for (let j = 1; j <= b.length; j += 1) {
914
+ current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
915
+ }
916
+ previous = current;
917
+ }
918
+ return previous[b.length];
919
+ }
920
+ function closestName(input, candidates) {
921
+ const normalizedInput = normalizeName(input);
922
+ let best;
923
+ let bestScore = Number.POSITIVE_INFINITY;
924
+ for (const candidate of candidates) {
925
+ const score = levenshtein(normalizedInput, normalizeName(candidate));
926
+ if (score < bestScore) {
927
+ bestScore = score;
928
+ best = candidate;
929
+ }
930
+ }
931
+ if (best === undefined)
932
+ return undefined;
933
+ const baseline = Math.max(normalizedInput.length, normalizeName(best).length, 1);
934
+ return bestScore <= Math.max(2, Math.floor(baseline / 3)) ? best : undefined;
935
+ }
936
+ export function describeUnknownRunServer(server, servers) {
937
+ const suggestion = closestName(server, servers);
938
+ const parts = [`Unknown MCP server '${server}'.`];
939
+ if (suggestion)
940
+ parts.push(`Did you mean 'tools.${suggestion}'?`);
941
+ parts.push(servers.length > 0
942
+ ? `Available servers: ${servers.join(", ")}.`
943
+ : "No MCP servers are configured.");
944
+ return parts.join(" ");
945
+ }
946
+ const RUN_TOOL_REJECTED_PATTERNS = [
947
+ /\btool\s+[\w.-]+\s+not found\b/i,
948
+ /\bunknown tool\b/i,
949
+ ];
950
+ const RUN_TOOL_BLOCKED_PATTERN = /is not accessible on server '[^']+' \(blocked by configuration\)/;
951
+ const RUN_AUTH_REQUIRED_PATTERN = /\b(?:authentication required|unauthenticated|HTTP 401|missing (?:access )?token|invalid (?:access )?token|failed to resolve header ['"]Authorization)/i;
952
+ export function describeUnavailableRunTool(server, tool, knownTools) {
953
+ const suggestion = closestName(tool, knownTools);
954
+ const parts = [`Tool '${tool}' is not available on server '${server}'.`];
955
+ if (suggestion)
956
+ parts.push(`Did you mean '${suggestion}'?`);
957
+ parts.push(`Run \`mcp list ${server}\` to see available tools.`);
958
+ return parts.join(" ");
959
+ }
960
+ function improveRunToolError(error, server, tool, knownTools) {
961
+ const message = error instanceof Error ? error.message : String(error);
962
+ if (RUN_AUTH_REQUIRED_PATTERN.test(message)) {
963
+ return new McpRemoteToolResultError({
964
+ code: "authentication_required",
965
+ message: `Authentication is required for MCP server '${server}'. ` +
966
+ "Ask the user to authenticate this MCP connection outside the agent session, then retry.",
967
+ retryable: false,
968
+ action: "ask_user_to_authenticate",
969
+ });
970
+ }
971
+ if (RUN_TOOL_BLOCKED_PATTERN.test(message)) {
972
+ return new Error(`Tool '${tool}' is not enabled for server '${server}' in this session. ` +
973
+ `Run \`mcp list ${server}\` to inspect the allowlist; if absent, the recipe configuration must grant it.`, { cause: error });
974
+ }
975
+ if (!RUN_TOOL_REJECTED_PATTERNS.some((pattern) => pattern.test(message)))
976
+ return error;
977
+ return new Error(describeUnavailableRunTool(server, tool, knownTools), { cause: error });
978
+ }
979
+ async function createTools(opts) {
980
+ const { runtime, owned } = await acquireRuntime();
981
+ const session = await readSession();
982
+ const knownTools = new Map();
983
+ await Promise.all(session.servers.map(async (server) => {
984
+ knownTools.set(server.id, (await readCachedCatalog(server))?.map((tool) => tool.name) ?? []);
985
+ }));
986
+ // The static session policy is the authority. The mcporter config is a
987
+ // transport projection, not another capability source, so extra config
988
+ // entries must never appear on the run proxy.
989
+ const configuredServers = new Set(runtime.listServers());
990
+ const servers = session.servers.filter((server) => configuredServers.has(server.id));
991
+ const calls = [];
992
+ let callCount = 0;
993
+ const queue = new ToolCallQueue(opts.maxConcurrentCalls);
994
+ const toolsByServer = Object.create(null);
995
+ for (const serverPolicy of servers) {
996
+ const server = serverPolicy.id;
997
+ toolsByServer[server] = new Proxy(Object.create(null), {
998
+ get(_target, property) {
999
+ if (typeof property !== "string" || PROXY_PROBE_PROPS.has(property))
1000
+ return undefined;
1001
+ const startCall = (args, format) => {
1002
+ validateRunToolArgs(server, property, args);
1003
+ const allowedToolNames = knownTools.get(server) ?? [];
1004
+ if (!mcpSessionAllowsTool(serverPolicy, property)) {
1005
+ throw new McpRunToolError(server, property, describeUnavailableRunTool(server, property, allowedToolNames));
1006
+ }
1007
+ if (callCount >= opts.maxCalls) {
1008
+ throw new McpRunUsageError(`mcp run tool-call limit exceeded (${opts.maxCalls}). Split the workflow or raise PI_RECIPES_MCP_RUN_MAX_CALLS.`);
1009
+ }
1010
+ callCount += 1;
1011
+ const rawCall = (async () => {
1012
+ let release;
1013
+ try {
1014
+ release = await queue.acquire();
1015
+ const remainingMs = opts.deadlineMs - Date.now();
1016
+ if (remainingMs <= 0) {
1017
+ throw new McpRunTimeoutError(`mcp run deadline reached before ${server}.${property} started.`);
1018
+ }
1019
+ const result = checkedCallResult(createCallResult(await runtime.callTool(server, property, {
1020
+ args,
1021
+ // A queued call must never outlive the workflow that owns
1022
+ // it. mcporter forwards this timeout to the MCP SDK and
1023
+ // resets the transport when it fires.
1024
+ timeoutMs: Math.min(opts.callTimeoutMs, remainingMs),
1025
+ disableOAuth: true,
1026
+ })));
1027
+ return decodeCallResult(result, format, `${server}.${property}`);
1028
+ }
1029
+ catch (error) {
1030
+ const improved = improveRunToolError(error, server, property, allowedToolNames ?? []);
1031
+ if (improved instanceof McpRunUsageError)
1032
+ throw improved;
1033
+ const message = improved instanceof Error ? improved.message : String(improved);
1034
+ const remoteDetails = improved instanceof McpRemoteToolResultError
1035
+ ? improved.details
1036
+ : error instanceof McpRemoteToolResultError
1037
+ ? error.details
1038
+ : undefined;
1039
+ const timeoutDetails = !remoteDetails &&
1040
+ (improved instanceof McpRunTimeoutError ||
1041
+ /\b(?:timed?\s*out|timeout)\b/i.test(message))
1042
+ ? {
1043
+ code: "timeout",
1044
+ message: improved instanceof McpRunTimeoutError
1045
+ ? message
1046
+ : `MCP tool call ${server}.${property} timed out.`,
1047
+ retryable: true,
1048
+ action: "inspect_state",
1049
+ outcome: improved instanceof McpRunTimeoutError ? "not_started" : "unknown",
1050
+ }
1051
+ : undefined;
1052
+ throw new McpRunToolError(server, property, remoteDetails?.message ?? timeoutDetails?.message ?? message, { cause: improved, details: remoteDetails ?? timeoutDetails });
1053
+ }
1054
+ finally {
1055
+ release?.();
1056
+ }
1057
+ })();
1058
+ const record = {
1059
+ ref: `${server}.${property}`,
1060
+ observed: false,
1061
+ outcome: "pending",
1062
+ promise: rawCall,
1063
+ };
1064
+ calls.push(record);
1065
+ rawCall.then(() => {
1066
+ record.outcome = "succeeded";
1067
+ }, (error) => {
1068
+ record.outcome = "failed";
1069
+ record.error = error instanceof Error ? error.message : String(error);
1070
+ });
1071
+ // JSON.stringify(promise) is "{}" — the classic missing-await
1072
+ // symptom. Make it print a diagnosis instead.
1073
+ return new Proxy(rawCall, {
1074
+ get(target, key) {
1075
+ if (key === "toJSON") {
1076
+ return () => `[pending tool call ${server}.${property} — did you forget await?]`;
1077
+ }
1078
+ if (key === "then" || key === "catch" || key === "finally") {
1079
+ record.observed = true;
1080
+ }
1081
+ const value = Reflect.get(target, key, target);
1082
+ return typeof value === "function" ? value.bind(target) : value;
1083
+ },
1084
+ });
1085
+ };
1086
+ const tool = ((args = {}) => startCall(args, "json"));
1087
+ tool.text = (args = {}) => startCall(args, "text");
1088
+ tool.markdown = (args = {}) => startCall(args, "markdown");
1089
+ tool.images = (args = {}) => startCall(args, "images");
1090
+ tool.content = (args = {}) => startCall(args, "content");
1091
+ tool.structuredContent = (args = {}) => startCall(args, "structuredContent");
1092
+ tool.raw = (args = {}) => startCall(args, "raw");
1093
+ return tool;
1094
+ },
1095
+ });
1096
+ }
1097
+ // Fail with a clear message when a script names a server that does not
1098
+ // exist, instead of "Cannot read properties of undefined".
1099
+ const tools = new Proxy(toolsByServer, {
1100
+ get(target, property) {
1101
+ if (typeof property !== "string" || PROXY_PROBE_PROPS.has(property))
1102
+ return undefined;
1103
+ if (property in target)
1104
+ return target[property];
1105
+ throw new Error(describeUnknownRunServer(property, servers.map((server) => server.id)));
1106
+ },
1107
+ });
1108
+ return {
1109
+ runtime,
1110
+ owned,
1111
+ tools,
1112
+ calls,
1113
+ cancelQueued: (error) => queue.cancel(error),
1114
+ };
1115
+ }
1116
+ // vars.TYPO silently becomes undefined, which strips the argument it feeds
1117
+ // and can turn a targeted query into a match-everything one. Throw instead.
1118
+ function createVars(vars) {
1119
+ return new Proxy({ ...vars }, {
1120
+ get(target, property) {
1121
+ if (typeof property !== "string" || PROXY_PROBE_PROPS.has(property))
1122
+ return undefined;
1123
+ if (property in target)
1124
+ return target[property];
1125
+ const defined = Object.keys(target);
1126
+ throw new Error(`vars.${property} is not defined. Pass it with --var ${property}=value` +
1127
+ (defined.length > 0 ? ` (defined vars: ${defined.join(", ")}).` : " (no vars were passed).") +
1128
+ ` Use \`"${property}" in vars\` to test for optional vars.`);
1129
+ },
1130
+ });
1131
+ }
1132
+ function createRunProcess() {
1133
+ return new Proxy(globalThis.process, {
1134
+ get(target, property) {
1135
+ if (property === "argv") {
1136
+ throw new McpRunUsageError("process.argv is unavailable in mcp run. Pass dynamic values with --var KEY=value and read them as vars.KEY.");
1137
+ }
1138
+ const value = Reflect.get(target, property, target);
1139
+ return typeof value === "function" ? value.bind(target) : value;
1140
+ },
1141
+ });
1142
+ }
1143
+ function concurrentOutcomeError(error, calls) {
1144
+ if (calls.length < 2)
1145
+ return error;
1146
+ const outcomes = calls.map((call) => ({
1147
+ ref: call.ref,
1148
+ outcome: call.outcome,
1149
+ ...(call.error ? { error: call.error } : {}),
1150
+ }));
1151
+ const summary = outcomes.map((call) => `${call.ref}=${call.outcome}`).join(", ");
1152
+ const message = `${error.message} Workflow call outcomes: ${summary}. Inspect state before retrying side-effecting calls.`;
1153
+ return new McpRunToolError(error.server, error.tool, message, {
1154
+ cause: error,
1155
+ details: { ...error.details, message, calls: outcomes },
1156
+ });
1157
+ }
1158
+ function positiveInteger(value, label, fallback) {
1159
+ const parsed = value === undefined ? fallback : Number(value);
1160
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
1161
+ throw new McpRunUsageError(`${label} expects a positive integer, got '${value ?? ""}'.`);
1162
+ }
1163
+ return parsed;
1164
+ }
1165
+ export async function runMcpJavaScript(code, opts = {}) {
1166
+ const timeoutMs = positiveInteger(opts.timeoutMs ?? process.env.PI_RECIPES_MCP_RUN_TIMEOUT_MS, "PI_RECIPES_MCP_RUN_TIMEOUT_MS", DEFAULT_RUN_TIMEOUT_MS);
1167
+ const callTimeoutMs = positiveInteger(opts.callTimeoutMs ?? process.env.PI_RECIPES_MCP_RUN_CALL_TIMEOUT_MS, "PI_RECIPES_MCP_RUN_CALL_TIMEOUT_MS", Math.min(DEFAULT_TOOL_CALL_TIMEOUT_MS, timeoutMs));
1168
+ const maxCalls = positiveInteger(opts.maxCalls ?? process.env.PI_RECIPES_MCP_RUN_MAX_CALLS, "PI_RECIPES_MCP_RUN_MAX_CALLS", DEFAULT_MAX_RUN_TOOL_CALLS);
1169
+ const maxConcurrentCalls = positiveInteger(opts.maxConcurrentCalls ?? process.env.PI_RECIPES_MCP_RUN_MAX_CONCURRENCY, "PI_RECIPES_MCP_RUN_MAX_CONCURRENCY", DEFAULT_MAX_CONCURRENT_TOOL_CALLS);
1170
+ const deadlineMs = Date.now() + timeoutMs;
1171
+ const { runtime, owned, tools, calls, cancelQueued } = await createTools({
1172
+ callTimeoutMs,
1173
+ maxCalls,
1174
+ maxConcurrentCalls,
1175
+ deadlineMs,
1176
+ });
1177
+ const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
1178
+ const run = new AsyncFunction("tools", "vars", "process", code);
1179
+ let timeout;
1180
+ let primaryError;
1181
+ try {
1182
+ let scriptError;
1183
+ try {
1184
+ await Promise.race([
1185
+ run(tools, createVars(opts.vars ?? {}), createRunProcess()),
1186
+ new Promise((_resolve, reject) => {
1187
+ timeout = setTimeout(() => {
1188
+ reject(new McpRunTimeoutError(`mcp run timed out after ${timeoutMs}ms. Remote side effects may already have occurred; inspect state before retrying.`));
1189
+ }, timeoutMs);
1190
+ }),
1191
+ ]);
1192
+ }
1193
+ catch (error) {
1194
+ scriptError = error;
1195
+ }
1196
+ // A workflow deadline is also the cancellation boundary. mcporter 0.12.3
1197
+ // does not expose an AbortSignal, but it does forward each effective
1198
+ // timeout to the MCP SDK. Cancel calls that have not started, then close
1199
+ // the runtime in finally to tear down active transports.
1200
+ if (scriptError instanceof McpRunTimeoutError) {
1201
+ cancelQueued(scriptError);
1202
+ for (const call of calls) {
1203
+ if (call.outcome === "pending")
1204
+ call.outcome = "outcome_unknown";
1205
+ }
1206
+ throw scriptError;
1207
+ }
1208
+ // Accessing .then/.catch is not proof that a detached chain was awaited.
1209
+ // Anything still pending when the script returns is unsafe even if a
1210
+ // handler was attached, because closing the runtime can cut it off.
1211
+ const unsafeAtExit = calls.filter((call) => !call.observed || call.outcome === "pending");
1212
+ if (unsafeAtExit.length > 0) {
1213
+ const remainingSettleMs = Math.max(0, Math.min(15_000, deadlineMs - Date.now()));
1214
+ if (remainingSettleMs > 0) {
1215
+ let settleTimer;
1216
+ try {
1217
+ await Promise.race([
1218
+ Promise.allSettled(unsafeAtExit.map((call) => call.promise)),
1219
+ new Promise((resolve) => {
1220
+ settleTimer = setTimeout(resolve, remainingSettleMs);
1221
+ }),
1222
+ ]);
1223
+ }
1224
+ finally {
1225
+ if (settleTimer)
1226
+ clearTimeout(settleTimer);
1227
+ }
1228
+ }
1229
+ for (const call of unsafeAtExit) {
1230
+ if (call.outcome === "pending")
1231
+ call.outcome = "outcome_unknown";
1232
+ }
1233
+ // Promise.all rejects as soon as one member fails, while its already-
1234
+ // observed siblings keep running. We wait those siblings above before
1235
+ // closing the transport. Once they have settled, do not misreport them
1236
+ // as detached calls; surface the original tool error instead.
1237
+ const remainingUnsafe = unsafeAtExit.filter((call) => !call.observed || call.outcome === "outcome_unknown");
1238
+ if (scriptError instanceof McpRunToolError &&
1239
+ remainingUnsafe.length === 0) {
1240
+ throw concurrentOutcomeError(scriptError, calls);
1241
+ }
1242
+ const summary = unsafeAtExit
1243
+ .map((call) => `${call.ref}=${call.outcome}` +
1244
+ (call.error ? ` (${call.error})` : ""))
1245
+ .join(", ");
1246
+ throw new McpRunUsageError(`mcp run: ${unsafeAtExit.length} tool call(s) were not awaited: ${summary}. ` +
1247
+ "Await or return every tool-call chain before the script exits; " +
1248
+ "attaching .then/.catch without awaiting the chain is not enough. " +
1249
+ "Remote side effects may already have occurred." +
1250
+ (scriptError
1251
+ ? ` The script also failed: ${scriptError instanceof Error ? scriptError.message : String(scriptError)}.`
1252
+ : ""));
1253
+ }
1254
+ if (scriptError)
1255
+ throw scriptError;
1256
+ }
1257
+ catch (error) {
1258
+ primaryError = error;
1259
+ throw error;
1260
+ }
1261
+ finally {
1262
+ if (timeout)
1263
+ clearTimeout(timeout);
1264
+ try {
1265
+ await closeOwnedRuntime(runtime, owned);
1266
+ }
1267
+ catch (closeError) {
1268
+ // Transport cleanup must not replace the typed tool/script error that
1269
+ // tells the agent what actually failed. A close-only failure still
1270
+ // surfaces normally.
1271
+ if (primaryError === undefined)
1272
+ throw closeError;
1273
+ }
1274
+ }
1275
+ }
1276
+ // The soft timeout in runMcpJavaScript is a Promise.race, which a script that
1277
+ // never yields (a synchronous busy-loop) starves forever. Worker threads run
1278
+ // their own event loop, so a watchdog there can still fire and kill the
1279
+ // process with a diagnostic once the deadline plus a grace period passes.
1280
+ const RUN_WATCHDOG_GRACE_MS = 2_000;
1281
+ function startRunWatchdog(timeoutMs) {
1282
+ const worker = new Worker(`const { workerData } = require("node:worker_threads");
1283
+ const { writeSync } = require("node:fs");
1284
+ setTimeout(() => {
1285
+ writeSync(
1286
+ 2,
1287
+ "mcp run: killed after " + workerData.limitMs + "ms. The script never yielded " +
1288
+ "(synchronous busy-loop?) or ignored the timeout. Use await inside loops and " +
1289
+ "set PI_RECIPES_MCP_RUN_TIMEOUT_MS to adjust the limit. Remote side effects " +
1290
+ "may already have occurred; inspect state before retrying.\\n"
1291
+ );
1292
+ process.kill(process.pid, "SIGKILL");
1293
+ }, workerData.limitMs);`, { eval: true, workerData: { limitMs: timeoutMs + RUN_WATCHDOG_GRACE_MS } });
1294
+ worker.unref();
1295
+ return () => void worker.terminate();
1296
+ }
1297
+ async function runCode(args) {
1298
+ const vars = {};
1299
+ const positional = [];
1300
+ let jsonErrors = false;
1301
+ for (let index = 0; index < args.length; index += 1) {
1302
+ const arg = args[index];
1303
+ if (arg === "--json-errors") {
1304
+ jsonErrors = true;
1305
+ continue;
1306
+ }
1307
+ const inline = arg.startsWith("--var=") ? arg.slice("--var=".length) : undefined;
1308
+ const value = inline ?? (arg === "--var" ? args[++index] : undefined);
1309
+ if (arg === "--var" || inline !== undefined) {
1310
+ const eq = value?.indexOf("=") ?? -1;
1311
+ if (value === undefined || eq < 1) {
1312
+ stderr.write("--var expects KEY=value.\n");
1313
+ return 2;
1314
+ }
1315
+ vars[value.slice(0, eq)] = value.slice(eq + 1);
1316
+ continue;
1317
+ }
1318
+ positional.push(arg);
1319
+ }
1320
+ const file = positional[0];
1321
+ if (positional.length > 1) {
1322
+ stderr.write("mcp run accepts at most one file path.\n");
1323
+ return 2;
1324
+ }
1325
+ const code = file ? await readFile(file, "utf8") : await readStdin();
1326
+ if (!code.trim()) {
1327
+ stderr.write("mcp run: empty script. Pass a file path or pipe JavaScript on stdin (see `mcp run --help`).\n");
1328
+ return 2;
1329
+ }
1330
+ let timeoutMs;
1331
+ try {
1332
+ timeoutMs = positiveInteger(process.env.PI_RECIPES_MCP_RUN_TIMEOUT_MS, "PI_RECIPES_MCP_RUN_TIMEOUT_MS", DEFAULT_RUN_TIMEOUT_MS);
1333
+ }
1334
+ catch (error) {
1335
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1336
+ return 2;
1337
+ }
1338
+ // Daemon-backed runs execute inside a disposable worker. The daemon owns
1339
+ // that worker's hard deadline, so a synchronous loop cannot kill the shared
1340
+ // MCP host process.
1341
+ const stopWatchdog = isMainThread ? startRunWatchdog(timeoutMs) : () => { };
1342
+ try {
1343
+ await runMcpJavaScript(code, { vars, timeoutMs });
1344
+ }
1345
+ catch (error) {
1346
+ // Keep the error class visible for non-generic failures (SyntaxError,
1347
+ // TypeError, ...) so script bugs read differently from tool failures.
1348
+ const message = error instanceof Error
1349
+ ? error.name === "Error"
1350
+ ? error.message
1351
+ : `${error.name}: ${error.message}`
1352
+ : String(error);
1353
+ const prefixed = message.startsWith("mcp run") ? message : `mcp run: ${message}`;
1354
+ const code = error instanceof McpRunUsageError ? 2 : 1;
1355
+ if (jsonErrors) {
1356
+ const remoteDetails = error instanceof McpRunToolError ? error.details : undefined;
1357
+ stderr.write(`${JSON.stringify({
1358
+ error: {
1359
+ ...remoteDetails,
1360
+ code: remoteDetails?.code ??
1361
+ (error instanceof McpRunUsageError
1362
+ ? "invalid_usage"
1363
+ : error instanceof McpRunToolError
1364
+ ? "tool_execution"
1365
+ : error instanceof McpRunTimeoutError
1366
+ ? "timeout"
1367
+ : "script_failure"),
1368
+ message: remoteDetails?.message ?? prefixed,
1369
+ retryable: remoteDetails?.retryable ?? false,
1370
+ action: remoteDetails?.action ??
1371
+ (error instanceof McpRunUsageError
1372
+ ? "correct_script"
1373
+ : error instanceof McpRunTimeoutError
1374
+ ? "inspect_state"
1375
+ : "inspect_error"),
1376
+ ...(error instanceof McpRunTimeoutError
1377
+ ? { outcome: error.outcome }
1378
+ : {}),
1379
+ ...(error instanceof McpRunToolError
1380
+ ? { server: error.server, tool: error.tool }
1381
+ : {}),
1382
+ },
1383
+ })}\n`);
1384
+ }
1385
+ else {
1386
+ stderr.write(`${prefixed}\n`);
1387
+ }
1388
+ return code;
1389
+ }
1390
+ finally {
1391
+ stopWatchdog();
1392
+ }
1393
+ return 0;
1394
+ }
1395
+ export function createDelegatedErrorFilter(write) {
1396
+ let buffer = "";
1397
+ let sawOAuthMetadataError = false;
1398
+ const emit = (line) => {
1399
+ // Keep mcporter's message, but omit implementation stack frames that do
1400
+ // not help an agent recover.
1401
+ if (/^\s+at\s\S/.test(line))
1402
+ return;
1403
+ if (/trying to load OAuth metadata/.test(line))
1404
+ sawOAuthMetadataError = true;
1405
+ const safe = line
1406
+ .replace(/Tool '([^']+)' is not accessible on server '([^']+)' \(blocked by configuration\)/g, "Tool '$1' is not enabled on server '$2' in this recipe session")
1407
+ .replace(/Failed to resolve header 'Authorization' for server '([^']+)': Environment variable\(s\) [A-Z0-9_, ]+ must be set for MCP header substitution\./g, "Authentication is required for MCP server '$1'. Ask the user to authenticate this MCP connection outside the agent session, then retry.")
1408
+ .replace(/Next: run ['`]mcporter auth [^'`]+['`] to finish authentication\.?/gi, "Authentication is required. Ask the user to authenticate this MCP connection outside the agent session, then retry.");
1409
+ write(safe);
1410
+ };
1411
+ return {
1412
+ push(chunk) {
1413
+ buffer += chunk;
1414
+ let index = buffer.indexOf("\n");
1415
+ while (index !== -1) {
1416
+ emit(buffer.slice(0, index + 1));
1417
+ buffer = buffer.slice(index + 1);
1418
+ index = buffer.indexOf("\n");
1419
+ }
1420
+ },
1421
+ flush() {
1422
+ if (buffer)
1423
+ emit(buffer);
1424
+ buffer = "";
1425
+ if (sawOAuthMetadataError) {
1426
+ write("Hint: this server is called with a configured bearer token; the token may be invalid or expired.\n");
1427
+ sawOAuthMetadataError = false;
1428
+ }
1429
+ },
1430
+ };
1431
+ }
1432
+ export function duplicateCallArgumentKeys(args) {
1433
+ const seen = new Set();
1434
+ const duplicates = new Set();
1435
+ for (const token of args.slice(1)) {
1436
+ const rawKey = token.match(/^([A-Za-z_][A-Za-z0-9_.-]*)=/)?.[1];
1437
+ if (!rawKey)
1438
+ continue;
1439
+ const key = rawKey.replace(/-([a-zA-Z0-9])/g, (_match, char) => char.toUpperCase());
1440
+ if (seen.has(key))
1441
+ duplicates.add(key);
1442
+ seen.add(key);
1443
+ }
1444
+ return [...duplicates];
1445
+ }
1446
+ export function callJsonArgumentError(args) {
1447
+ for (let index = 1; index < args.length; index += 1) {
1448
+ const arg = args[index];
1449
+ if (arg === "--json") {
1450
+ const value = args[index + 1];
1451
+ if (value === undefined || (value.startsWith("--") && value !== "-")) {
1452
+ return `mcp call: ${arg} expects a JSON object or - for stdin. Example: mcp call server.tool --json '{"tags":["a","b"]}'`;
1453
+ }
1454
+ index += 1;
1455
+ continue;
1456
+ }
1457
+ if (arg === "--json=") {
1458
+ return `mcp call: ${arg.slice(0, -1)} expects a JSON object or - for stdin.`;
1459
+ }
1460
+ }
1461
+ return null;
1462
+ }
1463
+ export function malformedCallExpression(args) {
1464
+ const ref = args.find((arg) => !arg.startsWith("-"));
1465
+ if (!ref || !/[()]/.test(ref))
1466
+ return null;
1467
+ return (`mcp call: function-call expressions are unavailable. ` +
1468
+ `Use mcp call <server>.<tool> key=value or --json for structured arguments.`);
1469
+ }
1470
+ async function callWithMcporter(args) {
1471
+ const shared = currentMcpCommandContext()?.runtime;
1472
+ if (shared)
1473
+ return callWithSharedRuntime(shared, args);
1474
+ const previousDisableAutorun = process.env.MCPORTER_DISABLE_AUTORUN;
1475
+ const previousExitCode = process.exitCode;
1476
+ process.env.MCPORTER_DISABLE_AUTORUN = "1";
1477
+ process.exitCode = undefined;
1478
+ let runtime;
1479
+ let owned = false;
1480
+ try {
1481
+ ({ runtime, owned } = await acquireRuntime());
1482
+ const { handleCall } = await import("mcporter/cli");
1483
+ await handleCall(runtime, args);
1484
+ return process.exitCode ?? 0;
1485
+ }
1486
+ finally {
1487
+ process.exitCode = previousExitCode;
1488
+ if (previousDisableAutorun === undefined) {
1489
+ delete process.env.MCPORTER_DISABLE_AUTORUN;
1490
+ }
1491
+ else {
1492
+ process.env.MCPORTER_DISABLE_AUTORUN = previousDisableAutorun;
1493
+ }
1494
+ if (runtime && owned)
1495
+ await runtime.close().catch(() => { });
1496
+ }
1497
+ }
1498
+ async function callWithSharedRuntime(runtime, args) {
1499
+ const selector = args[0];
1500
+ const separator = selector?.indexOf(".") ?? -1;
1501
+ if (!selector || separator <= 0 || separator === selector.length - 1) {
1502
+ stderr.write("mcp call requires <server>.<tool>.\n");
1503
+ return 2;
1504
+ }
1505
+ const server = selector.slice(0, separator);
1506
+ const tool = selector.slice(separator + 1);
1507
+ const values = {};
1508
+ let timeoutMs;
1509
+ let output;
1510
+ for (let index = 1; index < args.length; index += 1) {
1511
+ const token = args[index];
1512
+ if (token === "--no-oauth")
1513
+ continue;
1514
+ if (token === "--json" || token.startsWith("--json=")) {
1515
+ const raw = token === "--json" ? args[++index] : token.slice("--json=".length);
1516
+ const source = raw === "-" ? await readStdin() : raw;
1517
+ if (!source) {
1518
+ stderr.write("mcp call: --json expects an object or -.\n");
1519
+ return 2;
1520
+ }
1521
+ let parsed;
1522
+ try {
1523
+ parsed = JSON.parse(source);
1524
+ }
1525
+ catch {
1526
+ stderr.write("mcp call: --json contains invalid JSON.\n");
1527
+ return 2;
1528
+ }
1529
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1530
+ stderr.write("mcp call: --json expects a JSON object.\n");
1531
+ return 2;
1532
+ }
1533
+ Object.assign(values, parsed);
1534
+ continue;
1535
+ }
1536
+ if (token === "--timeout" || token.startsWith("--timeout=")) {
1537
+ const raw = token === "--timeout" ? args[++index] : token.slice("--timeout=".length);
1538
+ const parsed = Number(raw);
1539
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1540
+ stderr.write("mcp call: --timeout expects a positive number.\n");
1541
+ return 2;
1542
+ }
1543
+ timeoutMs = parsed;
1544
+ continue;
1545
+ }
1546
+ if (token === "--output" || token.startsWith("--output=")) {
1547
+ const raw = token === "--output" ? args[++index] : token.slice("--output=".length);
1548
+ if (!raw || !["text", "markdown", "json", "raw"].includes(raw)) {
1549
+ stderr.write("mcp call: --output expects text, markdown, json, or raw.\n");
1550
+ return 2;
1551
+ }
1552
+ output = raw;
1553
+ continue;
1554
+ }
1555
+ const equals = token.indexOf("=");
1556
+ if (equals <= 0) {
1557
+ stderr.write(`mcp call: invalid argument '${token}'.\n`);
1558
+ return 2;
1559
+ }
1560
+ const key = token.slice(0, equals).replace(/-([a-zA-Z0-9])/g, (_match, char) => char.toUpperCase());
1561
+ let raw = token.slice(equals + 1);
1562
+ if (raw.startsWith("@@"))
1563
+ raw = raw.slice(1);
1564
+ else if (raw.startsWith("@"))
1565
+ raw = await readFile(raw.slice(1), "utf8");
1566
+ try {
1567
+ values[key] = JSON.parse(raw);
1568
+ }
1569
+ catch {
1570
+ values[key] = raw;
1571
+ }
1572
+ }
1573
+ try {
1574
+ const call = runtime.callTool(server, tool, {
1575
+ args: values,
1576
+ timeoutMs,
1577
+ disableOAuth: true,
1578
+ });
1579
+ const signal = currentMcpCommandContext()?.signal;
1580
+ let cancelled;
1581
+ const raw = signal
1582
+ ? await Promise.race([
1583
+ call,
1584
+ new Promise((_resolve, reject) => {
1585
+ cancelled = () => reject(new Error("MCP command cancelled."));
1586
+ if (signal.aborted)
1587
+ cancelled();
1588
+ else
1589
+ signal.addEventListener("abort", cancelled, { once: true });
1590
+ }),
1591
+ ]).finally(() => {
1592
+ if (cancelled)
1593
+ signal.removeEventListener("abort", cancelled);
1594
+ })
1595
+ : await call;
1596
+ const result = createCallResult(raw);
1597
+ let rendered;
1598
+ if (output === "raw")
1599
+ rendered = raw;
1600
+ else if (output === "json")
1601
+ rendered = result.json();
1602
+ else if (output === "markdown")
1603
+ rendered = result.markdown();
1604
+ else if (output === "text")
1605
+ rendered = result.text();
1606
+ else
1607
+ rendered = result.json() ?? result.structuredContent() ?? result.text() ?? raw;
1608
+ const serialized = typeof rendered === "string" ? rendered : JSON.stringify(rendered, null, 2);
1609
+ const configuredLimit = process.env.PI_RECIPES_MCP_MAX_OUTPUT_BYTES;
1610
+ if (configuredLimit !== undefined) {
1611
+ const limit = Number(configuredLimit);
1612
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
1613
+ stderr.write("PI_RECIPES_MCP_MAX_OUTPUT_BYTES must be a positive integer.\n");
1614
+ return 2;
1615
+ }
1616
+ const size = Buffer.byteLength(serialized, "utf8");
1617
+ if (size > limit) {
1618
+ stderr.write(`MCP result is ${size} bytes, exceeding PI_RECIPES_MCP_MAX_OUTPUT_BYTES=${limit}. ` +
1619
+ "Use tool pagination or a narrower field projection.\n");
1620
+ return 1;
1621
+ }
1622
+ }
1623
+ stdout.write(`${serialized}\n`);
1624
+ return asRecord(raw).isError === true ? 1 : 0;
1625
+ }
1626
+ catch (error) {
1627
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1628
+ return 1;
1629
+ }
1630
+ }
1631
+ export async function main(args = process.argv.slice(2)) {
1632
+ if (args.length === 0 ||
1633
+ args[0] === "--help" ||
1634
+ args[0] === "-h" ||
1635
+ (args[0] === "help" && args.length === 1)) {
1636
+ stdout.write(`${mcpCliHelpText()}\n`);
1637
+ return 0;
1638
+ }
1639
+ if (args[0] === "search" && isHelpArg(args[1])) {
1640
+ stdout.write(`${mcpSearchHelpText()}\n`);
1641
+ return 0;
1642
+ }
1643
+ if (args[0] === "list" && args.slice(1).some(isHelpArg)) {
1644
+ stdout.write(`${mcpListHelpText()}\n`);
1645
+ return 0;
1646
+ }
1647
+ if (args[0] === "call" && args.slice(1).some(isHelpArg)) {
1648
+ stdout.write(`${mcpCallHelpText()}\n`);
1649
+ return 0;
1650
+ }
1651
+ if (args[0] === "run" && isHelpArg(args[1])) {
1652
+ stdout.write(`${mcpRunHelpText()}\n`);
1653
+ return 0;
1654
+ }
1655
+ pinSessionMcporterConfig();
1656
+ if (args[0] === "run")
1657
+ return runCode(args.slice(1));
1658
+ if (args[0] === "search")
1659
+ return searchCatalog(args.slice(1));
1660
+ if (args[0] === "call") {
1661
+ const malformed = malformedCallExpression(args.slice(1));
1662
+ if (malformed) {
1663
+ stderr.write(`${malformed}\n`);
1664
+ return 2;
1665
+ }
1666
+ const jsonArgumentError = callJsonArgumentError(args.slice(1));
1667
+ if (jsonArgumentError) {
1668
+ stderr.write(`${jsonArgumentError}\n`);
1669
+ return 2;
1670
+ }
1671
+ }
1672
+ let validated;
1673
+ try {
1674
+ validated = validateDelegatedMcpCommand(args, await sessionCliPolicy());
1675
+ }
1676
+ catch (error) {
1677
+ stderr.write(`mcp: failed to load the session MCP policy: ${error instanceof Error ? error.message : String(error)}\n`);
1678
+ return 1;
1679
+ }
1680
+ if (validated.error) {
1681
+ stderr.write(`mcp: ${validated.error}\n`);
1682
+ return 2;
1683
+ }
1684
+ if (!validated.command) {
1685
+ stderr.write("mcp: invalid session command policy result.\n");
1686
+ return 1;
1687
+ }
1688
+ if (args[0] === "call") {
1689
+ const duplicates = duplicateCallArgumentKeys(validated.command.args.slice(1));
1690
+ if (duplicates.length > 0) {
1691
+ stderr.write(`mcp call: argument${duplicates.length === 1 ? "" : "s"} ${duplicates
1692
+ .map((key) => `'${key}'`)
1693
+ .join(", ")} ${duplicates.length === 1 ? "was" : "were"} passed more than once. ` +
1694
+ "Pass each argument exactly once.\n");
1695
+ return 2;
1696
+ }
1697
+ }
1698
+ const delegatedArgs = validated.command.args;
1699
+ if (delegatedArgs[0] === "list")
1700
+ return compactList(delegatedArgs);
1701
+ if (delegatedArgs[0] === "call") {
1702
+ return callWithMcporter(delegatedArgs.slice(1));
1703
+ }
1704
+ stderr.write(`mcp: command '${delegatedArgs[0]}' is unavailable in recipe sessions.\n`);
1705
+ return 2;
1706
+ }
1707
+ export async function executeMcpCommand(opts) {
1708
+ const context = {
1709
+ runtime: opts.runtime,
1710
+ stdin: opts.stdin,
1711
+ stdout: opts.stdout,
1712
+ stderr: opts.stderr,
1713
+ signal: opts.signal,
1714
+ };
1715
+ return runWithMcpCommandContext(context, () => main(opts.args));
1716
+ }
1717
+ //# sourceMappingURL=mcp-cli-core.js.map