pi-mcp-adapter 2.18.0 → 2.19.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.
- package/CHANGELOG.md +13 -0
- package/README.md +31 -9
- package/direct-tools.ts +1 -1
- package/index.ts +4 -4
- package/mcp-code.ts +112 -55
- package/mcp-script-worker.mjs +20 -7
- package/package.json +1 -1
- package/skills/mcp-scripting/SKILL.md +9 -7
- package/types.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.19.0] - 2026-08-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `mcp_script` now records each search, describe, and call with its input, outcome, and duration in result details; emitted, returned, and console values retain readable Maps, Sets, cycles, functions, symbols, and BigInts. Its docs now lead with the plain JavaScript agents write and position it as the primary MCP multi-call workflow surface.
|
|
14
|
+
- Documented how to hide the bundled `mcp-scripting` Pi skill while keeping the adapter extension installed. Thanks @aryzing for issue #267.
|
|
15
|
+
- Documented Linux revoked-keyring recovery in the OAuth guide and `_meta.ui.visibility` behavior in the MCP UI guide.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `mcp_script` is now registered by default for trusted JavaScript MCP multi-call workflows, while `mcp` remains the right tool for status, discovery, auth, and single calls. Set `settings.scriptMode` to `false` to hide the tool.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- `mcp_script` traces now include missing describe attempts, and shared acyclic values no longer render as circular in script output formatting.
|
|
22
|
+
|
|
10
23
|
## [2.18.0] - 2026-08-02
|
|
11
24
|
|
|
12
25
|
### Added
|
package/README.md
CHANGED
|
@@ -263,7 +263,6 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
263
263
|
"mcpFooterStatus": "full",
|
|
264
264
|
"hostConfigDiscovery": "off",
|
|
265
265
|
"approveTools": ["github_delete_*", "notion_update_*"],
|
|
266
|
-
"scriptMode": true,
|
|
267
266
|
"oauthDir": ".pi/mcp-oauth",
|
|
268
267
|
"trace": {
|
|
269
268
|
"enabled": true,
|
|
@@ -289,7 +288,7 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
289
288
|
| `mcpServers.<name>.oauth.authorizationParams` | Extra authorization URL parameters for provider-specific OAuth extensions. Flow-owned parameters such as `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, `response_type`, and `resource` cannot be overridden. |
|
|
290
289
|
| `directTools` | Global default for all servers (default: false). Per-server overrides this. |
|
|
291
290
|
| `freezeDirectTools` | Keep direct-tool registration stable after the initial sync so automatic reconnects and list-change notifications do not rebuild the system prompt. Use `mcp({ connect: "server" })` or `/mcp reconnect <server>` to refresh deliberately. Default: false. |
|
|
292
|
-
| `scriptMode` | Register the MCP-only `mcp_script` plain-JavaScript tool (default:
|
|
291
|
+
| `scriptMode` | Register the MCP-only `mcp_script` plain-JavaScript tool (default: true). Set to `false` to hide it. |
|
|
293
292
|
| `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
|
|
294
293
|
| `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
|
|
295
294
|
| `sampling` | Allow MCP servers to sample through Pi models, honoring `modelPreferences.hints` before current/default fallback (default: true when UI approval is available). |
|
|
@@ -340,17 +339,39 @@ Set `"outputGuard": false` — or the env kill switch `MCP_OUTPUT_GUARD=0` — t
|
|
|
340
339
|
|
|
341
340
|
### MCP Scripting
|
|
342
341
|
|
|
343
|
-
|
|
342
|
+
For multi-call MCP work, write ordinary JavaScript: discover, inspect, call, loop, filter, chain, or fan out, then return one result. Run that code with the default-on `mcp_script` tool. For a single MCP call, search, describe, status check, or auth action, use `mcp` instead. Set `settings.scriptMode` to `false` to hide the scripting tool.
|
|
343
|
+
|
|
344
|
+
The bundled `mcp-scripting` skill is a separate Pi package resource. To hide that skill while keeping the adapter extension installed, replace the package entry in Pi settings with the object form and disable package skills:
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"packages": [
|
|
349
|
+
{ "source": "npm:pi-mcp-adapter", "skills": [] }
|
|
350
|
+
]
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Preserve any version pin in `source` if your existing package entry has one. You can also disable package resources through `pi config`.
|
|
355
|
+
|
|
356
|
+
For example, this is the JavaScript passed as the `code` argument to `mcp_script`:
|
|
344
357
|
|
|
345
358
|
```js
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
359
|
+
const { items } = await tools.search({ query: "search issues", server: "github" });
|
|
360
|
+
const candidate = items[0];
|
|
361
|
+
if (!candidate) return { error: "No matching tool" };
|
|
362
|
+
|
|
363
|
+
const details = await tools.describe({ path: candidate.path });
|
|
364
|
+
if (details.error) return details;
|
|
365
|
+
|
|
366
|
+
const result = await tools.call(details.path, { query: "is:open label:bug" });
|
|
367
|
+
if (!result.ok) return result;
|
|
368
|
+
emit({ tool: details.path, completed: true });
|
|
369
|
+
return result.data;
|
|
351
370
|
```
|
|
352
371
|
|
|
353
|
-
|
|
372
|
+
See the bundled `mcp-scripting` skill for the complete workflow guide. The API is `await tools.search({ query, server?, limit?, offset? })`, `await tools.describe({ path })`, `tools.call(path, args)`, direct flat calls, `emit(value)`, and a captured `console`. Use ordinary JavaScript loops and Promise utilities for composition; fluent helpers such as `tools.find(...).one()`, `tools.parallel(...)`, and `tools.retry(...)` are not provided. MCP calls return `{ ok: true, data }` or `{ ok: false, error: { code, message } }`, so a failed call does not stop the rest of the script. Result details include a concise `calls` trace with each operation, its path or query, outcome, and duration. Emitted values and console output appear before the script's final return value, and the combined result uses the normal MCP output guard. The default timeout is 30 seconds; each script runs in a worker thread that is terminated at the deadline, including for infinite loops.
|
|
373
|
+
|
|
374
|
+
For a tool-restricted subagent, launch the child Pi with its tool allowlist set to `["mcp_script"]`. Have the parent discover MCP tool names with `mcp({ search: "..." })` and include the relevant prefixed names in the child's task; the child can then loop, filter, and chain those MCP calls without filesystem, shell, or edit tools. The adapter's ordinary lazy connection, authentication, output guard, abort handling, and approval gates still apply to every call.
|
|
354
375
|
|
|
355
376
|
`mcp_script` is a trusted agent-authored MCP scripting layer, not an isolation boundary. If you need isolation, run Pi in an isolated environment. It is distinct from Pi's code-mode skill: Pi's skill batches general Pi tools, while `mcp_script` exposes MCP calls only and can be the child's sole tool.
|
|
356
377
|
|
|
@@ -510,6 +531,7 @@ Returns accumulated messages from UI sessions. Each message includes `type`, `se
|
|
|
510
531
|
**Technical notes:**
|
|
511
532
|
|
|
512
533
|
- Tool consent gates whether UIs can call MCP tools (never/once-per-server/always)
|
|
534
|
+
- `_meta.ui.visibility` controls audience: tools marked app-only stay out of the model tool list, and tools marked model-only cannot be called from the UI iframe.
|
|
513
535
|
- Works with both stdio and HTTP MCP servers
|
|
514
536
|
- Uses a local 408KB AppBridge bundle (MCP SDK + Zod) for browser↔server communication
|
|
515
537
|
- Enforces CSP from standard `_meta.ui.csp` and OpenAI-compatible `_meta["openai/widgetCSP"]` metadata in the response header while preserving provider HTML.
|
package/direct-tools.ts
CHANGED
|
@@ -207,7 +207,7 @@ export function buildProxyDescription(
|
|
|
207
207
|
directSpecs: DirectToolSpec[],
|
|
208
208
|
): string {
|
|
209
209
|
const prefix = config.settings?.toolPrefix ?? "server";
|
|
210
|
-
let desc = `MCP gateway
|
|
210
|
+
let desc = `MCP gateway — server status, tool search/describe, auth, and single MCP tool calls. When one request needs several MCP calls with logic between them, use mcp_script. Non-MCP Pi tools should be called directly, not through mcp.\n`;
|
|
211
211
|
|
|
212
212
|
const directByServer = new Map<string, number>();
|
|
213
213
|
for (const spec of directSpecs) {
|
package/index.ts
CHANGED
|
@@ -607,12 +607,12 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
607
607
|
},
|
|
608
608
|
});
|
|
609
609
|
|
|
610
|
-
if (earlyConfig.settings?.scriptMode
|
|
610
|
+
if (earlyConfig.settings?.scriptMode !== false) {
|
|
611
611
|
(pi.registerTool as (tool: unknown) => unknown)({
|
|
612
612
|
name: "mcp_script",
|
|
613
613
|
label: "MCP Script",
|
|
614
|
-
description: "Run
|
|
615
|
-
promptSnippet: "
|
|
614
|
+
description: "Run trusted JavaScript that makes multiple MCP tool calls in one request — loop, filter, chain, or fan out between calls. For a single MCP call, search, describe, status check, or auth action, use the mcp tool instead. Discover with await tools.search({ query }) — resolves to { items: [{ path, name, server, description? }], total, hasMore, nextOffset }, not an { ok, data } envelope. Inspect with await tools.describe({ path }) — resolves to the tool descriptor with inputTypeScript, or { path, error: { code, message, suggestions } }. Then call tools.call(path, args) — resolves to { ok: true, data } or { ok: false, error: { code, message } } — or use direct flat calls when the name is already known; use emit(value) for user-visible output. Load the mcp-scripting skill for the full workflow guide.",
|
|
615
|
+
promptSnippet: "Batch multiple MCP tool calls in one JavaScript request (loop, filter, chain)",
|
|
616
616
|
parameters: Type.Object({
|
|
617
617
|
code: Type.String({ description: "Trusted JavaScript MCP script. Use tools.<prefixedToolName>(args) and emit(value)." }),
|
|
618
618
|
// Raw JSON schema: host TypeBox shims may omit Type.Number (see index-lifecycle shim test).
|
|
@@ -658,7 +658,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
658
658
|
name: "mcp",
|
|
659
659
|
label: "MCP",
|
|
660
660
|
description,
|
|
661
|
-
promptSnippet: "MCP gateway
|
|
661
|
+
promptSnippet: "MCP gateway — status, search, describe, auth, and single MCP tool calls",
|
|
662
662
|
renderCall: renderMcpProxyToolCall,
|
|
663
663
|
parameters: Type.Object({
|
|
664
664
|
tool: Type.Optional(Type.String({ description: "Tool name to call (e.g., 'xcodebuild_list_sims')" })),
|
package/mcp-code.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolInfo } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { formatWithOptions } from "node:util";
|
|
2
3
|
import { Worker } from "node:worker_threads";
|
|
3
4
|
import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
|
|
4
5
|
import { executeCall } from "./proxy-modes.ts";
|
|
@@ -30,17 +31,29 @@ type WorkerMessage =
|
|
|
30
31
|
|
|
31
32
|
type WorkerResultMessage = { type: "result"; id: number; envelope: unknown };
|
|
32
33
|
|
|
34
|
+
function needsInspectableFormatting(value: unknown, stack = new WeakSet<object>()): boolean {
|
|
35
|
+
if (value === undefined || typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") return true;
|
|
36
|
+
if (typeof value !== "object" || value === null) return false;
|
|
37
|
+
if (stack.has(value)) return true;
|
|
38
|
+
if (value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) return true;
|
|
39
|
+
stack.add(value);
|
|
40
|
+
try {
|
|
41
|
+
return Object.values(value).some((entry) => needsInspectableFormatting(entry, stack));
|
|
42
|
+
} finally {
|
|
43
|
+
stack.delete(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
function formatValue(value: unknown): string {
|
|
34
48
|
if (typeof value === "string") return value;
|
|
35
|
-
if (value === undefined) return "undefined";
|
|
36
49
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return String(value);
|
|
41
|
-
} catch {
|
|
42
|
-
return "[unserializable value]";
|
|
50
|
+
if (!needsInspectableFormatting(value)) {
|
|
51
|
+
const json = JSON.stringify(value, null, 2);
|
|
52
|
+
if (json !== undefined) return json;
|
|
43
53
|
}
|
|
54
|
+
return formatWithOptions({ colors: false, depth: 6 }, value);
|
|
55
|
+
} catch {
|
|
56
|
+
return "[unserializable value]";
|
|
44
57
|
}
|
|
45
58
|
}
|
|
46
59
|
|
|
@@ -106,25 +119,45 @@ export async function runMcpScript(
|
|
|
106
119
|
const timeoutController = new AbortController();
|
|
107
120
|
const callSignal = combineAbortSignals(externalSignal, timeoutController.signal);
|
|
108
121
|
|
|
109
|
-
type
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
type ScriptOperation =
|
|
123
|
+
| { operation: "call"; path: string; ok: true; durationMs: number }
|
|
124
|
+
| { operation: "call"; path: string; ok: false; error: string; durationMs: number }
|
|
125
|
+
| { operation: "search"; query: string; ok: true; durationMs: number }
|
|
126
|
+
| { operation: "search"; query: string; ok: false; error: string; durationMs: number }
|
|
127
|
+
| { operation: "describe"; path: string; ok: true; durationMs: number }
|
|
128
|
+
| { operation: "describe"; path: string; ok: false; error: string; durationMs: number };
|
|
129
|
+
type TrackedScriptOperation = ScriptOperation & { startedAt: number };
|
|
130
|
+
const calls: TrackedScriptOperation[] = [];
|
|
131
|
+
const snapshotCalls = (): ScriptOperation[] => calls.map(({ startedAt, ...operation }) => ({
|
|
132
|
+
...operation,
|
|
133
|
+
durationMs: "error" in operation && operation.error === "incomplete"
|
|
134
|
+
? Math.max(0, Date.now() - startedAt)
|
|
135
|
+
: operation.durationMs,
|
|
136
|
+
}));
|
|
137
|
+
let callsSnapshot: ScriptOperation[] | undefined;
|
|
112
138
|
const callTool = async (path: string, args?: Record<string, unknown>) => {
|
|
113
139
|
// Record before dispatch so calls still in flight at timeout/abort appear in the trace.
|
|
114
|
-
const
|
|
140
|
+
const startedAt = Date.now();
|
|
141
|
+
const index = calls.push({ operation: "call", path, ok: false, error: "incomplete", durationMs: 0, startedAt }) - 1;
|
|
115
142
|
const result = await executeCall(state, path, args, undefined, getPiTools, callSignal);
|
|
116
143
|
const details = result.details;
|
|
117
144
|
if (details.error !== undefined) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
const errorCode = String(details.error);
|
|
146
|
+
const suggestions = Array.isArray(details.suggestions)
|
|
147
|
+
? details.suggestions.filter((suggestion): suggestion is string => typeof suggestion === "string")
|
|
148
|
+
: [];
|
|
149
|
+
const message = errorCode === "tool_not_found"
|
|
150
|
+
? `Tool "${path}" not found. Use await tools.search({ query: "..." }) inside mcp_script.${suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}` : ""}`
|
|
151
|
+
: typeof details.message === "string"
|
|
152
|
+
? details.message
|
|
153
|
+
: textFromContent(result.content);
|
|
154
|
+
calls[index] = { operation: "call", path, ok: false, error: errorCode, durationMs: Date.now() - startedAt, startedAt };
|
|
122
155
|
return {
|
|
123
156
|
ok: false as const,
|
|
124
|
-
error: { code:
|
|
157
|
+
error: { code: errorCode, message },
|
|
125
158
|
};
|
|
126
159
|
}
|
|
127
|
-
calls[index] = { path, ok: true };
|
|
160
|
+
calls[index] = { operation: "call", path, ok: true, durationMs: Date.now() - startedAt, startedAt };
|
|
128
161
|
return {
|
|
129
162
|
ok: true as const,
|
|
130
163
|
data: details.mcpResult !== undefined ? details.mcpResult : textFromContent(result.content),
|
|
@@ -132,48 +165,72 @@ export async function runMcpScript(
|
|
|
132
165
|
};
|
|
133
166
|
|
|
134
167
|
const searchTools = (input?: SearchInput) => {
|
|
135
|
-
|
|
136
|
-
|
|
168
|
+
const startedAt = Date.now();
|
|
169
|
+
const query = typeof input?.query === "string" ? input.query : "";
|
|
170
|
+
let error: unknown;
|
|
171
|
+
try {
|
|
172
|
+
if (query.trim() === "") {
|
|
173
|
+
return { items: [], total: 0, hasMore: false, nextOffset: null };
|
|
174
|
+
}
|
|
175
|
+
const server = typeof input.server === "string" ? input.server : undefined;
|
|
176
|
+
const limit = typeof input.limit === "number" ? input.limit : 12;
|
|
177
|
+
const offset = typeof input.offset === "number" ? input.offset : 0;
|
|
178
|
+
const page = paginate(rankToolMatches(state, query, server), offset, limit);
|
|
179
|
+
return {
|
|
180
|
+
...page,
|
|
181
|
+
items: page.items.map(({ server: matchServer, tool, score }) => ({
|
|
182
|
+
path: tool.name,
|
|
183
|
+
name: tool.originalName,
|
|
184
|
+
server: matchServer,
|
|
185
|
+
...(tool.description ? { description: tool.description } : {}),
|
|
186
|
+
score,
|
|
187
|
+
})),
|
|
188
|
+
};
|
|
189
|
+
} catch (caught) {
|
|
190
|
+
error = caught;
|
|
191
|
+
throw caught;
|
|
192
|
+
} finally {
|
|
193
|
+
calls.push(error === undefined
|
|
194
|
+
? { operation: "search", query, ok: true, durationMs: Date.now() - startedAt, startedAt }
|
|
195
|
+
: { operation: "search", query, ok: false, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - startedAt, startedAt });
|
|
137
196
|
}
|
|
138
|
-
const server = typeof input.server === "string" ? input.server : undefined;
|
|
139
|
-
const limit = typeof input.limit === "number" ? input.limit : 12;
|
|
140
|
-
const offset = typeof input.offset === "number" ? input.offset : 0;
|
|
141
|
-
const page = paginate(rankToolMatches(state, input.query, server), offset, limit);
|
|
142
|
-
return {
|
|
143
|
-
...page,
|
|
144
|
-
items: page.items.map(({ server: matchServer, tool, score }) => ({
|
|
145
|
-
path: tool.name,
|
|
146
|
-
name: tool.originalName,
|
|
147
|
-
server: matchServer,
|
|
148
|
-
...(tool.description ? { description: tool.description } : {}),
|
|
149
|
-
score,
|
|
150
|
-
})),
|
|
151
|
-
};
|
|
152
197
|
};
|
|
153
198
|
|
|
154
199
|
const describeTool = (input?: DescribeInput) => {
|
|
200
|
+
const startedAt = Date.now();
|
|
155
201
|
const path = typeof input?.path === "string" ? input.path : "";
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
202
|
+
let error: unknown;
|
|
203
|
+
try {
|
|
204
|
+
for (const [server, metadata] of state.toolMetadata) {
|
|
205
|
+
const tool = findToolByName(metadata, path);
|
|
206
|
+
if (!tool) continue;
|
|
207
|
+
const inputTypeScript = tool.inputSchema ? renderTsShape(tool.inputSchema) : null;
|
|
208
|
+
return {
|
|
209
|
+
path: tool.name,
|
|
210
|
+
name: tool.originalName,
|
|
211
|
+
server,
|
|
212
|
+
...(tool.description ? { description: tool.description } : {}),
|
|
213
|
+
...(inputTypeScript ? { inputTypeScript } : {}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const suggestions = path ? rankSuggestions(state, path, 5) : [];
|
|
217
|
+
error = "tool_not_found";
|
|
160
218
|
return {
|
|
161
|
-
path
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
219
|
+
path,
|
|
220
|
+
error: {
|
|
221
|
+
code: "tool_not_found",
|
|
222
|
+
message: `Tool not found: ${path}`,
|
|
223
|
+
suggestions,
|
|
224
|
+
},
|
|
166
225
|
};
|
|
226
|
+
} catch (caught) {
|
|
227
|
+
error = caught;
|
|
228
|
+
throw caught;
|
|
229
|
+
} finally {
|
|
230
|
+
calls.push(error === undefined
|
|
231
|
+
? { operation: "describe", path, ok: true, durationMs: Date.now() - startedAt, startedAt }
|
|
232
|
+
: { operation: "describe", path, ok: false, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - startedAt, startedAt });
|
|
167
233
|
}
|
|
168
|
-
const suggestions = path ? rankSuggestions(state, path, 5) : [];
|
|
169
|
-
return {
|
|
170
|
-
path,
|
|
171
|
-
error: {
|
|
172
|
-
code: "tool_not_found",
|
|
173
|
-
message: `Tool not found: ${path}`,
|
|
174
|
-
suggestions,
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
234
|
};
|
|
178
235
|
|
|
179
236
|
let worker: Worker | undefined;
|
|
@@ -234,7 +291,7 @@ export async function runMcpScript(
|
|
|
234
291
|
const timeoutError = new McpScriptTimeoutError(resolvedTimeoutMs);
|
|
235
292
|
const timeout = new Promise<never>((_resolve, reject) => {
|
|
236
293
|
timer = setTimeout(() => {
|
|
237
|
-
callsSnapshot =
|
|
294
|
+
callsSnapshot = snapshotCalls();
|
|
238
295
|
timeoutController.abort(timeoutError);
|
|
239
296
|
void activeWorker.terminate();
|
|
240
297
|
reject(timeoutError);
|
|
@@ -243,7 +300,7 @@ export async function runMcpScript(
|
|
|
243
300
|
const aborted = externalSignal
|
|
244
301
|
? new Promise<never>((_resolve, reject) => {
|
|
245
302
|
const onAbort = () => {
|
|
246
|
-
callsSnapshot =
|
|
303
|
+
callsSnapshot = snapshotCalls();
|
|
247
304
|
void activeWorker.terminate();
|
|
248
305
|
reject(abortReasonError(externalSignal.reason));
|
|
249
306
|
};
|
|
@@ -270,7 +327,7 @@ export async function runMcpScript(
|
|
|
270
327
|
removeAbortListener();
|
|
271
328
|
// "incomplete" means the call had not settled when the script finished
|
|
272
329
|
// (deadline, abort, or early return). Snapshot before aborting stragglers.
|
|
273
|
-
callsSnapshot ??=
|
|
330
|
+
callsSnapshot ??= snapshotCalls();
|
|
274
331
|
// A script may finish without awaiting every call; abort leftovers so
|
|
275
332
|
// parent-side dispatches do not outlive the script.
|
|
276
333
|
timeoutController.abort(new Error("mcp_script finished"));
|
|
@@ -288,7 +345,7 @@ export async function runMcpScript(
|
|
|
288
345
|
mode: "script",
|
|
289
346
|
...(errorCode ? { error: errorCode, message: errorMessage } : {}),
|
|
290
347
|
timeoutMs: resolvedTimeoutMs,
|
|
291
|
-
...(
|
|
348
|
+
...(callsSnapshot.length > 0 ? { calls: callsSnapshot } : {}),
|
|
292
349
|
...guardedMcpDetails(guarded),
|
|
293
350
|
},
|
|
294
351
|
};
|
package/mcp-script-worker.mjs
CHANGED
|
@@ -5,17 +5,30 @@ import vm from "node:vm";
|
|
|
5
5
|
const TOOLS_ENUMERATION_ERROR = "tools is not enumerable — use tools.search({ query })";
|
|
6
6
|
const RESERVED_TOOL_PROPS = new Set(["then", "catch", "finally", "toJSON", "toString", "valueOf"]);
|
|
7
7
|
|
|
8
|
+
// Keep this formatting logic in sync with mcp-code.ts; the standalone worker cannot import the TypeScript host module.
|
|
9
|
+
function needsInspectableFormatting(value, stack = new WeakSet()) {
|
|
10
|
+
if (value === undefined || typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") return true;
|
|
11
|
+
if (typeof value !== "object" || value === null) return false;
|
|
12
|
+
if (stack.has(value)) return true;
|
|
13
|
+
if (value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) return true;
|
|
14
|
+
stack.add(value);
|
|
15
|
+
try {
|
|
16
|
+
return Object.values(value).some((entry) => needsInspectableFormatting(entry, stack));
|
|
17
|
+
} finally {
|
|
18
|
+
stack.delete(value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
function formatValue(value) {
|
|
9
23
|
if (typeof value === "string") return value;
|
|
10
|
-
if (value === undefined) return "undefined";
|
|
11
24
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return String(value);
|
|
16
|
-
} catch {
|
|
17
|
-
return "[unserializable value]";
|
|
25
|
+
if (!needsInspectableFormatting(value)) {
|
|
26
|
+
const json = JSON.stringify(value, null, 2);
|
|
27
|
+
if (json !== undefined) return json;
|
|
18
28
|
}
|
|
29
|
+
return formatWithOptions({ colors: false, depth: 6 }, value);
|
|
30
|
+
} catch {
|
|
31
|
+
return "[unserializable value]";
|
|
19
32
|
}
|
|
20
33
|
}
|
|
21
34
|
|
package/package.json
CHANGED
|
@@ -5,13 +5,9 @@ description: Write mcp_script JavaScript for discovering, inspecting, and callin
|
|
|
5
5
|
|
|
6
6
|
# MCP scripting
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
For multi-call MCP work, write ordinary JavaScript with loops, filtering, chaining, fan-out, or other logic between calls. Run that source with `mcp_script`; it is the primary MCP orchestration surface. For a single MCP search, describe, status check, auth action, or tool call, use `mcp` instead.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
1. Find candidate tools with `await tools.search({ query, server?, limit?, offset? })`.
|
|
13
|
-
2. Inspect the exact returned path with `await tools.describe({ path })`.
|
|
14
|
-
3. Call it with `tools.call(path, args)`.
|
|
10
|
+
Write the source naturally, then pass it as `mcp_script`'s `code` argument:
|
|
15
11
|
|
|
16
12
|
```js
|
|
17
13
|
const { items } = await tools.search({ query: "search issues", server: "github" });
|
|
@@ -27,10 +23,16 @@ emit({ tool: details.path, completed: true });
|
|
|
27
23
|
return result.data;
|
|
28
24
|
```
|
|
29
25
|
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
1. Find candidate tools with `await tools.search({ query, server?, limit?, offset? })`.
|
|
29
|
+
2. Inspect the exact returned path with `await tools.describe({ path })`.
|
|
30
|
+
3. Call it with `tools.call(path, args)`.
|
|
31
|
+
|
|
30
32
|
Calls resolve to `{ ok: true, data }` or `{ ok: false, error }`; handle failed calls instead of expecting them to stop the script. `emit(value)` adds user-visible output before the final `return` value. `console` output is captured too.
|
|
31
33
|
|
|
32
34
|
`tools` is a non-enumerable proxy: `Object.keys(tools)` throws. Always use `tools.search` for discovery. When a known flat path is a valid identifier, direct calls such as `tools.github_search_issues(args)` are supported; use bracket syntax for hyphenated names: `tools["server_tool-name"](args)`. `search`, `call`, `describe`, and promise/serialization names (`then`, `catch`, `finally`, `toJSON`, `toString`, `valueOf`) are reserved on the proxy; if a flat path collides with one, call it via `tools.call("exact-path", args)`.
|
|
33
35
|
|
|
34
|
-
`tools.search` and `tools.describe` are asynchronous and must be awaited. The default script timeout is 30 seconds; the worker is terminated at the deadline, including for infinite loops. Every invocation still uses normal lazy connection, authentication, output guarding, and approval gates. Result details contain a concise `calls` trace with each path and
|
|
36
|
+
`tools.search` and `tools.describe` are asynchronous and must be awaited. The default script timeout is 30 seconds; the worker is terminated at the deadline, including for infinite loops. Every invocation still uses normal lazy connection, authentication, output guarding, and approval gates. Result details contain a concise `calls` trace with every search, describe, and call operation; each entry includes its query or path, outcome, and duration.
|
|
35
37
|
|
|
36
38
|
Use plain JavaScript loops and Promise utilities for composition. Fluent helpers such as `tools.find(...).one()`, `tools.parallel(...)`, and `tools.retry(...)` are not provided.
|
package/types.ts
CHANGED
|
@@ -433,7 +433,7 @@ export interface McpSettings {
|
|
|
433
433
|
idleTimeout?: number; // minutes, default 10, 0 to disable
|
|
434
434
|
requestTimeoutMs?: number; // milliseconds, overrides the SDK request timeout when > 0
|
|
435
435
|
directTools?: boolean;
|
|
436
|
-
/** Register the trusted MCP-only JavaScript scripting tool. Defaults to false. */
|
|
436
|
+
/** Register the trusted MCP-only JavaScript scripting tool. Defaults to true; set false to hide it. */
|
|
437
437
|
scriptMode?: boolean;
|
|
438
438
|
/** Default approval gate for matching tools/resources; per-server settings override it. */
|
|
439
439
|
approveTools?: boolean | string[];
|