pi-mcp-client 0.2.0 → 0.3.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/README.md +87 -23
- package/dist/index.js +367 -125
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,22 +29,66 @@ Start a new Pi session and ask it to search Cloudflare's documentation. Use `/mc
|
|
|
29
29
|
to inspect the connection. For authenticated services, see [OAuth](#oauth) or
|
|
30
30
|
[secret commands](#secret-commands).
|
|
31
31
|
|
|
32
|
-
Pi
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
Pi discovers candidates, explicitly activates the tools it needs, then calls
|
|
33
|
+
those tools natively. One `mcp_tools` tool supports both steps:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
// Discover candidates. Never activates, even for an exact-name query.
|
|
37
|
+
mcp_tools({ query: "list teams", server: "linear", limit: 5 })
|
|
38
|
+
|
|
39
|
+
// Activate exact identifiers. Never invokes.
|
|
40
|
+
mcp_tools({ activate: ["linear.list_teams", "linear.get_team"] })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Pass exactly one of `query` or `activate`. The optional `server` and `limit`
|
|
44
|
+
fields are valid only with `query`. Discovery returns up to five candidates by
|
|
45
|
+
default, or up to 50 with `limit`. Each candidate shows its exact activation
|
|
46
|
+
identifier, a short description, required parameter names only, and `[loaded]`
|
|
47
|
+
if already active. Results use local BM25-based ranking, with tool names weighted
|
|
48
|
+
more strongly than descriptions and support for prefix matching.
|
|
49
|
+
|
|
50
|
+
Activation accepts 1–50 exact `server.tool` or `mcp__server__tool` identifiers,
|
|
51
|
+
ignores duplicates, and works without a prior search. Typos never activate fuzzy
|
|
52
|
+
matches: failures list nearby catalog names when available so the assistant can
|
|
53
|
+
retry with an exact identifier. Each identifier reports `loaded`, `already loaded`,
|
|
54
|
+
or `not loaded` with a reason. Partial success keeps the tools that loaded.
|
|
55
|
+
|
|
56
|
+
Full schemas become available on the model turn after activation. First use of a
|
|
57
|
+
capability now takes three turns—discover, activate, call—so a fuzzy search match
|
|
58
|
+
can never become an active tool. Previously loaded tools remain available.
|
|
59
|
+
|
|
60
|
+
`mcp_tools` replaces `mcp_search` without backward compatibility. Update explicit
|
|
61
|
+
Pi tool allowlists to use `mcp_tools` and activate the tools you need again in
|
|
62
|
+
existing sessions. The UI labels discovery calls **mcp discover** and activation
|
|
63
|
+
calls **mcp activate**.
|
|
64
|
+
|
|
65
|
+
### Result display
|
|
66
|
+
|
|
67
|
+
Discovery rows show `○` for inactive candidates and `●` for already active tools,
|
|
68
|
+
without a status suffix. These reflect the state when discovery runs; earlier
|
|
69
|
+
results don't update retroactively. Activation results use `✔︎` for success and
|
|
70
|
+
`✘︎` for failure. Descriptions stay gray; identifiers remain prominent.
|
|
71
|
+
|
|
72
|
+
Expand a tool result to see JSON objects and arrays formatted with two-space
|
|
73
|
+
indentation and syntax highlighting. Explicit JSON resource MIME types (including
|
|
74
|
+
`application/*+json`) and structured content identify JSON without guessing.
|
|
75
|
+
Other explicit MIME types stay plain text; unlabeled text is checked for JSON.
|
|
76
|
+
|
|
77
|
+
Formatting changes only the display, not the response sent to the assistant.
|
|
78
|
+
Invalid or truncated JSON stays plain text. Results that would exceed formatting
|
|
79
|
+
limits also stay plain text. Resource-link MIME types describe the linked content,
|
|
80
|
+
not the displayed link label.
|
|
38
81
|
|
|
39
82
|
### Session behavior
|
|
40
83
|
|
|
41
84
|
- Tools accumulate rather than rotating with each prompt.
|
|
42
|
-
- Resume and branch navigation restore tools
|
|
85
|
+
- Resume and branch navigation restore tools activated through `mcp_tools` on
|
|
86
|
+
the selected branch. Discovery results never restore tools.
|
|
43
87
|
- Compaction retains the acquired tool set. New sessions start fresh.
|
|
44
88
|
- Pi uses native deferred loading where supported by the model and provider.
|
|
45
89
|
Other providers receive the expanded tool list normally.
|
|
46
|
-
-
|
|
47
|
-
allowlist must include both `
|
|
90
|
+
- Discovery respects server filters; activation also respects Pi's tool exclusions. An explicit tool
|
|
91
|
+
allowlist must include both `mcp_tools` and the native tools you want to load.
|
|
48
92
|
|
|
49
93
|
### Commands
|
|
50
94
|
|
|
@@ -54,6 +98,8 @@ remain available as the conversation continues.
|
|
|
54
98
|
| `/mcp inspect <server>` | Inspect status and configuration, including disabled servers. Connection values are hidden. |
|
|
55
99
|
| `/mcp tools <server>` | Browse the server's tools and inspect descriptions without activating tools. |
|
|
56
100
|
| `/mcp reload` | Apply configuration changes without restarting Pi. |
|
|
101
|
+
| `/mcp enable <server>` | Enable a server in its effective configuration file. |
|
|
102
|
+
| `/mcp disable <server>` | Disable a server, close its connection, and deactivate its tools. |
|
|
57
103
|
| `/mcp auth <server>` | Authenticate an OAuth-enabled HTTP server. |
|
|
58
104
|
| `/mcp reconnect <server>` | Replace a connection and refresh its catalog. |
|
|
59
105
|
| `/mcp refresh <server>` | Refresh a server's catalog without loading additional tools. |
|
|
@@ -64,8 +110,8 @@ connections open on demand. A dash (`—`) means the catalog hasn't been fetched
|
|
|
64
110
|
not that the server has no tools. The **Loaded** column counts tools currently
|
|
65
111
|
active for the assistant.
|
|
66
112
|
|
|
67
|
-
After refreshing a changed schema,
|
|
68
|
-
definition. Calls validate the live catalog before execution and refuse removed
|
|
113
|
+
After refreshing a changed schema, activate the tool again with its exact
|
|
114
|
+
identifier to load its current definition. Calls validate the live catalog before execution and refuse removed
|
|
69
115
|
or changed tools. The extension does not retry failed tool invocations; after an
|
|
70
116
|
interrupted call, check whether the operation completed before trying again.
|
|
71
117
|
|
|
@@ -145,12 +191,12 @@ Commands use `/bin/sh` on Unix or Pi's shell selection on Windows, inherit Pi's
|
|
|
145
191
|
process environment, and run in the server's configured `cwd` (the project
|
|
146
192
|
directory by default). They run once per connection, including reconnections,
|
|
147
193
|
not during configuration loading, status display, or cached discovery. Cold
|
|
148
|
-
searches can connect and therefore execute commands. Concurrent connection
|
|
194
|
+
searches and activations can connect and therefore execute commands. Concurrent connection
|
|
149
195
|
requests share the same resolution.
|
|
150
196
|
|
|
151
197
|
The client trims stdout and rejects empty output, nonzero exits, output above
|
|
152
198
|
64 KiB, and resolution taking more than 10 seconds (or a shorter `timeoutMs`).
|
|
153
|
-
Session shutdown cancels pending commands. Cancelling an individual search stops
|
|
199
|
+
Session shutdown cancels pending commands. Cancelling an individual search or activation stops
|
|
154
200
|
waiting but leaves shared connection work running for other callers. The client
|
|
155
201
|
discards command stderr and does not include resolved secrets in errors, session
|
|
156
202
|
records, or catalog caches. Commands themselves remain responsible for avoiding
|
|
@@ -200,6 +246,21 @@ invalid configuration leaves the previous setup intact. It closes existing
|
|
|
200
246
|
connections, which reopen on demand, and deactivates tools from changed, removed,
|
|
201
247
|
or disabled server definitions. Unchanged active tools remain available.
|
|
202
248
|
|
|
249
|
+
To toggle a server without editing JSON, use `/mcp disable <server>` or
|
|
250
|
+
`/mcp enable <server>`. The change persists in the trusted project's `.mcp.json`
|
|
251
|
+
if that file defines the server; otherwise, it persists in the global
|
|
252
|
+
`~/.pi/agent/mcp.json`. Untrusted project files are neither read nor changed.
|
|
253
|
+
The command reports which scope changed. It updates only the `disabled` option,
|
|
254
|
+
preserves other values (including secret references), and reformats the file as
|
|
255
|
+
indented JSON. Repeating a toggle that's already set leaves the file unchanged.
|
|
256
|
+
|
|
257
|
+
Both commands wait for active agent work to finish, then apply configuration as
|
|
258
|
+
`/mcp reload` does: connections close and reopen on demand, while unchanged active
|
|
259
|
+
tools from other servers remain available. Disabling removes the server from
|
|
260
|
+
search and deactivates its tools. Enabling does not connect, authenticate, or load
|
|
261
|
+
tools; ask the assistant to discover the capabilities you need. Other running Pi
|
|
262
|
+
sessions pick up the saved change when they reload their MCP configuration.
|
|
263
|
+
|
|
203
264
|
Use `/mcp inspect <server>` to check the effective transport, protocol, filters,
|
|
204
265
|
and connection status without connecting or running secret commands. Connection
|
|
205
266
|
values—including commands, arguments, URLs, headers, and environment variables—
|
|
@@ -216,19 +277,21 @@ context. This command requires an interactive UI.
|
|
|
216
277
|
|
|
217
278
|
Connections start on demand, never while the extension factory loads. A search
|
|
218
279
|
without a cached catalog contacts configured servers, with at most four discoveries
|
|
219
|
-
in flight. A server-scoped search only contacts that server.
|
|
220
|
-
|
|
280
|
+
in flight. A server-scoped search only contacts that server. Activation discovers
|
|
281
|
+
only the servers named by its identifiers, with the same concurrency bound.
|
|
282
|
+
Failed servers are reported as unavailable, not mistaken for an empty catalog.
|
|
221
283
|
|
|
222
284
|
Catalogs are cached privately under `~/.pi/agent/cache/pi-mcp-client/`, keyed by
|
|
223
285
|
server configuration and working directory. Disk caches expire after 24 hours.
|
|
224
|
-
They contain tool metadata, not configured credentials. Cached
|
|
225
|
-
connection; invocation refreshes the live catalog before
|
|
286
|
+
They contain tool metadata, not configured credentials. Cached discovery and
|
|
287
|
+
activation need no connection; invocation refreshes the live catalog before
|
|
288
|
+
calling the tool.
|
|
226
289
|
Connections remain open until shutdown or explicit reconnection.
|
|
227
290
|
|
|
228
291
|
When a connected server reports a tool-list change, the extension invalidates its
|
|
229
|
-
memory and disk catalogs. The next
|
|
230
|
-
or removed tools. Notifications don't replace active tool
|
|
231
|
-
schemas require another `
|
|
292
|
+
memory and disk catalogs. The next discovery or activation fetches the current
|
|
293
|
+
list, including new or removed tools. Notifications don't replace active tool
|
|
294
|
+
definitions: changed schemas require another `mcp_tools({activate: [...]})` before use. Disconnected, cache-only searches
|
|
232
295
|
can't receive notifications and still use the 24-hour disk-cache expiry.
|
|
233
296
|
|
|
234
297
|
### OAuth
|
|
@@ -255,8 +318,9 @@ Only load configuration you trust. Server executables and secret commands run
|
|
|
255
318
|
with your user permissions; trusted project configuration can replace global
|
|
256
319
|
connections and settings.
|
|
257
320
|
|
|
258
|
-
Server metadata is untrusted.
|
|
259
|
-
|
|
321
|
+
Server metadata is untrusted. Discovery never activates tools. Explicit
|
|
322
|
+
activation exposes schemas but does not approve tool side effects or provide
|
|
323
|
+
per-call confirmation. Use tool filters and Pi permission
|
|
260
324
|
extensions for additional controls. Cancelling a call does not guarantee that the
|
|
261
325
|
server rolled back its effects.
|
|
262
326
|
|
|
@@ -297,7 +361,7 @@ unavailable server is not an empty catalog.
|
|
|
297
361
|
| `connection_failed` | Server executable, working directory, endpoint, network, and TLS configuration. |
|
|
298
362
|
| `timeout` | Server responsiveness and the applicable request, secret-command, or OAuth time limit. |
|
|
299
363
|
| `protocol_error` | Server compatibility and the `protocol` setting. |
|
|
300
|
-
| `tool_changed` | Server filters and the current tool schema;
|
|
364
|
+
| `tool_changed` | Server filters and the current tool schema; activate the exact identifier again. Reload Pi if connection configuration changed. |
|
|
301
365
|
| `tool_error` | The server's tool result and inputs; verify the outcome before retrying. |
|
|
302
366
|
| `oauth_failed` | Browser access to the callback and support for dynamically registered public clients. |
|
|
303
367
|
| `callback_unavailable` | Another process using local port 19847. |
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ function diagnostic(code, context) {
|
|
|
30
30
|
cancelled: "Start the operation again when ready.",
|
|
31
31
|
connection_failed: `Check the URL or executable, working directory, network, and TLS setup; then run /mcp reconnect ${target}.`,
|
|
32
32
|
protocol_error: "Check the server's MCP compatibility and protocol setting. Only stdio and Streamable HTTP are supported.",
|
|
33
|
-
tool_changed: "Check server filters and
|
|
33
|
+
tool_changed: "Check server filters and use mcp_tools with activate and the exact identifier to activate the current tool definition. Reload Pi if the connection configuration changed.",
|
|
34
34
|
tool_error: "Review the server's tool result and inputs. Verify the outcome before retrying.",
|
|
35
35
|
oauth_failed: `Check OAuth support and browser access to the local callback, then run /mcp auth ${target}.`,
|
|
36
36
|
callback_unavailable: "Free local port 19847, then retry authentication.",
|
|
@@ -285,12 +285,14 @@ __export(config_exports, {
|
|
|
285
285
|
matches: () => matches,
|
|
286
286
|
object: () => object,
|
|
287
287
|
parseConfig: () => parseConfig,
|
|
288
|
-
resolveServer: () => resolveServer
|
|
288
|
+
resolveServer: () => resolveServer,
|
|
289
|
+
setServerDisabled: () => setServerDisabled
|
|
289
290
|
});
|
|
290
|
-
import { readFile } from "node:fs/promises";
|
|
291
|
+
import { chmod, readFile, realpath, stat, writeFile, rename, rm } from "node:fs/promises";
|
|
292
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
291
293
|
import { resolve, join } from "node:path";
|
|
292
294
|
import { homedir } from "node:os";
|
|
293
|
-
import { createHash } from "node:crypto";
|
|
295
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
294
296
|
function object(value) {
|
|
295
297
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
296
298
|
}
|
|
@@ -386,6 +388,46 @@ async function loadConfig(agentDir, cwd, trusted) {
|
|
|
386
388
|
}
|
|
387
389
|
return config;
|
|
388
390
|
}
|
|
391
|
+
async function setServerDisabled(agentDir, cwd, trusted, server, disabled, validate) {
|
|
392
|
+
const paths = [join(agentDir, "mcp.json"), ...trusted ? [join(cwd, ".mcp.json")] : []];
|
|
393
|
+
const locks = [...new Set(await Promise.all(
|
|
394
|
+
paths.map((path) => realpath(path).catch(() => resolve(path)))
|
|
395
|
+
))].sort();
|
|
396
|
+
const locked = async (index) => {
|
|
397
|
+
if (index < locks.length)
|
|
398
|
+
return withFileMutationQueue(locks[index], () => locked(index + 1));
|
|
399
|
+
const documents = await Promise.all(paths.map(readJson));
|
|
400
|
+
const config = /* @__PURE__ */ Object.create(null);
|
|
401
|
+
let source = -1;
|
|
402
|
+
for (const [index2, document2] of documents.entries()) {
|
|
403
|
+
if (document2 === void 0) continue;
|
|
404
|
+
const parsed = parseConfig(document2, paths[index2]);
|
|
405
|
+
Object.assign(config, parsed);
|
|
406
|
+
if (Object.hasOwn(parsed, server)) source = index2;
|
|
407
|
+
}
|
|
408
|
+
if (source < 0) throw new Error("Server is no longer configured. Run /mcp reload.");
|
|
409
|
+
const changed = Boolean(config[server].disabled) !== disabled;
|
|
410
|
+
if (changed) config[server].disabled = disabled;
|
|
411
|
+
validate(config);
|
|
412
|
+
const document = documents[source];
|
|
413
|
+
if (changed) {
|
|
414
|
+
document.mcpServers[server].disabled = disabled;
|
|
415
|
+
const target = await realpath(paths[source]);
|
|
416
|
+
const mode = (await stat(target)).mode & 511;
|
|
417
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
418
|
+
try {
|
|
419
|
+
await writeFile(temporary, JSON.stringify(document, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
420
|
+
await chmod(temporary, mode);
|
|
421
|
+
validate(config);
|
|
422
|
+
await rename(temporary, target);
|
|
423
|
+
} finally {
|
|
424
|
+
await rm(temporary, { force: true });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return { config, scope: source === 0 ? "global" : "project" };
|
|
428
|
+
};
|
|
429
|
+
return locked(0);
|
|
430
|
+
}
|
|
389
431
|
function interpolate(value, env = process.env) {
|
|
390
432
|
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => {
|
|
391
433
|
const found = env[name];
|
|
@@ -547,9 +589,9 @@ var recurse = (node, query, maxDistance, results, matrix, m, n, prefix) => {
|
|
|
547
589
|
const offset = m * n;
|
|
548
590
|
key: for (const key of node.keys()) {
|
|
549
591
|
if (key === LEAF) {
|
|
550
|
-
const
|
|
551
|
-
if (
|
|
552
|
-
results.set(prefix, [node.get(key),
|
|
592
|
+
const distance2 = matrix[offset - 1];
|
|
593
|
+
if (distance2 <= maxDistance) {
|
|
594
|
+
results.set(prefix, [node.get(key), distance2]);
|
|
553
595
|
}
|
|
554
596
|
} else {
|
|
555
597
|
let i = m;
|
|
@@ -1903,22 +1945,22 @@ var MiniSearch = class _MiniSearch {
|
|
|
1903
1945
|
}
|
|
1904
1946
|
if (prefixMatches) {
|
|
1905
1947
|
for (const [term, data2] of prefixMatches) {
|
|
1906
|
-
const
|
|
1907
|
-
if (!
|
|
1948
|
+
const distance2 = term.length - query.term.length;
|
|
1949
|
+
if (!distance2) {
|
|
1908
1950
|
continue;
|
|
1909
1951
|
}
|
|
1910
1952
|
fuzzyMatches === null || fuzzyMatches === void 0 ? void 0 : fuzzyMatches.delete(term);
|
|
1911
|
-
const weight = prefixWeight * term.length / (term.length + 0.3 *
|
|
1953
|
+
const weight = prefixWeight * term.length / (term.length + 0.3 * distance2);
|
|
1912
1954
|
this.termResults(query.term, term, weight, query.termBoost, data2, boosts, boostDocument, bm25params, results);
|
|
1913
1955
|
}
|
|
1914
1956
|
}
|
|
1915
1957
|
if (fuzzyMatches) {
|
|
1916
1958
|
for (const term of fuzzyMatches.keys()) {
|
|
1917
|
-
const [data2,
|
|
1918
|
-
if (!
|
|
1959
|
+
const [data2, distance2] = fuzzyMatches.get(term);
|
|
1960
|
+
if (!distance2) {
|
|
1919
1961
|
continue;
|
|
1920
1962
|
}
|
|
1921
|
-
const weight = fuzzyWeight * term.length / (term.length +
|
|
1963
|
+
const weight = fuzzyWeight * term.length / (term.length + distance2);
|
|
1922
1964
|
this.termResults(query.term, term, weight, query.termBoost, data2, boosts, boostDocument, bm25params, results);
|
|
1923
1965
|
}
|
|
1924
1966
|
}
|
|
@@ -2310,6 +2352,41 @@ function prepareTool(server, identity, tool) {
|
|
|
2310
2352
|
schemaHash: fingerprint(inputSchema)
|
|
2311
2353
|
};
|
|
2312
2354
|
}
|
|
2355
|
+
function summarize(tool, includeIdentifier = true) {
|
|
2356
|
+
const schema = tool.inputSchema;
|
|
2357
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((name) => typeof name === "string") : [];
|
|
2358
|
+
const prefix = includeIdentifier ? `${tool.server}.${tool.name} \u2014 ` : "";
|
|
2359
|
+
return `${prefix}${line(tool.description).slice(0, 180)} (required: ${required.map(line).join(", ") || "none"})`;
|
|
2360
|
+
}
|
|
2361
|
+
function distance(a, b) {
|
|
2362
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
2363
|
+
for (let i = 0; i < a.length; i++) {
|
|
2364
|
+
let diagonal = row[0];
|
|
2365
|
+
row[0] = i + 1;
|
|
2366
|
+
for (let j = 0; j < b.length; j++) {
|
|
2367
|
+
const previous = row[j + 1];
|
|
2368
|
+
row[j + 1] = Math.min(row[j] + 1, previous + 1, diagonal + Number(a[i] !== b[j]));
|
|
2369
|
+
diagonal = previous;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
return row[b.length];
|
|
2373
|
+
}
|
|
2374
|
+
function resolveTools(tools, identifiers) {
|
|
2375
|
+
return [...new Set(identifiers)].map((identifier) => {
|
|
2376
|
+
const matches2 = tools.filter(
|
|
2377
|
+
(tool2) => identifier === tool2.nativeName || identifier === `${tool2.server}.${tool2.name}`
|
|
2378
|
+
);
|
|
2379
|
+
const tool = matches2.length === 1 ? matches2[0] : void 0;
|
|
2380
|
+
const suggestions = tool ? [] : tools.map((candidate) => ({
|
|
2381
|
+
name: `${candidate.server}.${candidate.name}`,
|
|
2382
|
+
score: Math.min(
|
|
2383
|
+
distance(identifier, `${candidate.server}.${candidate.name}`),
|
|
2384
|
+
distance(identifier, candidate.nativeName)
|
|
2385
|
+
)
|
|
2386
|
+
})).sort((a, b) => a.score - b.score || a.name.localeCompare(b.name)).slice(0, 3).map(({ name }) => name);
|
|
2387
|
+
return { identifier, tool, suggestions };
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2313
2390
|
var STOP = /* @__PURE__ */ new Set([
|
|
2314
2391
|
"a",
|
|
2315
2392
|
"an",
|
|
@@ -2467,7 +2544,7 @@ function inspectServer(name, config, status) {
|
|
|
2467
2544
|
// src/auth.ts
|
|
2468
2545
|
init_config();
|
|
2469
2546
|
init_diagnostics();
|
|
2470
|
-
import { randomUUID } from "node:crypto";
|
|
2547
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2471
2548
|
import { createServer } from "node:http";
|
|
2472
2549
|
import {
|
|
2473
2550
|
auth
|
|
@@ -2517,7 +2594,7 @@ var OAuthProvider = class {
|
|
|
2517
2594
|
data;
|
|
2518
2595
|
verifier;
|
|
2519
2596
|
discovery;
|
|
2520
|
-
expectedState =
|
|
2597
|
+
expectedState = randomUUID2();
|
|
2521
2598
|
save() {
|
|
2522
2599
|
this.store.write(JSON.stringify(this.data));
|
|
2523
2600
|
}
|
|
@@ -2641,9 +2718,9 @@ async function authenticate(url, open, signal, store) {
|
|
|
2641
2718
|
|
|
2642
2719
|
// src/runtime.ts
|
|
2643
2720
|
init_config();
|
|
2644
|
-
import { mkdir, readFile as readFile2, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2721
|
+
import { mkdir, readFile as readFile2, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2645
2722
|
import { join as join2 } from "node:path";
|
|
2646
|
-
import { randomUUID as
|
|
2723
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2647
2724
|
import {
|
|
2648
2725
|
Client,
|
|
2649
2726
|
StreamableHTTPClientTransport
|
|
@@ -2652,7 +2729,7 @@ import {
|
|
|
2652
2729
|
StdioClientTransport,
|
|
2653
2730
|
getDefaultEnvironment
|
|
2654
2731
|
} from "@modelcontextprotocol/client/stdio";
|
|
2655
|
-
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
2732
|
+
import { withFileMutationQueue as withFileMutationQueue2 } from "@earendil-works/pi-coding-agent";
|
|
2656
2733
|
init_secrets();
|
|
2657
2734
|
init_diagnostics();
|
|
2658
2735
|
var ToolContractError = class extends Error {
|
|
@@ -2825,9 +2902,9 @@ var McpRuntime = class {
|
|
|
2825
2902
|
state.tools = void 0;
|
|
2826
2903
|
state.warnings = void 0;
|
|
2827
2904
|
const path = join2(this.cacheDir, `${identity}.json`);
|
|
2828
|
-
state.invalidating =
|
|
2905
|
+
state.invalidating = withFileMutationQueue2(
|
|
2829
2906
|
path,
|
|
2830
|
-
() =>
|
|
2907
|
+
() => rm2(path, { force: true })
|
|
2831
2908
|
).catch(() => {
|
|
2832
2909
|
state.warnings = [
|
|
2833
2910
|
`${name}: stale catalog cache could not be removed.`
|
|
@@ -2934,7 +3011,7 @@ var McpRuntime = class {
|
|
|
2934
3011
|
return tools;
|
|
2935
3012
|
}
|
|
2936
3013
|
throw new ToolContractError(
|
|
2937
|
-
"MCP tool catalog kept changing.
|
|
3014
|
+
"MCP tool catalog kept changing. Retry discovery or activation."
|
|
2938
3015
|
);
|
|
2939
3016
|
})().catch((error) => {
|
|
2940
3017
|
state.error = this.failure(name, error);
|
|
@@ -2947,8 +3024,8 @@ var McpRuntime = class {
|
|
|
2947
3024
|
return waitFor(state.listing, signal);
|
|
2948
3025
|
}
|
|
2949
3026
|
async discover(server, signal) {
|
|
2950
|
-
if (server) this.definition(server);
|
|
2951
|
-
const names = server ? [server] : Object.keys(this.config).filter((name) => !this.config[name].disabled).sort();
|
|
3027
|
+
if (typeof server === "string") this.definition(server);
|
|
3028
|
+
const names = Array.isArray(server) ? [...new Set(server)] : server ? [server] : Object.keys(this.config).filter((name) => !this.config[name].disabled).sort();
|
|
2952
3029
|
const result = {
|
|
2953
3030
|
tools: [],
|
|
2954
3031
|
unavailable: [],
|
|
@@ -2985,7 +3062,7 @@ var McpRuntime = class {
|
|
|
2985
3062
|
const found = current.find((candidate) => candidate.name === tool.name);
|
|
2986
3063
|
if (!found || found.schemaHash !== tool.schemaHash)
|
|
2987
3064
|
throw new ToolContractError(
|
|
2988
|
-
"MCP tool was removed or its schema changed.
|
|
3065
|
+
"MCP tool was removed or its schema changed. Use mcp_tools with activate and its exact identifier to activate its current definition."
|
|
2989
3066
|
);
|
|
2990
3067
|
const client = await waitFor(this.client(tool.server), signal);
|
|
2991
3068
|
try {
|
|
@@ -3074,7 +3151,7 @@ var McpRuntime = class {
|
|
|
3074
3151
|
async readCache(name, identity) {
|
|
3075
3152
|
try {
|
|
3076
3153
|
const path = join2(this.cacheDir, `${identity}.json`);
|
|
3077
|
-
const info = await
|
|
3154
|
+
const info = await stat2(path);
|
|
3078
3155
|
if (info.size > 4 * 1024 * 1024 || Date.now() - info.mtimeMs > 864e5)
|
|
3079
3156
|
return;
|
|
3080
3157
|
const data = JSON.parse(await readFile2(path, "utf8"));
|
|
@@ -3095,28 +3172,28 @@ var McpRuntime = class {
|
|
|
3095
3172
|
if (Buffer.byteLength(text) > 4 * 1024 * 1024) return;
|
|
3096
3173
|
await mkdir(this.cacheDir, { recursive: true, mode: 448 });
|
|
3097
3174
|
const path = join2(this.cacheDir, `${identity}.json`);
|
|
3098
|
-
await
|
|
3175
|
+
await withFileMutationQueue2(path, async () => {
|
|
3099
3176
|
if (!isCurrent()) return;
|
|
3100
|
-
const temp = `${path}.${
|
|
3101
|
-
await
|
|
3102
|
-
if (isCurrent()) await
|
|
3103
|
-
else await
|
|
3177
|
+
const temp = `${path}.${randomUUID3()}.tmp`;
|
|
3178
|
+
await writeFile2(temp, text, { mode: 384 });
|
|
3179
|
+
if (isCurrent()) await rename2(temp, path);
|
|
3180
|
+
else await rm2(temp, { force: true });
|
|
3104
3181
|
});
|
|
3105
3182
|
}
|
|
3106
3183
|
};
|
|
3107
3184
|
|
|
3108
3185
|
// src/exposure.ts
|
|
3109
3186
|
init_config();
|
|
3110
|
-
var
|
|
3187
|
+
var TOOLS_TOOL = "mcp_tools";
|
|
3111
3188
|
function restoredTools(entries) {
|
|
3112
3189
|
const tools = /* @__PURE__ */ new Map();
|
|
3113
3190
|
for (const entry of entries) {
|
|
3114
|
-
if (entry.type !== "message" || entry.message.role !== "toolResult" || entry.message.toolName !==
|
|
3191
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult" || entry.message.toolName !== TOOLS_TOOL || entry.message.isError)
|
|
3115
3192
|
continue;
|
|
3116
3193
|
const details = entry.message.details;
|
|
3117
3194
|
if (!object(details) || details.mcpClient !== 1 || !Array.isArray(details.loaded))
|
|
3118
3195
|
continue;
|
|
3119
|
-
for (const raw of details.loaded.slice(0,
|
|
3196
|
+
for (const raw of details.loaded.slice(0, MAX_SEARCH_LIMIT)) {
|
|
3120
3197
|
if (!object(raw) || typeof raw.server !== "string" || typeof raw.identity !== "string" || typeof raw.name !== "string" || typeof raw.description !== "string" || !object(raw.inputSchema))
|
|
3121
3198
|
continue;
|
|
3122
3199
|
try {
|
|
@@ -3179,40 +3256,59 @@ var Exposure = class {
|
|
|
3179
3256
|
|
|
3180
3257
|
// src/output.ts
|
|
3181
3258
|
init_diagnostics();
|
|
3182
|
-
import { mkdtemp, writeFile as
|
|
3259
|
+
import { mkdtemp, writeFile as writeFile3 } from "node:fs/promises";
|
|
3183
3260
|
import { tmpdir } from "node:os";
|
|
3184
3261
|
import { join as join3 } from "node:path";
|
|
3185
3262
|
import {
|
|
3186
3263
|
truncateHead,
|
|
3187
|
-
withFileMutationQueue as
|
|
3264
|
+
withFileMutationQueue as withFileMutationQueue3
|
|
3188
3265
|
} from "@earendil-works/pi-coding-agent";
|
|
3189
3266
|
function textResult(text, details) {
|
|
3190
3267
|
return { content: [{ type: "text", text }], details };
|
|
3191
3268
|
}
|
|
3192
3269
|
async function convertResult(result, label) {
|
|
3193
3270
|
const texts = [];
|
|
3271
|
+
const blocks = [];
|
|
3272
|
+
let offset = 0;
|
|
3273
|
+
const append = (text2, metadata = {}) => {
|
|
3274
|
+
const start = offset + (texts.length ? 2 : 0);
|
|
3275
|
+
offset = start + text2.length;
|
|
3276
|
+
texts.push(text2);
|
|
3277
|
+
blocks.push({ ...metadata, start, end: offset });
|
|
3278
|
+
};
|
|
3194
3279
|
const images = [];
|
|
3195
3280
|
let imageBytes = 0;
|
|
3196
3281
|
let needsSpill = false;
|
|
3197
3282
|
for (const part of result.content ?? []) {
|
|
3198
|
-
if (part.type === "text")
|
|
3283
|
+
if (part.type === "text") append(part.text);
|
|
3199
3284
|
else if (part.type === "image" && ["image/png", "image/jpeg", "image/gif", "image/webp"].includes(part.mimeType) && imageBytes + part.data.length <= 8 * 1024 * 1024) {
|
|
3200
3285
|
images.push({ type: "image", data: part.data, mimeType: part.mimeType });
|
|
3201
3286
|
imageBytes += part.data.length;
|
|
3202
3287
|
} else if (part.type === "resource" && "text" in part.resource)
|
|
3203
|
-
|
|
3204
|
-
else if (part.type === "resource_link")
|
|
3288
|
+
append(part.resource.text, { mimeType: part.resource.mimeType });
|
|
3289
|
+
else if (part.type === "resource_link")
|
|
3290
|
+
append(`${part.name}: ${part.uri}`, {
|
|
3291
|
+
mimeType: part.mimeType,
|
|
3292
|
+
resourceLink: true
|
|
3293
|
+
});
|
|
3205
3294
|
else {
|
|
3206
|
-
|
|
3295
|
+
append(`[${part.type} content saved in full result file]`, {
|
|
3296
|
+
mimeType: "text/plain"
|
|
3297
|
+
});
|
|
3207
3298
|
needsSpill = true;
|
|
3208
3299
|
}
|
|
3209
3300
|
}
|
|
3210
3301
|
if (result.structuredContent !== void 0)
|
|
3211
|
-
|
|
3302
|
+
append(JSON.stringify(result.structuredContent, null, 2), { structured: true });
|
|
3212
3303
|
const truncated = truncateHead(texts.join("\n\n"));
|
|
3213
3304
|
let text = truncated.content;
|
|
3214
3305
|
const details = {
|
|
3215
3306
|
mcpClient: 1,
|
|
3307
|
+
displayBlocks: blocks.filter((block) => block.start < text.length).map((block) => ({
|
|
3308
|
+
...block,
|
|
3309
|
+
end: Math.min(block.end, text.length),
|
|
3310
|
+
...block.end > text.length ? { truncated: true } : {}
|
|
3311
|
+
})),
|
|
3216
3312
|
failed: result.isError === true,
|
|
3217
3313
|
...result.isError ? {
|
|
3218
3314
|
diagnostics: [
|
|
@@ -3226,9 +3322,9 @@ async function convertResult(result, label) {
|
|
|
3226
3322
|
};
|
|
3227
3323
|
if (truncated.truncated || needsSpill) {
|
|
3228
3324
|
const path = join3(await mkdtemp(join3(tmpdir(), "pi-mcp-client-")), "result.json");
|
|
3229
|
-
await
|
|
3325
|
+
await withFileMutationQueue3(
|
|
3230
3326
|
path,
|
|
3231
|
-
() =>
|
|
3327
|
+
() => writeFile3(path, JSON.stringify(result), { mode: 384 })
|
|
3232
3328
|
);
|
|
3233
3329
|
details.fullOutputPath = path;
|
|
3234
3330
|
text += `
|
|
@@ -3245,7 +3341,102 @@ Full MCP result: ${path}`;
|
|
|
3245
3341
|
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
3246
3342
|
import { Text, truncateToWidth as truncateToWidth2 } from "@earendil-works/pi-tui";
|
|
3247
3343
|
init_config();
|
|
3344
|
+
|
|
3345
|
+
// src/format.ts
|
|
3346
|
+
import {
|
|
3347
|
+
DEFAULT_MAX_BYTES,
|
|
3348
|
+
DEFAULT_MAX_LINES
|
|
3349
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3350
|
+
function formatBlock(text, block, theme) {
|
|
3351
|
+
const fallback = plain(text);
|
|
3352
|
+
if (block.truncated || block.resourceLink || Buffer.byteLength(text) > DEFAULT_MAX_BYTES)
|
|
3353
|
+
return fallback;
|
|
3354
|
+
const mime = block.mimeType?.split(";", 1)[0]?.trim().toLowerCase();
|
|
3355
|
+
const explicitJson = mime === "application/json" || mime === "text/json" || /^application\/[^\s/;]+\+json$/.test(mime ?? "");
|
|
3356
|
+
if (mime && !explicitJson) return fallback;
|
|
3357
|
+
if (!explicitJson && !block.structured && !/^[\s]*[\[{]/.test(text))
|
|
3358
|
+
return fallback;
|
|
3359
|
+
try {
|
|
3360
|
+
JSON.parse(text);
|
|
3361
|
+
} catch {
|
|
3362
|
+
return fallback;
|
|
3363
|
+
}
|
|
3364
|
+
const tokens2 = text.match(
|
|
3365
|
+
/"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}\[\],:]/g
|
|
3366
|
+
) ?? [];
|
|
3367
|
+
const pieces = [];
|
|
3368
|
+
let depth = 0;
|
|
3369
|
+
let bytes = 0;
|
|
3370
|
+
let lines = 1;
|
|
3371
|
+
const add = (value, color) => {
|
|
3372
|
+
bytes += Buffer.byteLength(value);
|
|
3373
|
+
lines += value.split("\n").length - 1;
|
|
3374
|
+
pieces.push({ text: value, color });
|
|
3375
|
+
};
|
|
3376
|
+
const newline = () => add("\n" + " ".repeat(depth));
|
|
3377
|
+
for (let i = 0; i < tokens2.length; i++) {
|
|
3378
|
+
const token = tokens2[i];
|
|
3379
|
+
if (token === "{" || token === "[") {
|
|
3380
|
+
add(token, "syntaxPunctuation");
|
|
3381
|
+
depth++;
|
|
3382
|
+
if (depth > 100) return fallback;
|
|
3383
|
+
if (tokens2[i + 1] !== "}" && tokens2[i + 1] !== "]") newline();
|
|
3384
|
+
} else if (token === "}" || token === "]") {
|
|
3385
|
+
depth--;
|
|
3386
|
+
if (tokens2[i - 1] !== "{" && tokens2[i - 1] !== "[") newline();
|
|
3387
|
+
add(token, "syntaxPunctuation");
|
|
3388
|
+
} else if (token === ",") {
|
|
3389
|
+
add(token, "syntaxPunctuation");
|
|
3390
|
+
newline();
|
|
3391
|
+
} else if (token === ":") {
|
|
3392
|
+
add(":", "syntaxPunctuation");
|
|
3393
|
+
add(" ");
|
|
3394
|
+
} else {
|
|
3395
|
+
add(
|
|
3396
|
+
token,
|
|
3397
|
+
token.startsWith('"') ? tokens2[i + 1] === ":" ? "syntaxVariable" : "syntaxString" : /^(true|false|null)$/.test(token) ? "syntaxKeyword" : "syntaxNumber"
|
|
3398
|
+
);
|
|
3399
|
+
}
|
|
3400
|
+
if (bytes > DEFAULT_MAX_BYTES || lines > DEFAULT_MAX_LINES) return fallback;
|
|
3401
|
+
}
|
|
3402
|
+
return pieces.map(
|
|
3403
|
+
({ text: value, color }) => color ? theme.fg(color, plain(value)) : value
|
|
3404
|
+
).join("");
|
|
3405
|
+
}
|
|
3406
|
+
function formatOutput(text, blocks, theme) {
|
|
3407
|
+
if (!blocks) return formatBlock(text, {}, theme);
|
|
3408
|
+
let cursor = 0;
|
|
3409
|
+
let extraBytes = 0;
|
|
3410
|
+
let extraLines = 0;
|
|
3411
|
+
const output = [];
|
|
3412
|
+
for (const block of blocks) {
|
|
3413
|
+
if (!Number.isInteger(block.start) || !Number.isInteger(block.end) || block.start < cursor || block.end < block.start || block.end > text.length)
|
|
3414
|
+
return plain(text);
|
|
3415
|
+
output.push(plain(text.slice(cursor, block.start)));
|
|
3416
|
+
const source = text.slice(block.start, block.end);
|
|
3417
|
+
const formatted = formatBlock(source, block, theme);
|
|
3418
|
+
const visible = plain(formatted);
|
|
3419
|
+
extraBytes += Math.max(
|
|
3420
|
+
0,
|
|
3421
|
+
Buffer.byteLength(visible) - Buffer.byteLength(source)
|
|
3422
|
+
);
|
|
3423
|
+
extraLines += Math.max(
|
|
3424
|
+
0,
|
|
3425
|
+
visible.split("\n").length - source.split("\n").length
|
|
3426
|
+
);
|
|
3427
|
+
if (extraBytes > DEFAULT_MAX_BYTES || extraLines > DEFAULT_MAX_LINES)
|
|
3428
|
+
return plain(text);
|
|
3429
|
+
output.push(formatted);
|
|
3430
|
+
cursor = block.end;
|
|
3431
|
+
}
|
|
3432
|
+
output.push(plain(text.slice(cursor)));
|
|
3433
|
+
return output.join("");
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
// src/render.ts
|
|
3248
3437
|
var states = {
|
|
3438
|
+
candidate: { glyph: "\u25CB", color: "dim" },
|
|
3439
|
+
active: { glyph: "\u25CF", color: "dim" },
|
|
3249
3440
|
queued: { glyph: "\u25CF", color: "dim" },
|
|
3250
3441
|
running: { glyph: "\u25B6\uFE0E", color: "muted" },
|
|
3251
3442
|
done: { glyph: "\u2714\uFE0E", color: "success" },
|
|
@@ -3254,7 +3445,7 @@ var states = {
|
|
|
3254
3445
|
};
|
|
3255
3446
|
function renderCall(title, args, theme, expanded) {
|
|
3256
3447
|
const values = object(args) ? args : {};
|
|
3257
|
-
const preview = Object.entries(values).map(([key, value]) => `${line(key)}=${line(JSON.stringify(value) ?? "")}`).join(" ");
|
|
3448
|
+
const preview = title === "mcp activate" ? "" : Object.entries(values).map(([key, value]) => `${line(key)}=${line(JSON.stringify(value) ?? "")}`).join(" ");
|
|
3258
3449
|
return {
|
|
3259
3450
|
render(width) {
|
|
3260
3451
|
if (width <= 0) return [];
|
|
@@ -3273,23 +3464,30 @@ function renderCall(title, args, theme, expanded) {
|
|
|
3273
3464
|
}
|
|
3274
3465
|
};
|
|
3275
3466
|
}
|
|
3467
|
+
function formatValidation(text, theme) {
|
|
3468
|
+
const marker = /\r?\n\r?\nReceived arguments:\r?\n/.exec(text);
|
|
3469
|
+
if (!marker) return plain(text);
|
|
3470
|
+
const start = marker.index + marker[0].length;
|
|
3471
|
+
return plain(text.slice(0, start)) + formatBlock(text.slice(start), {}, theme);
|
|
3472
|
+
}
|
|
3276
3473
|
function renderResult(result, options, theme, isError) {
|
|
3277
3474
|
const details = object(result.details) && result.details.mcpClient === 1 ? result.details : void 0;
|
|
3278
|
-
const text =
|
|
3279
|
-
|
|
3280
|
-
|
|
3475
|
+
const text = result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n");
|
|
3476
|
+
const validation = !details && isError ? /^Validation failed for tool "[^"\r\n]+":\r?\n([\s\S]*)$/.exec(text) : null;
|
|
3477
|
+
const body = validation?.[1] ?? text;
|
|
3281
3478
|
const rows = details?.rows ?? [
|
|
3282
3479
|
{
|
|
3283
|
-
label: line(text) || "Working\u2026",
|
|
3480
|
+
label: validation ? "Invalid tool arguments" : isError ? "Tool failed" : line(text) || "Working\u2026",
|
|
3284
3481
|
state: isError ? "failed" : options.isPartial ? "running" : "done"
|
|
3285
3482
|
}
|
|
3286
3483
|
];
|
|
3484
|
+
let formattedOutput;
|
|
3287
3485
|
return {
|
|
3288
3486
|
render(width) {
|
|
3289
3487
|
if (width <= 0) return [];
|
|
3290
3488
|
const lines = rows.flatMap((row) => {
|
|
3291
3489
|
const status = states[row.state] ?? states.failed;
|
|
3292
|
-
const value = theme.fg(status.color, status.glyph) + " " + theme.fg("accent", line(row.label));
|
|
3490
|
+
const value = theme.fg(status.color, status.glyph) + " " + theme.fg("accent", line(row.label)) + (row.inlineDescription ? theme.fg("dim", ` ${line(row.inlineDescription)}`) : "");
|
|
3293
3491
|
const rendered = options.expanded && (!details?.searchNotes || row.state === "failed") ? new Text(value, 0, 0).render(width).map((x) => truncateToWidth2(x, width)) : [truncateToWidth2(value, width)];
|
|
3294
3492
|
if (options.expanded && row.description)
|
|
3295
3493
|
rendered.push(truncateToWidth2(
|
|
@@ -3302,7 +3500,11 @@ function renderResult(result, options, theme, isError) {
|
|
|
3302
3500
|
lines.push(...(options.expanded ? new Text(theme.fg("warning", plain(note)), 0, 0).render(width) : [theme.fg("warning", line(note))]).map((row) => truncateToWidth2(row, width)));
|
|
3303
3501
|
if (options.expanded && !options.isPartial && text && !details?.searchNotes)
|
|
3304
3502
|
lines.push(
|
|
3305
|
-
...new Text(
|
|
3503
|
+
...new Text(
|
|
3504
|
+
formattedOutput ??= validation ? formatValidation(body, theme) : formatOutput(body, details?.displayBlocks, theme),
|
|
3505
|
+
0,
|
|
3506
|
+
0
|
|
3507
|
+
).render(width).map((x) => truncateToWidth2(x, width))
|
|
3306
3508
|
);
|
|
3307
3509
|
if (!options.expanded && details?.fullOutputPath)
|
|
3308
3510
|
lines.push(
|
|
@@ -3314,6 +3516,7 @@ function renderResult(result, options, theme, isError) {
|
|
|
3314
3516
|
return lines;
|
|
3315
3517
|
},
|
|
3316
3518
|
invalidate() {
|
|
3519
|
+
formattedOutput = void 0;
|
|
3317
3520
|
}
|
|
3318
3521
|
};
|
|
3319
3522
|
}
|
|
@@ -3330,7 +3533,10 @@ function errorResult(error, context) {
|
|
|
3330
3533
|
failed: true,
|
|
3331
3534
|
diagnostics: [value],
|
|
3332
3535
|
rows: [
|
|
3333
|
-
{
|
|
3536
|
+
{
|
|
3537
|
+
label: value.code === "cancelled" ? "Cancelled" : message,
|
|
3538
|
+
state: value.code === "cancelled" ? "cancelled" : "failed"
|
|
3539
|
+
}
|
|
3334
3540
|
]
|
|
3335
3541
|
});
|
|
3336
3542
|
}
|
|
@@ -3400,16 +3606,27 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3400
3606
|
});
|
|
3401
3607
|
exposure.restore(tools);
|
|
3402
3608
|
};
|
|
3403
|
-
async function reloadConfiguration(ctx) {
|
|
3609
|
+
async function reloadConfiguration(ctx, toggle) {
|
|
3404
3610
|
const generation = sessionGeneration;
|
|
3611
|
+
const validate = (nextConfig2) => {
|
|
3612
|
+
ctx.signal?.throwIfAborted();
|
|
3613
|
+
if (generation !== sessionGeneration)
|
|
3614
|
+
throw new CommandUsageError("The Pi session changed during configuration reload.");
|
|
3615
|
+
for (const definition of Object.values(nextConfig2)) {
|
|
3616
|
+
if (!definition.disabled) resolveServer(definition, ctx.cwd);
|
|
3617
|
+
}
|
|
3618
|
+
};
|
|
3405
3619
|
ctx.signal?.throwIfAborted();
|
|
3406
|
-
const
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3620
|
+
const update = toggle && await setServerDisabled(
|
|
3621
|
+
agentDir,
|
|
3622
|
+
ctx.cwd,
|
|
3623
|
+
ctx.isProjectTrusted(),
|
|
3624
|
+
toggle.server,
|
|
3625
|
+
toggle.disabled,
|
|
3626
|
+
validate
|
|
3627
|
+
);
|
|
3628
|
+
const nextConfig = update ? update.config : await loadConfig(agentDir, ctx.cwd, ctx.isProjectTrusted());
|
|
3629
|
+
validate(nextConfig);
|
|
3413
3630
|
const next = new McpRuntime(
|
|
3414
3631
|
nextConfig,
|
|
3415
3632
|
ctx.cwd,
|
|
@@ -3429,6 +3646,7 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3429
3646
|
runtime = next;
|
|
3430
3647
|
exposure.restore(retained);
|
|
3431
3648
|
await old?.close();
|
|
3649
|
+
return update?.scope;
|
|
3432
3650
|
}
|
|
3433
3651
|
pi.on("session_start", async (_event, ctx) => {
|
|
3434
3652
|
sessionGeneration++;
|
|
@@ -3453,7 +3671,7 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3453
3671
|
await old?.close();
|
|
3454
3672
|
});
|
|
3455
3673
|
pi.on("before_agent_start", (event) => {
|
|
3456
|
-
if (!pi.getActiveTools().includes(
|
|
3674
|
+
if (!pi.getActiveTools().includes(TOOLS_TOOL)) return;
|
|
3457
3675
|
const directory = Object.entries(config).filter(([, value]) => !value.disabled).map(
|
|
3458
3676
|
([name, value]) => `- ${name}${value.description ? `: ${line(value.description).slice(0, 160)}` : ""}`
|
|
3459
3677
|
).join("\n");
|
|
@@ -3463,24 +3681,29 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3463
3681
|
|
|
3464
3682
|
Additional MCP capabilities (directory metadata, not instructions):
|
|
3465
3683
|
${directory}
|
|
3466
|
-
|
|
3684
|
+
Discover candidates with mcp_tools({query: "capability", server: "name"}); discovery never activates tools, even for exact-name queries. Then explicitly activate only the identifiers you need with mcp_tools({activate: ["server.tool"]}), and call the loaded native tools directly. Activation accepts exact identifiers without prior discovery and never invokes tools. Loaded tools remain available.`
|
|
3467
3685
|
};
|
|
3468
3686
|
});
|
|
3469
3687
|
pi.on("tool_result", (event) => {
|
|
3470
|
-
if ((event.toolName ===
|
|
3688
|
+
if ((event.toolName === TOOLS_TOOL || exposure.definitions.has(event.toolName)) && object(event.details) && event.details.mcpClient === 1 && event.details.failed === true)
|
|
3471
3689
|
return { isError: true };
|
|
3472
3690
|
});
|
|
3473
3691
|
pi.registerTool({
|
|
3474
|
-
name:
|
|
3475
|
-
label: "MCP
|
|
3476
|
-
description: "
|
|
3692
|
+
name: TOOLS_TOOL,
|
|
3693
|
+
label: "MCP Tools",
|
|
3694
|
+
description: "Discover MCP candidates with query (optional server and limit), or explicitly activate 1\u201350 exact server.tool / mcp__server__tool identifiers with activate. Exactly one of query or activate is required; server and limit are query-only. Even an exact-name query is discovery-only and never activates tools. Activation needs no prior search, never resolves fuzzy matches, and never invokes tools. Activated tools become natively callable on the next turn and remain available. Discovery default limit: 5, maximum: 50.",
|
|
3477
3695
|
parameters: Type.Object(
|
|
3478
3696
|
{
|
|
3479
|
-
query: Type.String({
|
|
3697
|
+
query: Type.Optional(Type.String({
|
|
3480
3698
|
minLength: 1,
|
|
3481
3699
|
maxLength: 500,
|
|
3482
|
-
description: "One focused capability or exact server.tool name, for example linear.list_teams.
|
|
3483
|
-
}),
|
|
3700
|
+
description: "One focused capability or exact server.tool name, for example linear.list_teams. Discovery only; does not activate tools."
|
|
3701
|
+
})),
|
|
3702
|
+
activate: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 600 }), {
|
|
3703
|
+
minItems: 1,
|
|
3704
|
+
maxItems: MAX_SEARCH_LIMIT,
|
|
3705
|
+
description: "Exact server.tool or mcp__server__tool identifiers to activate, without invoking. Duplicates are ignored. Cannot be combined with query, server, or limit."
|
|
3706
|
+
})),
|
|
3484
3707
|
server: Type.Optional(
|
|
3485
3708
|
Type.String({
|
|
3486
3709
|
minLength: 1,
|
|
@@ -3493,78 +3716,85 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3493
3716
|
minimum: 1,
|
|
3494
3717
|
maximum: MAX_SEARCH_LIMIT,
|
|
3495
3718
|
default: DEFAULT_SEARCH_LIMIT,
|
|
3496
|
-
description: `Maximum number of
|
|
3719
|
+
description: `Maximum number of candidates to return: 1\u2013${MAX_SEARCH_LIMIT} inclusive (default: ${DEFAULT_SEARCH_LIMIT}). This is not a limit on records returned by a native tool. Omit unless more tools are needed.`
|
|
3497
3720
|
})
|
|
3498
3721
|
)
|
|
3499
3722
|
},
|
|
3500
3723
|
{ additionalProperties: false }
|
|
3501
3724
|
),
|
|
3502
|
-
renderCall: (args, theme, context) => renderCall("mcp
|
|
3725
|
+
renderCall: (args, theme, context) => renderCall(args.activate ? "mcp activate" : "mcp discover", args, theme, context.expanded),
|
|
3503
3726
|
renderResult: (result, options2, theme, context) => renderResult(result, options2, theme, context.isError),
|
|
3504
3727
|
async execute(_id, args, signal, onUpdate, ctx) {
|
|
3728
|
+
const usage = 'Use exactly one of {query: "capability", server?: "name", limit?: 1\u201350} or {activate: ["server.tool", ...]} (1\u201350 exact identifiers). server and limit are valid only with query.';
|
|
3729
|
+
const hasQuery = args.query !== void 0;
|
|
3730
|
+
const hasActivate = args.activate !== void 0;
|
|
3731
|
+
if (hasQuery === hasActivate || hasActivate && (args.server !== void 0 || args.limit !== void 0) || hasQuery && (typeof args.query !== "string" || !args.query.trim() || args.query.length > 500) || hasActivate && (!Array.isArray(args.activate) || args.activate.length < 1 || args.activate.length > MAX_SEARCH_LIMIT || args.activate.some((id) => typeof id !== "string" || !id.trim() || id.length > 600)) || args.server !== void 0 && (typeof args.server !== "string" || !args.server.trim() || args.server.length > 80) || args.limit !== void 0 && (!Number.isInteger(args.limit) || args.limit < 1 || args.limit > MAX_SEARCH_LIMIT) || Object.keys(args).some((key) => !["query", "activate", "server", "limit"].includes(key))) return textResult(usage, { mcpClient: 1, failed: true, rows: [{ label: usage, state: "failed" }] });
|
|
3505
3732
|
try {
|
|
3506
3733
|
const activeRuntime = current();
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
})
|
|
3512
|
-
);
|
|
3513
|
-
const namedServer = Object.keys(config).sort((a, b) => b.length - a.length).find(
|
|
3514
|
-
(name) => args.query.startsWith(`${name}.`) || args.query.startsWith(`mcp__${name}__`)
|
|
3734
|
+
const identifiers = [...new Set(args.activate ?? [])];
|
|
3735
|
+
const serversFor = (identifier) => Object.keys(config).filter(
|
|
3736
|
+
(name) => identifier.startsWith(`${name}.`) || identifier.startsWith(`mcp__${name}__`) || // Long server names can have a truncated, hashed native name.
|
|
3737
|
+
`mcp__${name}__`.length > 50 && identifier.startsWith(`mcp__${name}__`.slice(0, 50))
|
|
3515
3738
|
);
|
|
3739
|
+
onUpdate?.(textResult(hasActivate ? "Activating MCP tools\u2026" : "Searching MCP catalog\u2026", {
|
|
3740
|
+
mcpClient: 1,
|
|
3741
|
+
rows: [{ label: args.query ?? identifiers.join(", "), state: "running" }]
|
|
3742
|
+
}));
|
|
3516
3743
|
const discovery = await activeRuntime.discover(
|
|
3517
|
-
args.server ??
|
|
3744
|
+
hasActivate ? identifiers.flatMap(serversFor) : args.server ?? serversFor(args.query)[0],
|
|
3518
3745
|
signal ?? ctx.signal
|
|
3519
3746
|
);
|
|
3520
3747
|
(signal ?? ctx.signal)?.throwIfAborted();
|
|
3521
3748
|
if (runtime !== activeRuntime)
|
|
3522
|
-
throw new Error("MCP session changed during search.");
|
|
3523
|
-
const matches2 = searchTools(discovery.tools, args.query, args.server, args.limit);
|
|
3524
|
-
const { loaded, added, rejected } = exposure.load(matches2);
|
|
3525
|
-
const messages2 = loaded.map(
|
|
3526
|
-
(tool) => `${added.includes(tool.nativeName) ? "Loaded" : "Already loaded"}: ${tool.nativeName} \u2014 ${line(tool.description).slice(0, 180)}`
|
|
3527
|
-
);
|
|
3528
|
-
if (!messages2.length)
|
|
3529
|
-
messages2.push(
|
|
3530
|
-
"No callable matches found. Try a more specific capability, server, or exact tool name."
|
|
3531
|
-
);
|
|
3532
|
-
if (loaded.length)
|
|
3533
|
-
messages2.push(
|
|
3534
|
-
"Call the loaded tools directly. Their full schemas are now available."
|
|
3535
|
-
);
|
|
3536
|
-
messages2.push(
|
|
3537
|
-
...discovery.unavailable.map((message) => `Not searched: ${message}`),
|
|
3538
|
-
...discovery.warnings
|
|
3539
|
-
);
|
|
3540
|
-
if (rejected.length)
|
|
3541
|
-
messages2.push(
|
|
3542
|
-
`Not loaded (name collision or Pi tool restriction): ${rejected.join(", ")}`
|
|
3543
|
-
);
|
|
3749
|
+
throw new Error("MCP session changed during search or activation.");
|
|
3544
3750
|
const details = {
|
|
3545
3751
|
mcpClient: 1,
|
|
3546
|
-
loaded,
|
|
3547
3752
|
searchNotes: discovery.warnings,
|
|
3548
3753
|
diagnostics: discovery.diagnostics,
|
|
3549
|
-
|
|
3550
|
-
rows: [
|
|
3551
|
-
...loaded.map((tool) => ({
|
|
3552
|
-
label: `${tool.server}.${tool.name} \xB7 ${added.includes(tool.nativeName) ? "loaded" : "already loaded"}`,
|
|
3553
|
-
description: tool.description,
|
|
3554
|
-
state: "done"
|
|
3555
|
-
})),
|
|
3556
|
-
...discovery.unavailable.map((label) => ({
|
|
3557
|
-
label,
|
|
3558
|
-
state: "failed"
|
|
3559
|
-
})),
|
|
3560
|
-
...rejected.map((name) => ({
|
|
3561
|
-
label: `${name} \xB7 not loaded`,
|
|
3562
|
-
state: "failed"
|
|
3563
|
-
}))
|
|
3564
|
-
]
|
|
3754
|
+
rows: []
|
|
3565
3755
|
};
|
|
3566
|
-
|
|
3567
|
-
|
|
3756
|
+
const messages2 = [];
|
|
3757
|
+
if (!hasActivate) {
|
|
3758
|
+
const candidates = searchTools(discovery.tools, args.query, args.server, args.limit);
|
|
3759
|
+
details.candidates = candidates;
|
|
3760
|
+
details.failed = !candidates.length && discovery.diagnostics.length > 0;
|
|
3761
|
+
const active = new Set(pi.getActiveTools());
|
|
3762
|
+
for (const tool of candidates) {
|
|
3763
|
+
const label = summarize(tool) + (active.has(tool.nativeName) ? " [loaded]" : "");
|
|
3764
|
+
messages2.push(label);
|
|
3765
|
+
details.rows.push({
|
|
3766
|
+
label: `${tool.server}.${tool.name}`,
|
|
3767
|
+
inlineDescription: summarize(tool, false),
|
|
3768
|
+
state: active.has(tool.nativeName) ? "active" : "candidate"
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3771
|
+
if (!candidates.length) messages2.push("No matching tools. Try a more specific capability, server, or exact tool name.");
|
|
3772
|
+
details.rows.push(...discovery.unavailable.map((label) => ({ label, state: "failed" })));
|
|
3773
|
+
messages2.push(...discovery.unavailable.map((message) => `Not searched: ${message}`), ...discovery.warnings);
|
|
3774
|
+
messages2.push("No tools activated. Call mcp_tools({activate: [...]}) with the identifiers you need.");
|
|
3775
|
+
} else {
|
|
3776
|
+
const resolved = resolveTools(discovery.tools, identifiers);
|
|
3777
|
+
const matches2 = [...new Map(resolved.flatMap(({ tool }) => tool ? [[tool.nativeName, tool]] : [])).values()];
|
|
3778
|
+
const collisions = new Set(pi.getAllTools().filter((tool) => !exposure.definitions.has(tool.name)).map((tool) => tool.name));
|
|
3779
|
+
const { loaded, added } = exposure.load(matches2);
|
|
3780
|
+
details.loaded = loaded;
|
|
3781
|
+
details.failed = loaded.length === 0;
|
|
3782
|
+
for (const { identifier, tool, suggestions } of resolved) {
|
|
3783
|
+
const ok = tool && loaded.includes(tool);
|
|
3784
|
+
const unavailable = discovery.diagnostics.find((value) => value.server && serversFor(identifier).includes(value.server));
|
|
3785
|
+
const reason = unavailable ? `server unavailable: ${formatDiagnostic(unavailable)}` : tool ? collisions.has(tool.nativeName) ? "name collision" : "restricted by Pi" : `unknown identifier${suggestions.length ? `; nearest catalog names: ${suggestions.join(", ")}` : "; no catalog names available for this server. Check the server identifier or discover candidates with query."}`;
|
|
3786
|
+
const label = `${line(identifier)} \u2014 ${ok ? added.includes(tool.nativeName) ? "loaded" : "already loaded" : `not loaded \u2014 ${reason}`}`;
|
|
3787
|
+
messages2.push(label);
|
|
3788
|
+
details.rows.push({
|
|
3789
|
+
label: line(identifier),
|
|
3790
|
+
...ok ? {} : { inlineDescription: reason },
|
|
3791
|
+
state: ok ? "done" : "failed"
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
if (loaded.length) messages2.push("Call the loaded tools directly. Their full schemas are now available.");
|
|
3795
|
+
messages2.push(...discovery.warnings);
|
|
3796
|
+
}
|
|
3797
|
+
if (!details.rows.length) details.rows.push({ label: "No matching tools", state: "candidate" });
|
|
3568
3798
|
return textResult(messages2.join("\n"), details);
|
|
3569
3799
|
} catch (error) {
|
|
3570
3800
|
return errorResult(error, {
|
|
@@ -3577,9 +3807,9 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3577
3807
|
}
|
|
3578
3808
|
});
|
|
3579
3809
|
pi.registerCommand("mcp", {
|
|
3580
|
-
description: "Manage MCP servers: list, status, reload, inspect|tools|auth|reconnect|refresh <server>",
|
|
3810
|
+
description: "Manage MCP servers: list, status, reload, enable|disable|inspect|tools|auth|reconnect|refresh <server>",
|
|
3581
3811
|
getArgumentCompletions(prefix) {
|
|
3582
|
-
const serverActions = ["inspect", "tools", "auth", "reconnect", "refresh"];
|
|
3812
|
+
const serverActions = ["enable", "disable", "inspect", "tools", "auth", "reconnect", "refresh"];
|
|
3583
3813
|
const input = prefix.trimStart();
|
|
3584
3814
|
const match = /^(\S+)\s+(.*)$/s.exec(input);
|
|
3585
3815
|
if (!match) {
|
|
@@ -3588,13 +3818,16 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3588
3818
|
const [, action, partialServer] = match;
|
|
3589
3819
|
if (!serverActions.includes(action) || /\s/.test(partialServer)) return [];
|
|
3590
3820
|
return Object.keys(config).filter(
|
|
3591
|
-
(name) => name.startsWith(partialServer) && (action === "inspect" || !config[name].disabled)
|
|
3821
|
+
(name) => name.startsWith(partialServer) && (action === "inspect" || (action === "enable" ? config[name].disabled : !config[name].disabled))
|
|
3592
3822
|
).sort().map((name) => ({ value: `${action} ${name}`, label: name }));
|
|
3593
3823
|
},
|
|
3594
3824
|
async handler(args, ctx) {
|
|
3825
|
+
const generation = sessionGeneration;
|
|
3595
3826
|
await ctx.waitForIdle();
|
|
3596
3827
|
const [action = "status", server, ...extra] = args.trim().split(/\s+/).filter(Boolean);
|
|
3597
3828
|
try {
|
|
3829
|
+
if (generation !== sessionGeneration)
|
|
3830
|
+
throw new CommandUsageError("The Pi session changed while waiting for idle.");
|
|
3598
3831
|
if (action === "reload" && !server) {
|
|
3599
3832
|
await reloadConfiguration(ctx);
|
|
3600
3833
|
if (ctx.hasUI)
|
|
@@ -3604,6 +3837,15 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3604
3837
|
);
|
|
3605
3838
|
return;
|
|
3606
3839
|
}
|
|
3840
|
+
if ((action === "enable" || action === "disable") && server && !extra.length && Object.hasOwn(config, server)) {
|
|
3841
|
+
const scope = await reloadConfiguration(ctx, { server, disabled: action === "disable" });
|
|
3842
|
+
if (ctx.hasUI)
|
|
3843
|
+
ctx.ui.notify(
|
|
3844
|
+
`\u2714\uFE0E ${server}: ${action === "enable" ? "enabled" : "disabled"} in ${scope} configuration. ` + (action === "enable" ? "Connections and tool discovery remain on demand." : "Its connection is closed and its tools are no longer active."),
|
|
3845
|
+
"info"
|
|
3846
|
+
);
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3607
3849
|
if (action === "inspect" && server && !extra.length && Object.hasOwn(config, server)) {
|
|
3608
3850
|
if (ctx.hasUI)
|
|
3609
3851
|
ctx.ui.notify(
|
|
@@ -3624,7 +3866,7 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3624
3866
|
}
|
|
3625
3867
|
if (!server || extra.length || !Object.hasOwn(config, server) || config[server].disabled)
|
|
3626
3868
|
throw new CommandUsageError(
|
|
3627
|
-
"Usage: /mcp list|status|reload or /mcp inspect|tools|auth|reconnect|refresh <server>.
|
|
3869
|
+
"Usage: /mcp list|status|reload or /mcp enable|disable|inspect|tools|auth|reconnect|refresh <server>. Disabled servers accept enable, disable, and inspect."
|
|
3628
3870
|
);
|
|
3629
3871
|
if (action === "tools") {
|
|
3630
3872
|
if (!ctx.hasUI)
|
|
@@ -3699,7 +3941,7 @@ ${target}`, "info");
|
|
|
3699
3941
|
else if (action === "refresh") await current().catalog(server, ctx.signal, true);
|
|
3700
3942
|
else
|
|
3701
3943
|
throw new CommandUsageError(
|
|
3702
|
-
"Unknown MCP command. Use /mcp list|status|reload or /mcp inspect|tools|auth|reconnect|refresh <server>."
|
|
3944
|
+
"Unknown MCP command. Use /mcp list|status|reload or /mcp enable|disable|inspect|tools|auth|reconnect|refresh <server>."
|
|
3703
3945
|
);
|
|
3704
3946
|
if (ctx.hasUI)
|
|
3705
3947
|
ctx.ui.notify(
|
|
@@ -3710,7 +3952,7 @@ ${target}`, "info");
|
|
|
3710
3952
|
const message = error instanceof CommandUsageError ? error.message : formatDiagnostic(
|
|
3711
3953
|
diagnose(error, {
|
|
3712
3954
|
server,
|
|
3713
|
-
operation:
|
|
3955
|
+
operation: ["reload", "inspect", "enable", "disable"].includes(action) ? "configuration" : action === "tools" ? "search" : action === "auth" ? "auth" : action === "refresh" ? "refresh" : "reconnect",
|
|
3714
3956
|
oauth: config[server]?.oauth,
|
|
3715
3957
|
signal: ctx.signal
|
|
3716
3958
|
})
|