pi-mcp-client 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -23
- package/dist/index.js +380 -126
- 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,9 @@ 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
|
-
|
|
33
|
+
server_unknown: "Choose a configured MCP server from the capability directory or run /mcp to list servers. Omit server in mcp_tools to search all enabled servers.",
|
|
34
|
+
server_disabled: `Run /mcp enable ${target}, or omit server in mcp_tools to search all enabled servers.`,
|
|
35
|
+
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
36
|
tool_error: "Review the server's tool result and inputs. Verify the outcome before retrying.",
|
|
35
37
|
oauth_failed: `Check OAuth support and browser access to the local callback, then run /mcp auth ${target}.`,
|
|
36
38
|
callback_unavailable: "Free local port 19847, then retry authentication.",
|
|
@@ -137,6 +139,8 @@ var init_diagnostics = __esm({
|
|
|
137
139
|
cancelled: "The operation was cancelled.",
|
|
138
140
|
connection_failed: "The server connection failed.",
|
|
139
141
|
protocol_error: "The server response or protocol is not supported.",
|
|
142
|
+
server_unknown: "The MCP server is not configured.",
|
|
143
|
+
server_disabled: "The MCP server is disabled.",
|
|
140
144
|
tool_changed: "The tool is unavailable or its configuration or schema changed.",
|
|
141
145
|
tool_error: "The tool reported an error.",
|
|
142
146
|
oauth_failed: "OAuth authentication did not complete.",
|
|
@@ -285,12 +289,14 @@ __export(config_exports, {
|
|
|
285
289
|
matches: () => matches,
|
|
286
290
|
object: () => object,
|
|
287
291
|
parseConfig: () => parseConfig,
|
|
288
|
-
resolveServer: () => resolveServer
|
|
292
|
+
resolveServer: () => resolveServer,
|
|
293
|
+
setServerDisabled: () => setServerDisabled
|
|
289
294
|
});
|
|
290
|
-
import { readFile } from "node:fs/promises";
|
|
295
|
+
import { chmod, readFile, realpath, stat, writeFile, rename, rm } from "node:fs/promises";
|
|
296
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
291
297
|
import { resolve, join } from "node:path";
|
|
292
298
|
import { homedir } from "node:os";
|
|
293
|
-
import { createHash } from "node:crypto";
|
|
299
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
294
300
|
function object(value) {
|
|
295
301
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
296
302
|
}
|
|
@@ -386,6 +392,46 @@ async function loadConfig(agentDir, cwd, trusted) {
|
|
|
386
392
|
}
|
|
387
393
|
return config;
|
|
388
394
|
}
|
|
395
|
+
async function setServerDisabled(agentDir, cwd, trusted, server, disabled, validate) {
|
|
396
|
+
const paths = [join(agentDir, "mcp.json"), ...trusted ? [join(cwd, ".mcp.json")] : []];
|
|
397
|
+
const locks = [...new Set(await Promise.all(
|
|
398
|
+
paths.map((path) => realpath(path).catch(() => resolve(path)))
|
|
399
|
+
))].sort();
|
|
400
|
+
const locked = async (index) => {
|
|
401
|
+
if (index < locks.length)
|
|
402
|
+
return withFileMutationQueue(locks[index], () => locked(index + 1));
|
|
403
|
+
const documents = await Promise.all(paths.map(readJson));
|
|
404
|
+
const config = /* @__PURE__ */ Object.create(null);
|
|
405
|
+
let source = -1;
|
|
406
|
+
for (const [index2, document2] of documents.entries()) {
|
|
407
|
+
if (document2 === void 0) continue;
|
|
408
|
+
const parsed = parseConfig(document2, paths[index2]);
|
|
409
|
+
Object.assign(config, parsed);
|
|
410
|
+
if (Object.hasOwn(parsed, server)) source = index2;
|
|
411
|
+
}
|
|
412
|
+
if (source < 0) throw new Error("Server is no longer configured. Run /mcp reload.");
|
|
413
|
+
const changed = Boolean(config[server].disabled) !== disabled;
|
|
414
|
+
if (changed) config[server].disabled = disabled;
|
|
415
|
+
validate(config);
|
|
416
|
+
const document = documents[source];
|
|
417
|
+
if (changed) {
|
|
418
|
+
document.mcpServers[server].disabled = disabled;
|
|
419
|
+
const target = await realpath(paths[source]);
|
|
420
|
+
const mode = (await stat(target)).mode & 511;
|
|
421
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
422
|
+
try {
|
|
423
|
+
await writeFile(temporary, JSON.stringify(document, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
424
|
+
await chmod(temporary, mode);
|
|
425
|
+
validate(config);
|
|
426
|
+
await rename(temporary, target);
|
|
427
|
+
} finally {
|
|
428
|
+
await rm(temporary, { force: true });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return { config, scope: source === 0 ? "global" : "project" };
|
|
432
|
+
};
|
|
433
|
+
return locked(0);
|
|
434
|
+
}
|
|
389
435
|
function interpolate(value, env = process.env) {
|
|
390
436
|
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => {
|
|
391
437
|
const found = env[name];
|
|
@@ -547,9 +593,9 @@ var recurse = (node, query, maxDistance, results, matrix, m, n, prefix) => {
|
|
|
547
593
|
const offset = m * n;
|
|
548
594
|
key: for (const key of node.keys()) {
|
|
549
595
|
if (key === LEAF) {
|
|
550
|
-
const
|
|
551
|
-
if (
|
|
552
|
-
results.set(prefix, [node.get(key),
|
|
596
|
+
const distance2 = matrix[offset - 1];
|
|
597
|
+
if (distance2 <= maxDistance) {
|
|
598
|
+
results.set(prefix, [node.get(key), distance2]);
|
|
553
599
|
}
|
|
554
600
|
} else {
|
|
555
601
|
let i = m;
|
|
@@ -1903,22 +1949,22 @@ var MiniSearch = class _MiniSearch {
|
|
|
1903
1949
|
}
|
|
1904
1950
|
if (prefixMatches) {
|
|
1905
1951
|
for (const [term, data2] of prefixMatches) {
|
|
1906
|
-
const
|
|
1907
|
-
if (!
|
|
1952
|
+
const distance2 = term.length - query.term.length;
|
|
1953
|
+
if (!distance2) {
|
|
1908
1954
|
continue;
|
|
1909
1955
|
}
|
|
1910
1956
|
fuzzyMatches === null || fuzzyMatches === void 0 ? void 0 : fuzzyMatches.delete(term);
|
|
1911
|
-
const weight = prefixWeight * term.length / (term.length + 0.3 *
|
|
1957
|
+
const weight = prefixWeight * term.length / (term.length + 0.3 * distance2);
|
|
1912
1958
|
this.termResults(query.term, term, weight, query.termBoost, data2, boosts, boostDocument, bm25params, results);
|
|
1913
1959
|
}
|
|
1914
1960
|
}
|
|
1915
1961
|
if (fuzzyMatches) {
|
|
1916
1962
|
for (const term of fuzzyMatches.keys()) {
|
|
1917
|
-
const [data2,
|
|
1918
|
-
if (!
|
|
1963
|
+
const [data2, distance2] = fuzzyMatches.get(term);
|
|
1964
|
+
if (!distance2) {
|
|
1919
1965
|
continue;
|
|
1920
1966
|
}
|
|
1921
|
-
const weight = fuzzyWeight * term.length / (term.length +
|
|
1967
|
+
const weight = fuzzyWeight * term.length / (term.length + distance2);
|
|
1922
1968
|
this.termResults(query.term, term, weight, query.termBoost, data2, boosts, boostDocument, bm25params, results);
|
|
1923
1969
|
}
|
|
1924
1970
|
}
|
|
@@ -2310,6 +2356,41 @@ function prepareTool(server, identity, tool) {
|
|
|
2310
2356
|
schemaHash: fingerprint(inputSchema)
|
|
2311
2357
|
};
|
|
2312
2358
|
}
|
|
2359
|
+
function summarize(tool, includeIdentifier = true) {
|
|
2360
|
+
const schema = tool.inputSchema;
|
|
2361
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((name) => typeof name === "string") : [];
|
|
2362
|
+
const prefix = includeIdentifier ? `${tool.server}.${tool.name} \u2014 ` : "";
|
|
2363
|
+
return `${prefix}${line(tool.description).slice(0, 180)} (required: ${required.map(line).join(", ") || "none"})`;
|
|
2364
|
+
}
|
|
2365
|
+
function distance(a, b) {
|
|
2366
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
2367
|
+
for (let i = 0; i < a.length; i++) {
|
|
2368
|
+
let diagonal = row[0];
|
|
2369
|
+
row[0] = i + 1;
|
|
2370
|
+
for (let j = 0; j < b.length; j++) {
|
|
2371
|
+
const previous = row[j + 1];
|
|
2372
|
+
row[j + 1] = Math.min(row[j] + 1, previous + 1, diagonal + Number(a[i] !== b[j]));
|
|
2373
|
+
diagonal = previous;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
return row[b.length];
|
|
2377
|
+
}
|
|
2378
|
+
function resolveTools(tools, identifiers) {
|
|
2379
|
+
return [...new Set(identifiers)].map((identifier) => {
|
|
2380
|
+
const matches2 = tools.filter(
|
|
2381
|
+
(tool2) => identifier === tool2.nativeName || identifier === `${tool2.server}.${tool2.name}`
|
|
2382
|
+
);
|
|
2383
|
+
const tool = matches2.length === 1 ? matches2[0] : void 0;
|
|
2384
|
+
const suggestions = tool ? [] : tools.map((candidate) => ({
|
|
2385
|
+
name: `${candidate.server}.${candidate.name}`,
|
|
2386
|
+
score: Math.min(
|
|
2387
|
+
distance(identifier, `${candidate.server}.${candidate.name}`),
|
|
2388
|
+
distance(identifier, candidate.nativeName)
|
|
2389
|
+
)
|
|
2390
|
+
})).sort((a, b) => a.score - b.score || a.name.localeCompare(b.name)).slice(0, 3).map(({ name }) => name);
|
|
2391
|
+
return { identifier, tool, suggestions };
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2313
2394
|
var STOP = /* @__PURE__ */ new Set([
|
|
2314
2395
|
"a",
|
|
2315
2396
|
"an",
|
|
@@ -2467,7 +2548,7 @@ function inspectServer(name, config, status) {
|
|
|
2467
2548
|
// src/auth.ts
|
|
2468
2549
|
init_config();
|
|
2469
2550
|
init_diagnostics();
|
|
2470
|
-
import { randomUUID } from "node:crypto";
|
|
2551
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2471
2552
|
import { createServer } from "node:http";
|
|
2472
2553
|
import {
|
|
2473
2554
|
auth
|
|
@@ -2517,7 +2598,7 @@ var OAuthProvider = class {
|
|
|
2517
2598
|
data;
|
|
2518
2599
|
verifier;
|
|
2519
2600
|
discovery;
|
|
2520
|
-
expectedState =
|
|
2601
|
+
expectedState = randomUUID2();
|
|
2521
2602
|
save() {
|
|
2522
2603
|
this.store.write(JSON.stringify(this.data));
|
|
2523
2604
|
}
|
|
@@ -2641,9 +2722,9 @@ async function authenticate(url, open, signal, store) {
|
|
|
2641
2722
|
|
|
2642
2723
|
// src/runtime.ts
|
|
2643
2724
|
init_config();
|
|
2644
|
-
import { mkdir, readFile as readFile2, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2725
|
+
import { mkdir, readFile as readFile2, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2645
2726
|
import { join as join2 } from "node:path";
|
|
2646
|
-
import { randomUUID as
|
|
2727
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2647
2728
|
import {
|
|
2648
2729
|
Client,
|
|
2649
2730
|
StreamableHTTPClientTransport
|
|
@@ -2652,7 +2733,7 @@ import {
|
|
|
2652
2733
|
StdioClientTransport,
|
|
2653
2734
|
getDefaultEnvironment
|
|
2654
2735
|
} from "@modelcontextprotocol/client/stdio";
|
|
2655
|
-
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
2736
|
+
import { withFileMutationQueue as withFileMutationQueue2 } from "@earendil-works/pi-coding-agent";
|
|
2656
2737
|
init_secrets();
|
|
2657
2738
|
init_diagnostics();
|
|
2658
2739
|
var ToolContractError = class extends Error {
|
|
@@ -2825,9 +2906,9 @@ var McpRuntime = class {
|
|
|
2825
2906
|
state.tools = void 0;
|
|
2826
2907
|
state.warnings = void 0;
|
|
2827
2908
|
const path = join2(this.cacheDir, `${identity}.json`);
|
|
2828
|
-
state.invalidating =
|
|
2909
|
+
state.invalidating = withFileMutationQueue2(
|
|
2829
2910
|
path,
|
|
2830
|
-
() =>
|
|
2911
|
+
() => rm2(path, { force: true })
|
|
2831
2912
|
).catch(() => {
|
|
2832
2913
|
state.warnings = [
|
|
2833
2914
|
`${name}: stale catalog cache could not be removed.`
|
|
@@ -2934,7 +3015,7 @@ var McpRuntime = class {
|
|
|
2934
3015
|
return tools;
|
|
2935
3016
|
}
|
|
2936
3017
|
throw new ToolContractError(
|
|
2937
|
-
"MCP tool catalog kept changing.
|
|
3018
|
+
"MCP tool catalog kept changing. Retry discovery or activation."
|
|
2938
3019
|
);
|
|
2939
3020
|
})().catch((error) => {
|
|
2940
3021
|
state.error = this.failure(name, error);
|
|
@@ -2947,8 +3028,15 @@ var McpRuntime = class {
|
|
|
2947
3028
|
return waitFor(state.listing, signal);
|
|
2948
3029
|
}
|
|
2949
3030
|
async discover(server, signal) {
|
|
2950
|
-
if (server)
|
|
2951
|
-
|
|
3031
|
+
if (typeof server === "string") {
|
|
3032
|
+
const config = Object.hasOwn(this.config, server) ? this.config[server] : void 0;
|
|
3033
|
+
if (!config || config.disabled)
|
|
3034
|
+
throw new DiagnosticError(diagnostic(
|
|
3035
|
+
config ? "server_disabled" : "server_unknown",
|
|
3036
|
+
{ server, operation: "search" }
|
|
3037
|
+
));
|
|
3038
|
+
}
|
|
3039
|
+
const names = Array.isArray(server) ? [...new Set(server)] : server ? [server] : Object.keys(this.config).filter((name) => !this.config[name].disabled).sort();
|
|
2952
3040
|
const result = {
|
|
2953
3041
|
tools: [],
|
|
2954
3042
|
unavailable: [],
|
|
@@ -2985,7 +3073,7 @@ var McpRuntime = class {
|
|
|
2985
3073
|
const found = current.find((candidate) => candidate.name === tool.name);
|
|
2986
3074
|
if (!found || found.schemaHash !== tool.schemaHash)
|
|
2987
3075
|
throw new ToolContractError(
|
|
2988
|
-
"MCP tool was removed or its schema changed.
|
|
3076
|
+
"MCP tool was removed or its schema changed. Use mcp_tools with activate and its exact identifier to activate its current definition."
|
|
2989
3077
|
);
|
|
2990
3078
|
const client = await waitFor(this.client(tool.server), signal);
|
|
2991
3079
|
try {
|
|
@@ -3074,7 +3162,7 @@ var McpRuntime = class {
|
|
|
3074
3162
|
async readCache(name, identity) {
|
|
3075
3163
|
try {
|
|
3076
3164
|
const path = join2(this.cacheDir, `${identity}.json`);
|
|
3077
|
-
const info = await
|
|
3165
|
+
const info = await stat2(path);
|
|
3078
3166
|
if (info.size > 4 * 1024 * 1024 || Date.now() - info.mtimeMs > 864e5)
|
|
3079
3167
|
return;
|
|
3080
3168
|
const data = JSON.parse(await readFile2(path, "utf8"));
|
|
@@ -3095,28 +3183,28 @@ var McpRuntime = class {
|
|
|
3095
3183
|
if (Buffer.byteLength(text) > 4 * 1024 * 1024) return;
|
|
3096
3184
|
await mkdir(this.cacheDir, { recursive: true, mode: 448 });
|
|
3097
3185
|
const path = join2(this.cacheDir, `${identity}.json`);
|
|
3098
|
-
await
|
|
3186
|
+
await withFileMutationQueue2(path, async () => {
|
|
3099
3187
|
if (!isCurrent()) return;
|
|
3100
|
-
const temp = `${path}.${
|
|
3101
|
-
await
|
|
3102
|
-
if (isCurrent()) await
|
|
3103
|
-
else await
|
|
3188
|
+
const temp = `${path}.${randomUUID3()}.tmp`;
|
|
3189
|
+
await writeFile2(temp, text, { mode: 384 });
|
|
3190
|
+
if (isCurrent()) await rename2(temp, path);
|
|
3191
|
+
else await rm2(temp, { force: true });
|
|
3104
3192
|
});
|
|
3105
3193
|
}
|
|
3106
3194
|
};
|
|
3107
3195
|
|
|
3108
3196
|
// src/exposure.ts
|
|
3109
3197
|
init_config();
|
|
3110
|
-
var
|
|
3198
|
+
var TOOLS_TOOL = "mcp_tools";
|
|
3111
3199
|
function restoredTools(entries) {
|
|
3112
3200
|
const tools = /* @__PURE__ */ new Map();
|
|
3113
3201
|
for (const entry of entries) {
|
|
3114
|
-
if (entry.type !== "message" || entry.message.role !== "toolResult" || entry.message.toolName !==
|
|
3202
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult" || entry.message.toolName !== TOOLS_TOOL || entry.message.isError)
|
|
3115
3203
|
continue;
|
|
3116
3204
|
const details = entry.message.details;
|
|
3117
3205
|
if (!object(details) || details.mcpClient !== 1 || !Array.isArray(details.loaded))
|
|
3118
3206
|
continue;
|
|
3119
|
-
for (const raw of details.loaded.slice(0,
|
|
3207
|
+
for (const raw of details.loaded.slice(0, MAX_SEARCH_LIMIT)) {
|
|
3120
3208
|
if (!object(raw) || typeof raw.server !== "string" || typeof raw.identity !== "string" || typeof raw.name !== "string" || typeof raw.description !== "string" || !object(raw.inputSchema))
|
|
3121
3209
|
continue;
|
|
3122
3210
|
try {
|
|
@@ -3179,40 +3267,59 @@ var Exposure = class {
|
|
|
3179
3267
|
|
|
3180
3268
|
// src/output.ts
|
|
3181
3269
|
init_diagnostics();
|
|
3182
|
-
import { mkdtemp, writeFile as
|
|
3270
|
+
import { mkdtemp, writeFile as writeFile3 } from "node:fs/promises";
|
|
3183
3271
|
import { tmpdir } from "node:os";
|
|
3184
3272
|
import { join as join3 } from "node:path";
|
|
3185
3273
|
import {
|
|
3186
3274
|
truncateHead,
|
|
3187
|
-
withFileMutationQueue as
|
|
3275
|
+
withFileMutationQueue as withFileMutationQueue3
|
|
3188
3276
|
} from "@earendil-works/pi-coding-agent";
|
|
3189
3277
|
function textResult(text, details) {
|
|
3190
3278
|
return { content: [{ type: "text", text }], details };
|
|
3191
3279
|
}
|
|
3192
3280
|
async function convertResult(result, label) {
|
|
3193
3281
|
const texts = [];
|
|
3282
|
+
const blocks = [];
|
|
3283
|
+
let offset = 0;
|
|
3284
|
+
const append = (text2, metadata = {}) => {
|
|
3285
|
+
const start = offset + (texts.length ? 2 : 0);
|
|
3286
|
+
offset = start + text2.length;
|
|
3287
|
+
texts.push(text2);
|
|
3288
|
+
blocks.push({ ...metadata, start, end: offset });
|
|
3289
|
+
};
|
|
3194
3290
|
const images = [];
|
|
3195
3291
|
let imageBytes = 0;
|
|
3196
3292
|
let needsSpill = false;
|
|
3197
3293
|
for (const part of result.content ?? []) {
|
|
3198
|
-
if (part.type === "text")
|
|
3294
|
+
if (part.type === "text") append(part.text);
|
|
3199
3295
|
else if (part.type === "image" && ["image/png", "image/jpeg", "image/gif", "image/webp"].includes(part.mimeType) && imageBytes + part.data.length <= 8 * 1024 * 1024) {
|
|
3200
3296
|
images.push({ type: "image", data: part.data, mimeType: part.mimeType });
|
|
3201
3297
|
imageBytes += part.data.length;
|
|
3202
3298
|
} else if (part.type === "resource" && "text" in part.resource)
|
|
3203
|
-
|
|
3204
|
-
else if (part.type === "resource_link")
|
|
3299
|
+
append(part.resource.text, { mimeType: part.resource.mimeType });
|
|
3300
|
+
else if (part.type === "resource_link")
|
|
3301
|
+
append(`${part.name}: ${part.uri}`, {
|
|
3302
|
+
mimeType: part.mimeType,
|
|
3303
|
+
resourceLink: true
|
|
3304
|
+
});
|
|
3205
3305
|
else {
|
|
3206
|
-
|
|
3306
|
+
append(`[${part.type} content saved in full result file]`, {
|
|
3307
|
+
mimeType: "text/plain"
|
|
3308
|
+
});
|
|
3207
3309
|
needsSpill = true;
|
|
3208
3310
|
}
|
|
3209
3311
|
}
|
|
3210
3312
|
if (result.structuredContent !== void 0)
|
|
3211
|
-
|
|
3313
|
+
append(JSON.stringify(result.structuredContent, null, 2), { structured: true });
|
|
3212
3314
|
const truncated = truncateHead(texts.join("\n\n"));
|
|
3213
3315
|
let text = truncated.content;
|
|
3214
3316
|
const details = {
|
|
3215
3317
|
mcpClient: 1,
|
|
3318
|
+
displayBlocks: blocks.filter((block) => block.start < text.length).map((block) => ({
|
|
3319
|
+
...block,
|
|
3320
|
+
end: Math.min(block.end, text.length),
|
|
3321
|
+
...block.end > text.length ? { truncated: true } : {}
|
|
3322
|
+
})),
|
|
3216
3323
|
failed: result.isError === true,
|
|
3217
3324
|
...result.isError ? {
|
|
3218
3325
|
diagnostics: [
|
|
@@ -3226,9 +3333,9 @@ async function convertResult(result, label) {
|
|
|
3226
3333
|
};
|
|
3227
3334
|
if (truncated.truncated || needsSpill) {
|
|
3228
3335
|
const path = join3(await mkdtemp(join3(tmpdir(), "pi-mcp-client-")), "result.json");
|
|
3229
|
-
await
|
|
3336
|
+
await withFileMutationQueue3(
|
|
3230
3337
|
path,
|
|
3231
|
-
() =>
|
|
3338
|
+
() => writeFile3(path, JSON.stringify(result), { mode: 384 })
|
|
3232
3339
|
);
|
|
3233
3340
|
details.fullOutputPath = path;
|
|
3234
3341
|
text += `
|
|
@@ -3245,7 +3352,102 @@ Full MCP result: ${path}`;
|
|
|
3245
3352
|
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
3246
3353
|
import { Text, truncateToWidth as truncateToWidth2 } from "@earendil-works/pi-tui";
|
|
3247
3354
|
init_config();
|
|
3355
|
+
|
|
3356
|
+
// src/format.ts
|
|
3357
|
+
import {
|
|
3358
|
+
DEFAULT_MAX_BYTES,
|
|
3359
|
+
DEFAULT_MAX_LINES
|
|
3360
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3361
|
+
function formatBlock(text, block, theme) {
|
|
3362
|
+
const fallback = plain(text);
|
|
3363
|
+
if (block.truncated || block.resourceLink || Buffer.byteLength(text) > DEFAULT_MAX_BYTES)
|
|
3364
|
+
return fallback;
|
|
3365
|
+
const mime = block.mimeType?.split(";", 1)[0]?.trim().toLowerCase();
|
|
3366
|
+
const explicitJson = mime === "application/json" || mime === "text/json" || /^application\/[^\s/;]+\+json$/.test(mime ?? "");
|
|
3367
|
+
if (mime && !explicitJson) return fallback;
|
|
3368
|
+
if (!explicitJson && !block.structured && !/^[\s]*[\[{]/.test(text))
|
|
3369
|
+
return fallback;
|
|
3370
|
+
try {
|
|
3371
|
+
JSON.parse(text);
|
|
3372
|
+
} catch {
|
|
3373
|
+
return fallback;
|
|
3374
|
+
}
|
|
3375
|
+
const tokens2 = text.match(
|
|
3376
|
+
/"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}\[\],:]/g
|
|
3377
|
+
) ?? [];
|
|
3378
|
+
const pieces = [];
|
|
3379
|
+
let depth = 0;
|
|
3380
|
+
let bytes = 0;
|
|
3381
|
+
let lines = 1;
|
|
3382
|
+
const add = (value, color) => {
|
|
3383
|
+
bytes += Buffer.byteLength(value);
|
|
3384
|
+
lines += value.split("\n").length - 1;
|
|
3385
|
+
pieces.push({ text: value, color });
|
|
3386
|
+
};
|
|
3387
|
+
const newline = () => add("\n" + " ".repeat(depth));
|
|
3388
|
+
for (let i = 0; i < tokens2.length; i++) {
|
|
3389
|
+
const token = tokens2[i];
|
|
3390
|
+
if (token === "{" || token === "[") {
|
|
3391
|
+
add(token, "syntaxPunctuation");
|
|
3392
|
+
depth++;
|
|
3393
|
+
if (depth > 100) return fallback;
|
|
3394
|
+
if (tokens2[i + 1] !== "}" && tokens2[i + 1] !== "]") newline();
|
|
3395
|
+
} else if (token === "}" || token === "]") {
|
|
3396
|
+
depth--;
|
|
3397
|
+
if (tokens2[i - 1] !== "{" && tokens2[i - 1] !== "[") newline();
|
|
3398
|
+
add(token, "syntaxPunctuation");
|
|
3399
|
+
} else if (token === ",") {
|
|
3400
|
+
add(token, "syntaxPunctuation");
|
|
3401
|
+
newline();
|
|
3402
|
+
} else if (token === ":") {
|
|
3403
|
+
add(":", "syntaxPunctuation");
|
|
3404
|
+
add(" ");
|
|
3405
|
+
} else {
|
|
3406
|
+
add(
|
|
3407
|
+
token,
|
|
3408
|
+
token.startsWith('"') ? tokens2[i + 1] === ":" ? "syntaxVariable" : "syntaxString" : /^(true|false|null)$/.test(token) ? "syntaxKeyword" : "syntaxNumber"
|
|
3409
|
+
);
|
|
3410
|
+
}
|
|
3411
|
+
if (bytes > DEFAULT_MAX_BYTES || lines > DEFAULT_MAX_LINES) return fallback;
|
|
3412
|
+
}
|
|
3413
|
+
return pieces.map(
|
|
3414
|
+
({ text: value, color }) => color ? theme.fg(color, plain(value)) : value
|
|
3415
|
+
).join("");
|
|
3416
|
+
}
|
|
3417
|
+
function formatOutput(text, blocks, theme) {
|
|
3418
|
+
if (!blocks) return formatBlock(text, {}, theme);
|
|
3419
|
+
let cursor = 0;
|
|
3420
|
+
let extraBytes = 0;
|
|
3421
|
+
let extraLines = 0;
|
|
3422
|
+
const output = [];
|
|
3423
|
+
for (const block of blocks) {
|
|
3424
|
+
if (!Number.isInteger(block.start) || !Number.isInteger(block.end) || block.start < cursor || block.end < block.start || block.end > text.length)
|
|
3425
|
+
return plain(text);
|
|
3426
|
+
output.push(plain(text.slice(cursor, block.start)));
|
|
3427
|
+
const source = text.slice(block.start, block.end);
|
|
3428
|
+
const formatted = formatBlock(source, block, theme);
|
|
3429
|
+
const visible = plain(formatted);
|
|
3430
|
+
extraBytes += Math.max(
|
|
3431
|
+
0,
|
|
3432
|
+
Buffer.byteLength(visible) - Buffer.byteLength(source)
|
|
3433
|
+
);
|
|
3434
|
+
extraLines += Math.max(
|
|
3435
|
+
0,
|
|
3436
|
+
visible.split("\n").length - source.split("\n").length
|
|
3437
|
+
);
|
|
3438
|
+
if (extraBytes > DEFAULT_MAX_BYTES || extraLines > DEFAULT_MAX_LINES)
|
|
3439
|
+
return plain(text);
|
|
3440
|
+
output.push(formatted);
|
|
3441
|
+
cursor = block.end;
|
|
3442
|
+
}
|
|
3443
|
+
output.push(plain(text.slice(cursor)));
|
|
3444
|
+
return output.join("");
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
// src/render.ts
|
|
3248
3448
|
var states = {
|
|
3449
|
+
candidate: { glyph: "\u25CB", color: "dim" },
|
|
3450
|
+
active: { glyph: "\u25CF", color: "dim" },
|
|
3249
3451
|
queued: { glyph: "\u25CF", color: "dim" },
|
|
3250
3452
|
running: { glyph: "\u25B6\uFE0E", color: "muted" },
|
|
3251
3453
|
done: { glyph: "\u2714\uFE0E", color: "success" },
|
|
@@ -3254,7 +3456,7 @@ var states = {
|
|
|
3254
3456
|
};
|
|
3255
3457
|
function renderCall(title, args, theme, expanded) {
|
|
3256
3458
|
const values = object(args) ? args : {};
|
|
3257
|
-
const preview = Object.entries(values).map(([key, value]) => `${line(key)}=${line(JSON.stringify(value) ?? "")}`).join(" ");
|
|
3459
|
+
const preview = title === "mcp activate" ? "" : Object.entries(values).map(([key, value]) => `${line(key)}=${line(JSON.stringify(value) ?? "")}`).join(" ");
|
|
3258
3460
|
return {
|
|
3259
3461
|
render(width) {
|
|
3260
3462
|
if (width <= 0) return [];
|
|
@@ -3273,23 +3475,31 @@ function renderCall(title, args, theme, expanded) {
|
|
|
3273
3475
|
}
|
|
3274
3476
|
};
|
|
3275
3477
|
}
|
|
3478
|
+
function formatValidation(text, theme) {
|
|
3479
|
+
const marker = /\r?\n\r?\nReceived arguments:\r?\n/.exec(text);
|
|
3480
|
+
if (!marker) return plain(text);
|
|
3481
|
+
const start = marker.index + marker[0].length;
|
|
3482
|
+
return plain(text.slice(0, start)) + formatBlock(text.slice(start), {}, theme);
|
|
3483
|
+
}
|
|
3276
3484
|
function renderResult(result, options, theme, isError) {
|
|
3277
3485
|
const details = object(result.details) && result.details.mcpClient === 1 ? result.details : void 0;
|
|
3278
|
-
const text =
|
|
3279
|
-
|
|
3280
|
-
|
|
3486
|
+
const text = result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n");
|
|
3487
|
+
const validation = !details && isError ? /^Validation failed for tool "[^"\r\n]+":\r?\n([\s\S]*)$/.exec(text) : null;
|
|
3488
|
+
const body = validation?.[1] ?? text;
|
|
3281
3489
|
const rows = details?.rows ?? [
|
|
3282
3490
|
{
|
|
3283
|
-
label: line(text) || "Working\u2026",
|
|
3491
|
+
label: validation ? "Invalid tool arguments" : isError ? "Tool failed" : line(text) || "Working\u2026",
|
|
3284
3492
|
state: isError ? "failed" : options.isPartial ? "running" : "done"
|
|
3285
3493
|
}
|
|
3286
3494
|
];
|
|
3495
|
+
const bodyInRow = details?.rows.length === 1 && details.rows[0].state === "failed" && details.rows[0].label === text && !/[\r\n]/.test(text);
|
|
3496
|
+
let formattedOutput;
|
|
3287
3497
|
return {
|
|
3288
3498
|
render(width) {
|
|
3289
3499
|
if (width <= 0) return [];
|
|
3290
3500
|
const lines = rows.flatMap((row) => {
|
|
3291
3501
|
const status = states[row.state] ?? states.failed;
|
|
3292
|
-
const value = theme.fg(status.color, status.glyph) + " " + theme.fg("accent", line(row.label));
|
|
3502
|
+
const value = theme.fg(status.color, status.glyph) + " " + theme.fg("accent", line(row.label)) + (row.inlineDescription ? theme.fg("dim", ` ${line(row.inlineDescription)}`) : "");
|
|
3293
3503
|
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
3504
|
if (options.expanded && row.description)
|
|
3295
3505
|
rendered.push(truncateToWidth2(
|
|
@@ -3300,9 +3510,13 @@ function renderResult(result, options, theme, isError) {
|
|
|
3300
3510
|
});
|
|
3301
3511
|
for (const note of details?.searchNotes ?? [])
|
|
3302
3512
|
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
|
-
if (options.expanded && !options.isPartial && text && !details?.searchNotes)
|
|
3513
|
+
if (options.expanded && !options.isPartial && text && !details?.searchNotes && !bodyInRow)
|
|
3304
3514
|
lines.push(
|
|
3305
|
-
...new Text(
|
|
3515
|
+
...new Text(
|
|
3516
|
+
formattedOutput ??= validation ? formatValidation(body, theme) : formatOutput(body, details?.displayBlocks, theme),
|
|
3517
|
+
0,
|
|
3518
|
+
0
|
|
3519
|
+
).render(width).map((x) => truncateToWidth2(x, width))
|
|
3306
3520
|
);
|
|
3307
3521
|
if (!options.expanded && details?.fullOutputPath)
|
|
3308
3522
|
lines.push(
|
|
@@ -3314,6 +3528,7 @@ function renderResult(result, options, theme, isError) {
|
|
|
3314
3528
|
return lines;
|
|
3315
3529
|
},
|
|
3316
3530
|
invalidate() {
|
|
3531
|
+
formattedOutput = void 0;
|
|
3317
3532
|
}
|
|
3318
3533
|
};
|
|
3319
3534
|
}
|
|
@@ -3330,7 +3545,10 @@ function errorResult(error, context) {
|
|
|
3330
3545
|
failed: true,
|
|
3331
3546
|
diagnostics: [value],
|
|
3332
3547
|
rows: [
|
|
3333
|
-
{
|
|
3548
|
+
{
|
|
3549
|
+
label: value.code === "cancelled" ? "Cancelled" : message,
|
|
3550
|
+
state: value.code === "cancelled" ? "cancelled" : "failed"
|
|
3551
|
+
}
|
|
3334
3552
|
]
|
|
3335
3553
|
});
|
|
3336
3554
|
}
|
|
@@ -3400,16 +3618,27 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3400
3618
|
});
|
|
3401
3619
|
exposure.restore(tools);
|
|
3402
3620
|
};
|
|
3403
|
-
async function reloadConfiguration(ctx) {
|
|
3621
|
+
async function reloadConfiguration(ctx, toggle) {
|
|
3404
3622
|
const generation = sessionGeneration;
|
|
3623
|
+
const validate = (nextConfig2) => {
|
|
3624
|
+
ctx.signal?.throwIfAborted();
|
|
3625
|
+
if (generation !== sessionGeneration)
|
|
3626
|
+
throw new CommandUsageError("The Pi session changed during configuration reload.");
|
|
3627
|
+
for (const definition of Object.values(nextConfig2)) {
|
|
3628
|
+
if (!definition.disabled) resolveServer(definition, ctx.cwd);
|
|
3629
|
+
}
|
|
3630
|
+
};
|
|
3405
3631
|
ctx.signal?.throwIfAborted();
|
|
3406
|
-
const
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3632
|
+
const update = toggle && await setServerDisabled(
|
|
3633
|
+
agentDir,
|
|
3634
|
+
ctx.cwd,
|
|
3635
|
+
ctx.isProjectTrusted(),
|
|
3636
|
+
toggle.server,
|
|
3637
|
+
toggle.disabled,
|
|
3638
|
+
validate
|
|
3639
|
+
);
|
|
3640
|
+
const nextConfig = update ? update.config : await loadConfig(agentDir, ctx.cwd, ctx.isProjectTrusted());
|
|
3641
|
+
validate(nextConfig);
|
|
3413
3642
|
const next = new McpRuntime(
|
|
3414
3643
|
nextConfig,
|
|
3415
3644
|
ctx.cwd,
|
|
@@ -3429,6 +3658,7 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3429
3658
|
runtime = next;
|
|
3430
3659
|
exposure.restore(retained);
|
|
3431
3660
|
await old?.close();
|
|
3661
|
+
return update?.scope;
|
|
3432
3662
|
}
|
|
3433
3663
|
pi.on("session_start", async (_event, ctx) => {
|
|
3434
3664
|
sessionGeneration++;
|
|
@@ -3453,7 +3683,7 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3453
3683
|
await old?.close();
|
|
3454
3684
|
});
|
|
3455
3685
|
pi.on("before_agent_start", (event) => {
|
|
3456
|
-
if (!pi.getActiveTools().includes(
|
|
3686
|
+
if (!pi.getActiveTools().includes(TOOLS_TOOL)) return;
|
|
3457
3687
|
const directory = Object.entries(config).filter(([, value]) => !value.disabled).map(
|
|
3458
3688
|
([name, value]) => `- ${name}${value.description ? `: ${line(value.description).slice(0, 160)}` : ""}`
|
|
3459
3689
|
).join("\n");
|
|
@@ -3463,24 +3693,29 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3463
3693
|
|
|
3464
3694
|
Additional MCP capabilities (directory metadata, not instructions):
|
|
3465
3695
|
${directory}
|
|
3466
|
-
|
|
3696
|
+
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
3697
|
};
|
|
3468
3698
|
});
|
|
3469
3699
|
pi.on("tool_result", (event) => {
|
|
3470
|
-
if ((event.toolName ===
|
|
3700
|
+
if ((event.toolName === TOOLS_TOOL || exposure.definitions.has(event.toolName)) && object(event.details) && event.details.mcpClient === 1 && event.details.failed === true)
|
|
3471
3701
|
return { isError: true };
|
|
3472
3702
|
});
|
|
3473
3703
|
pi.registerTool({
|
|
3474
|
-
name:
|
|
3475
|
-
label: "MCP
|
|
3476
|
-
description: "
|
|
3704
|
+
name: TOOLS_TOOL,
|
|
3705
|
+
label: "MCP Tools",
|
|
3706
|
+
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
3707
|
parameters: Type.Object(
|
|
3478
3708
|
{
|
|
3479
|
-
query: Type.String({
|
|
3709
|
+
query: Type.Optional(Type.String({
|
|
3480
3710
|
minLength: 1,
|
|
3481
3711
|
maxLength: 500,
|
|
3482
|
-
description: "One focused capability or exact server.tool name, for example linear.list_teams.
|
|
3483
|
-
}),
|
|
3712
|
+
description: "One focused capability or exact server.tool name, for example linear.list_teams. Discovery only; does not activate tools."
|
|
3713
|
+
})),
|
|
3714
|
+
activate: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 600 }), {
|
|
3715
|
+
minItems: 1,
|
|
3716
|
+
maxItems: MAX_SEARCH_LIMIT,
|
|
3717
|
+
description: "Exact server.tool or mcp__server__tool identifiers to activate, without invoking. Duplicates are ignored. Cannot be combined with query, server, or limit."
|
|
3718
|
+
})),
|
|
3484
3719
|
server: Type.Optional(
|
|
3485
3720
|
Type.String({
|
|
3486
3721
|
minLength: 1,
|
|
@@ -3493,78 +3728,85 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3493
3728
|
minimum: 1,
|
|
3494
3729
|
maximum: MAX_SEARCH_LIMIT,
|
|
3495
3730
|
default: DEFAULT_SEARCH_LIMIT,
|
|
3496
|
-
description: `Maximum number of
|
|
3731
|
+
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
3732
|
})
|
|
3498
3733
|
)
|
|
3499
3734
|
},
|
|
3500
3735
|
{ additionalProperties: false }
|
|
3501
3736
|
),
|
|
3502
|
-
renderCall: (args, theme, context) => renderCall("mcp
|
|
3737
|
+
renderCall: (args, theme, context) => renderCall(args.activate ? "mcp activate" : "mcp discover", args, theme, context.expanded),
|
|
3503
3738
|
renderResult: (result, options2, theme, context) => renderResult(result, options2, theme, context.isError),
|
|
3504
3739
|
async execute(_id, args, signal, onUpdate, ctx) {
|
|
3740
|
+
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.';
|
|
3741
|
+
const hasQuery = args.query !== void 0;
|
|
3742
|
+
const hasActivate = args.activate !== void 0;
|
|
3743
|
+
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
3744
|
try {
|
|
3506
3745
|
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}__`)
|
|
3746
|
+
const identifiers = [...new Set(args.activate ?? [])];
|
|
3747
|
+
const serversFor = (identifier) => Object.keys(config).filter(
|
|
3748
|
+
(name) => identifier.startsWith(`${name}.`) || identifier.startsWith(`mcp__${name}__`) || // Long server names can have a truncated, hashed native name.
|
|
3749
|
+
`mcp__${name}__`.length > 50 && identifier.startsWith(`mcp__${name}__`.slice(0, 50))
|
|
3515
3750
|
);
|
|
3751
|
+
onUpdate?.(textResult(hasActivate ? "Activating MCP tools\u2026" : "Searching MCP catalog\u2026", {
|
|
3752
|
+
mcpClient: 1,
|
|
3753
|
+
rows: [{ label: args.query ?? identifiers.join(", "), state: "running" }]
|
|
3754
|
+
}));
|
|
3516
3755
|
const discovery = await activeRuntime.discover(
|
|
3517
|
-
args.server ??
|
|
3756
|
+
hasActivate ? identifiers.flatMap(serversFor) : args.server ?? serversFor(args.query)[0],
|
|
3518
3757
|
signal ?? ctx.signal
|
|
3519
3758
|
);
|
|
3520
3759
|
(signal ?? ctx.signal)?.throwIfAborted();
|
|
3521
3760
|
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
|
-
);
|
|
3761
|
+
throw new Error("MCP session changed during search or activation.");
|
|
3544
3762
|
const details = {
|
|
3545
3763
|
mcpClient: 1,
|
|
3546
|
-
loaded,
|
|
3547
3764
|
searchNotes: discovery.warnings,
|
|
3548
3765
|
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
|
-
]
|
|
3766
|
+
rows: []
|
|
3565
3767
|
};
|
|
3566
|
-
|
|
3567
|
-
|
|
3768
|
+
const messages2 = [];
|
|
3769
|
+
if (!hasActivate) {
|
|
3770
|
+
const candidates = searchTools(discovery.tools, args.query, args.server, args.limit);
|
|
3771
|
+
details.candidates = candidates;
|
|
3772
|
+
details.failed = !candidates.length && discovery.diagnostics.length > 0;
|
|
3773
|
+
const active = new Set(pi.getActiveTools());
|
|
3774
|
+
for (const tool of candidates) {
|
|
3775
|
+
const label = summarize(tool) + (active.has(tool.nativeName) ? " [loaded]" : "");
|
|
3776
|
+
messages2.push(label);
|
|
3777
|
+
details.rows.push({
|
|
3778
|
+
label: `${tool.server}.${tool.name}`,
|
|
3779
|
+
inlineDescription: summarize(tool, false),
|
|
3780
|
+
state: active.has(tool.nativeName) ? "active" : "candidate"
|
|
3781
|
+
});
|
|
3782
|
+
}
|
|
3783
|
+
if (!candidates.length) messages2.push("No matching tools. Try a more specific capability, server, or exact tool name.");
|
|
3784
|
+
details.rows.push(...discovery.unavailable.map((label) => ({ label, state: "failed" })));
|
|
3785
|
+
messages2.push(...discovery.unavailable.map((message) => `Not searched: ${message}`), ...discovery.warnings);
|
|
3786
|
+
messages2.push("No tools activated. Call mcp_tools({activate: [...]}) with the identifiers you need.");
|
|
3787
|
+
} else {
|
|
3788
|
+
const resolved = resolveTools(discovery.tools, identifiers);
|
|
3789
|
+
const matches2 = [...new Map(resolved.flatMap(({ tool }) => tool ? [[tool.nativeName, tool]] : [])).values()];
|
|
3790
|
+
const collisions = new Set(pi.getAllTools().filter((tool) => !exposure.definitions.has(tool.name)).map((tool) => tool.name));
|
|
3791
|
+
const { loaded, added } = exposure.load(matches2);
|
|
3792
|
+
details.loaded = loaded;
|
|
3793
|
+
details.failed = loaded.length === 0;
|
|
3794
|
+
for (const { identifier, tool, suggestions } of resolved) {
|
|
3795
|
+
const ok = tool && loaded.includes(tool);
|
|
3796
|
+
const unavailable = discovery.diagnostics.find((value) => value.server && serversFor(identifier).includes(value.server));
|
|
3797
|
+
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."}`;
|
|
3798
|
+
const label = `${line(identifier)} \u2014 ${ok ? added.includes(tool.nativeName) ? "loaded" : "already loaded" : `not loaded \u2014 ${reason}`}`;
|
|
3799
|
+
messages2.push(label);
|
|
3800
|
+
details.rows.push({
|
|
3801
|
+
label: line(identifier),
|
|
3802
|
+
...ok ? {} : { inlineDescription: reason },
|
|
3803
|
+
state: ok ? "done" : "failed"
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
if (loaded.length) messages2.push("Call the loaded tools directly. Their full schemas are now available.");
|
|
3807
|
+
messages2.push(...discovery.warnings);
|
|
3808
|
+
}
|
|
3809
|
+
if (!details.rows.length) details.rows.push({ label: "No matching tools", state: "candidate" });
|
|
3568
3810
|
return textResult(messages2.join("\n"), details);
|
|
3569
3811
|
} catch (error) {
|
|
3570
3812
|
return errorResult(error, {
|
|
@@ -3577,9 +3819,9 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3577
3819
|
}
|
|
3578
3820
|
});
|
|
3579
3821
|
pi.registerCommand("mcp", {
|
|
3580
|
-
description: "Manage MCP servers: list, status, reload, inspect|tools|auth|reconnect|refresh <server>",
|
|
3822
|
+
description: "Manage MCP servers: list, status, reload, enable|disable|inspect|tools|auth|reconnect|refresh <server>",
|
|
3581
3823
|
getArgumentCompletions(prefix) {
|
|
3582
|
-
const serverActions = ["inspect", "tools", "auth", "reconnect", "refresh"];
|
|
3824
|
+
const serverActions = ["enable", "disable", "inspect", "tools", "auth", "reconnect", "refresh"];
|
|
3583
3825
|
const input = prefix.trimStart();
|
|
3584
3826
|
const match = /^(\S+)\s+(.*)$/s.exec(input);
|
|
3585
3827
|
if (!match) {
|
|
@@ -3588,13 +3830,16 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3588
3830
|
const [, action, partialServer] = match;
|
|
3589
3831
|
if (!serverActions.includes(action) || /\s/.test(partialServer)) return [];
|
|
3590
3832
|
return Object.keys(config).filter(
|
|
3591
|
-
(name) => name.startsWith(partialServer) && (action === "inspect" || !config[name].disabled)
|
|
3833
|
+
(name) => name.startsWith(partialServer) && (action === "inspect" || (action === "enable" ? config[name].disabled : !config[name].disabled))
|
|
3592
3834
|
).sort().map((name) => ({ value: `${action} ${name}`, label: name }));
|
|
3593
3835
|
},
|
|
3594
3836
|
async handler(args, ctx) {
|
|
3837
|
+
const generation = sessionGeneration;
|
|
3595
3838
|
await ctx.waitForIdle();
|
|
3596
3839
|
const [action = "status", server, ...extra] = args.trim().split(/\s+/).filter(Boolean);
|
|
3597
3840
|
try {
|
|
3841
|
+
if (generation !== sessionGeneration)
|
|
3842
|
+
throw new CommandUsageError("The Pi session changed while waiting for idle.");
|
|
3598
3843
|
if (action === "reload" && !server) {
|
|
3599
3844
|
await reloadConfiguration(ctx);
|
|
3600
3845
|
if (ctx.hasUI)
|
|
@@ -3604,6 +3849,15 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3604
3849
|
);
|
|
3605
3850
|
return;
|
|
3606
3851
|
}
|
|
3852
|
+
if ((action === "enable" || action === "disable") && server && !extra.length && Object.hasOwn(config, server)) {
|
|
3853
|
+
const scope = await reloadConfiguration(ctx, { server, disabled: action === "disable" });
|
|
3854
|
+
if (ctx.hasUI)
|
|
3855
|
+
ctx.ui.notify(
|
|
3856
|
+
`\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."),
|
|
3857
|
+
"info"
|
|
3858
|
+
);
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3607
3861
|
if (action === "inspect" && server && !extra.length && Object.hasOwn(config, server)) {
|
|
3608
3862
|
if (ctx.hasUI)
|
|
3609
3863
|
ctx.ui.notify(
|
|
@@ -3624,7 +3878,7 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3624
3878
|
}
|
|
3625
3879
|
if (!server || extra.length || !Object.hasOwn(config, server) || config[server].disabled)
|
|
3626
3880
|
throw new CommandUsageError(
|
|
3627
|
-
"Usage: /mcp list|status|reload or /mcp inspect|tools|auth|reconnect|refresh <server>.
|
|
3881
|
+
"Usage: /mcp list|status|reload or /mcp enable|disable|inspect|tools|auth|reconnect|refresh <server>. Disabled servers accept enable, disable, and inspect."
|
|
3628
3882
|
);
|
|
3629
3883
|
if (action === "tools") {
|
|
3630
3884
|
if (!ctx.hasUI)
|
|
@@ -3699,7 +3953,7 @@ ${target}`, "info");
|
|
|
3699
3953
|
else if (action === "refresh") await current().catalog(server, ctx.signal, true);
|
|
3700
3954
|
else
|
|
3701
3955
|
throw new CommandUsageError(
|
|
3702
|
-
"Unknown MCP command. Use /mcp list|status|reload or /mcp inspect|tools|auth|reconnect|refresh <server>."
|
|
3956
|
+
"Unknown MCP command. Use /mcp list|status|reload or /mcp enable|disable|inspect|tools|auth|reconnect|refresh <server>."
|
|
3703
3957
|
);
|
|
3704
3958
|
if (ctx.hasUI)
|
|
3705
3959
|
ctx.ui.notify(
|
|
@@ -3710,7 +3964,7 @@ ${target}`, "info");
|
|
|
3710
3964
|
const message = error instanceof CommandUsageError ? error.message : formatDiagnostic(
|
|
3711
3965
|
diagnose(error, {
|
|
3712
3966
|
server,
|
|
3713
|
-
operation:
|
|
3967
|
+
operation: ["reload", "inspect", "enable", "disable"].includes(action) ? "configuration" : action === "tools" ? "search" : action === "auth" ? "auth" : action === "refresh" ? "refresh" : "reconnect",
|
|
3714
3968
|
oauth: config[server]?.oauth,
|
|
3715
3969
|
signal: ctx.signal
|
|
3716
3970
|
})
|