pi-mcp-client 0.1.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 +117 -23
- package/dist/index.js +736 -214
- package/package.json +1 -1
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",
|
|
@@ -2354,10 +2431,120 @@ function searchTools(tools, query, server, limit = DEFAULT_SEARCH_LIMIT) {
|
|
|
2354
2431
|
return index.search(query).sort((a, b) => b.score - a.score || String(a.id).localeCompare(String(b.id))).slice(0, Math.max(1, Math.min(limit, MAX_SEARCH_LIMIT))).map((result) => byName.get(result.id));
|
|
2355
2432
|
}
|
|
2356
2433
|
|
|
2434
|
+
// src/management.ts
|
|
2435
|
+
init_config();
|
|
2436
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2437
|
+
var serverStates = {
|
|
2438
|
+
disconnected: { glyph: "\u25CB", label: "idle" },
|
|
2439
|
+
connected: { glyph: "\u25CF", label: "connected" },
|
|
2440
|
+
connecting: { glyph: "\u25B6\uFE0E", label: "connecting" },
|
|
2441
|
+
failed: { glyph: "\u2718\uFE0E", label: "error" },
|
|
2442
|
+
disabled: { glyph: "\u25CB", label: "disabled" }
|
|
2443
|
+
};
|
|
2444
|
+
function serverMatrix(servers, loaded, width = 80) {
|
|
2445
|
+
if (width <= 0) return "";
|
|
2446
|
+
const fit = (text) => plain(truncateToWidth(text, width));
|
|
2447
|
+
if (!servers.length) return fit("No MCP servers configured.");
|
|
2448
|
+
const nameWidth = Math.min(40, Math.max(6, ...servers.map(({ name }) => name.length)));
|
|
2449
|
+
const heading = ` ${"Server".padEnd(nameWidth)} ${"State".padEnd(10)} ${"Tools".padStart(5)} ${"Loaded".padStart(6)}`;
|
|
2450
|
+
const rows = servers.map((server) => {
|
|
2451
|
+
const state = serverStates[server.state];
|
|
2452
|
+
const name = plain(truncateToWidth(line(server.name), nameWidth)).padEnd(nameWidth);
|
|
2453
|
+
return `${state.glyph} ${name} ${state.label.padEnd(10)} ${String(server.catalogSize ?? "\u2014").padStart(5)} ${String(loaded.get(server.name) ?? 0).padStart(6)}`;
|
|
2454
|
+
});
|
|
2455
|
+
const errors = servers.filter((server) => server.state === "failed" && server.error).map((server) => `\u2718\uFE0E ${line(server.name)}: [${server.error.code}] ${line(server.error.message)}`);
|
|
2456
|
+
return [
|
|
2457
|
+
heading,
|
|
2458
|
+
...rows,
|
|
2459
|
+
"",
|
|
2460
|
+
"Connections open on demand. \u2014 = catalog not fetched.",
|
|
2461
|
+
...errors
|
|
2462
|
+
].map(fit).join("\n");
|
|
2463
|
+
}
|
|
2464
|
+
function toolPickerLabel(tool, index, columns = 80) {
|
|
2465
|
+
const width = Math.max(0, columns - 4);
|
|
2466
|
+
const text = `${index + 1}. ${line(tool.name)}: ${line(tool.description) || "No description."}`;
|
|
2467
|
+
return plain(truncateToWidth(text, width, "\u2026"));
|
|
2468
|
+
}
|
|
2469
|
+
function schemaType(schema, depth = 0) {
|
|
2470
|
+
if (!object(schema) || depth > 2) return "unknown";
|
|
2471
|
+
const alternatives = schema.anyOf ?? schema.oneOf;
|
|
2472
|
+
if (Array.isArray(alternatives)) {
|
|
2473
|
+
if (alternatives.length > 4) return "union";
|
|
2474
|
+
return [...new Set(alternatives.map((part) => schemaType(part, depth + 1)))].join(" | ") || "unknown";
|
|
2475
|
+
}
|
|
2476
|
+
if (schema.$ref || schema.allOf) return "unknown";
|
|
2477
|
+
const type = schema.type;
|
|
2478
|
+
if (Array.isArray(type))
|
|
2479
|
+
return type.slice(0, 4).map((part) => schemaType({ ...schema, type: part }, depth + 1)).join(" | ");
|
|
2480
|
+
if (type === "array") return `Array<${schemaType(schema.items, depth + 1)}>`;
|
|
2481
|
+
if (type === "integer") return "integer";
|
|
2482
|
+
if (["string", "number", "boolean", "object", "null"].includes(String(type))) return String(type);
|
|
2483
|
+
return "unknown";
|
|
2484
|
+
}
|
|
2485
|
+
function toolParameters(tool) {
|
|
2486
|
+
const schema = tool.inputSchema;
|
|
2487
|
+
if (!object(schema)) return [];
|
|
2488
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
2489
|
+
return Object.entries(object(schema.properties) ? schema.properties : {}).map(([name, value]) => ({
|
|
2490
|
+
name: line(name).slice(0, 80),
|
|
2491
|
+
required: required.has(name),
|
|
2492
|
+
type: schemaType(value),
|
|
2493
|
+
description: object(value) && typeof value.description === "string" ? line(value.description) : ""
|
|
2494
|
+
}));
|
|
2495
|
+
}
|
|
2496
|
+
function toolSignature(tool, limit = 40) {
|
|
2497
|
+
const parameters = toolParameters(tool);
|
|
2498
|
+
const args = parameters.slice(0, limit).map(
|
|
2499
|
+
(parameter) => `${parameter.name}${parameter.required ? "" : "?"}: ${parameter.type}`
|
|
2500
|
+
);
|
|
2501
|
+
if (parameters.length > limit) args.push(`\u2026 +${parameters.length - limit} more`);
|
|
2502
|
+
const schema = tool.inputSchema;
|
|
2503
|
+
if (object(schema) && schema.additionalProperties !== false) args.push("\u2026");
|
|
2504
|
+
const name = line(tool.name).slice(0, 160);
|
|
2505
|
+
return args.length ? `${name}(
|
|
2506
|
+
${args.map((arg) => ` ${arg},`).join("\n")}
|
|
2507
|
+
)` : `${name}()`;
|
|
2508
|
+
}
|
|
2509
|
+
function inspectTool(tool) {
|
|
2510
|
+
const parameters = toolParameters(tool);
|
|
2511
|
+
return [
|
|
2512
|
+
toolSignature(tool),
|
|
2513
|
+
line(tool.description) || "No description.",
|
|
2514
|
+
...parameters.length ? ["Parameters:"] : [],
|
|
2515
|
+
...parameters.slice(0, 40).map(
|
|
2516
|
+
(parameter) => `${parameter.name}: ${parameter.type} (${parameter.required ? "required" : "optional"})${parameter.description ? `
|
|
2517
|
+
${parameter.description}` : ""}`
|
|
2518
|
+
),
|
|
2519
|
+
...parameters.length > 40 ? [`\u2026 ${parameters.length - 40} more parameters`] : [],
|
|
2520
|
+
"Types are summaries; the full schema may impose additional constraints."
|
|
2521
|
+
].join("\n\n");
|
|
2522
|
+
}
|
|
2523
|
+
function inspectServer(name, config, status) {
|
|
2524
|
+
return [
|
|
2525
|
+
status,
|
|
2526
|
+
`Server: ${name}`,
|
|
2527
|
+
`Transport: ${config.command ? "stdio" : "HTTP"}`,
|
|
2528
|
+
`Protocol: ${config.protocol ?? "auto"}`,
|
|
2529
|
+
`OAuth: ${config.oauth ? "enabled" : "disabled"}`,
|
|
2530
|
+
`Timeout: ${config.timeoutMs ? `${config.timeoutMs} ms` : "default"}`,
|
|
2531
|
+
...config.command ? [
|
|
2532
|
+
"Command and working directory: hidden",
|
|
2533
|
+
`Arguments: ${config.args?.length ?? 0} (values hidden)`,
|
|
2534
|
+
`Environment overrides: ${Object.keys(config.env ?? {}).length} (names and values hidden)`
|
|
2535
|
+
] : [
|
|
2536
|
+
"URL: hidden",
|
|
2537
|
+
`Headers: ${Object.keys(config.headers ?? {}).length} (names and values hidden)`
|
|
2538
|
+
],
|
|
2539
|
+
`Include tools: ${config.includeTools ? config.includeTools.map(line).join(", ") : "all"}`,
|
|
2540
|
+
`Exclude tools: ${config.excludeTools?.map(line).join(", ") || "none"}`
|
|
2541
|
+
].join("\n");
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2357
2544
|
// src/auth.ts
|
|
2358
2545
|
init_config();
|
|
2359
2546
|
init_diagnostics();
|
|
2360
|
-
import { randomUUID } from "node:crypto";
|
|
2547
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2361
2548
|
import { createServer } from "node:http";
|
|
2362
2549
|
import {
|
|
2363
2550
|
auth
|
|
@@ -2407,7 +2594,7 @@ var OAuthProvider = class {
|
|
|
2407
2594
|
data;
|
|
2408
2595
|
verifier;
|
|
2409
2596
|
discovery;
|
|
2410
|
-
expectedState =
|
|
2597
|
+
expectedState = randomUUID2();
|
|
2411
2598
|
save() {
|
|
2412
2599
|
this.store.write(JSON.stringify(this.data));
|
|
2413
2600
|
}
|
|
@@ -2531,9 +2718,9 @@ async function authenticate(url, open, signal, store) {
|
|
|
2531
2718
|
|
|
2532
2719
|
// src/runtime.ts
|
|
2533
2720
|
init_config();
|
|
2534
|
-
import { mkdir, readFile as readFile2, rename, 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";
|
|
2535
2722
|
import { join as join2 } from "node:path";
|
|
2536
|
-
import { randomUUID as
|
|
2723
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2537
2724
|
import {
|
|
2538
2725
|
Client,
|
|
2539
2726
|
StreamableHTTPClientTransport
|
|
@@ -2542,7 +2729,7 @@ import {
|
|
|
2542
2729
|
StdioClientTransport,
|
|
2543
2730
|
getDefaultEnvironment
|
|
2544
2731
|
} from "@modelcontextprotocol/client/stdio";
|
|
2545
|
-
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
2732
|
+
import { withFileMutationQueue as withFileMutationQueue2 } from "@earendil-works/pi-coding-agent";
|
|
2546
2733
|
init_secrets();
|
|
2547
2734
|
init_diagnostics();
|
|
2548
2735
|
var ToolContractError = class extends Error {
|
|
@@ -2565,11 +2752,18 @@ function waitFor(promise, signal) {
|
|
|
2565
2752
|
if (signal.aborted) abort();
|
|
2566
2753
|
});
|
|
2567
2754
|
}
|
|
2568
|
-
var connectSdk = async (_name, config, signal) => {
|
|
2755
|
+
var connectSdk = async (_name, config, signal, onToolsChanged) => {
|
|
2569
2756
|
const timeout = config.timeoutMs ?? 15e3;
|
|
2570
2757
|
const client = new Client(
|
|
2571
2758
|
{ name: "pi-mcp-client", version: "0.1.0" },
|
|
2572
2759
|
{
|
|
2760
|
+
listChanged: {
|
|
2761
|
+
tools: {
|
|
2762
|
+
autoRefresh: false,
|
|
2763
|
+
debounceMs: 0,
|
|
2764
|
+
onChanged: () => onToolsChanged?.()
|
|
2765
|
+
}
|
|
2766
|
+
},
|
|
2573
2767
|
versionNegotiation: {
|
|
2574
2768
|
mode: config.protocol ?? "auto",
|
|
2575
2769
|
probe: { timeoutMs: timeout }
|
|
@@ -2585,21 +2779,39 @@ var connectSdk = async (_name, config, signal) => {
|
|
|
2585
2779
|
}) : new StreamableHTTPClientTransport(new URL(config.url), {
|
|
2586
2780
|
requestInit: { headers: config.headers },
|
|
2587
2781
|
authProvider: config.oauth ? new OAuthProvider(config.url, await credentialStore(config.url)) : void 0,
|
|
2588
|
-
// Bound
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2782
|
+
// Bound HTTP responses (including OAuth), but not established SSE streams.
|
|
2783
|
+
// The SDK bounds ordinary MCP requests with their request timeout.
|
|
2784
|
+
fetch: async (input, init) => {
|
|
2785
|
+
const deadline = new AbortController();
|
|
2786
|
+
const timer = setTimeout(
|
|
2787
|
+
() => deadline.abort(new Error("HTTP response timed out.")),
|
|
2788
|
+
timeout
|
|
2789
|
+
);
|
|
2790
|
+
timer.unref();
|
|
2791
|
+
try {
|
|
2792
|
+
const response = await fetch(input, {
|
|
2793
|
+
...init,
|
|
2794
|
+
signal: AbortSignal.any([
|
|
2795
|
+
signal,
|
|
2796
|
+
deadline.signal,
|
|
2797
|
+
...init?.signal ? [init.signal] : []
|
|
2798
|
+
])
|
|
2799
|
+
});
|
|
2800
|
+
if (response.headers.get("content-type")?.split(";")[0].trim() === "text/event-stream")
|
|
2801
|
+
clearTimeout(timer);
|
|
2802
|
+
return response;
|
|
2803
|
+
} catch (error) {
|
|
2804
|
+
clearTimeout(timer);
|
|
2805
|
+
throw error;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2599
2808
|
});
|
|
2809
|
+
if (transport instanceof StdioClientTransport)
|
|
2810
|
+
transport.stderr?.on("data", () => {
|
|
2811
|
+
});
|
|
2600
2812
|
try {
|
|
2601
2813
|
await waitFor(
|
|
2602
|
-
client.connect(transport),
|
|
2814
|
+
client.connect(transport, { signal, timeout }),
|
|
2603
2815
|
AbortSignal.any([signal, AbortSignal.timeout(timeout)])
|
|
2604
2816
|
);
|
|
2605
2817
|
signal.throwIfAborted();
|
|
@@ -2621,6 +2833,7 @@ var McpRuntime = class {
|
|
|
2621
2833
|
}
|
|
2622
2834
|
states = /* @__PURE__ */ new Map();
|
|
2623
2835
|
lifetime = new AbortController();
|
|
2836
|
+
closing;
|
|
2624
2837
|
identity(name) {
|
|
2625
2838
|
const config = this.definition(name);
|
|
2626
2839
|
let resolved;
|
|
@@ -2643,18 +2856,21 @@ var McpRuntime = class {
|
|
|
2643
2856
|
definition(name) {
|
|
2644
2857
|
const config = Object.hasOwn(this.config, name) ? this.config[name] : void 0;
|
|
2645
2858
|
if (!config || config.disabled)
|
|
2646
|
-
throw new ToolContractError(
|
|
2859
|
+
throw new ToolContractError(
|
|
2860
|
+
"MCP server is not configured or is disabled."
|
|
2861
|
+
);
|
|
2647
2862
|
return config;
|
|
2648
2863
|
}
|
|
2649
2864
|
state(name) {
|
|
2650
2865
|
let state = this.states.get(name);
|
|
2651
2866
|
if (!state) {
|
|
2652
|
-
state = {};
|
|
2867
|
+
state = { catalogGeneration: 0 };
|
|
2653
2868
|
this.states.set(name, state);
|
|
2654
2869
|
}
|
|
2655
2870
|
return state;
|
|
2656
2871
|
}
|
|
2657
2872
|
async client(name) {
|
|
2873
|
+
if (this.closing) throw new Error("MCP session ended.");
|
|
2658
2874
|
this.lifetime.signal.throwIfAborted();
|
|
2659
2875
|
const state = this.state(name);
|
|
2660
2876
|
const identity = this.identity(name);
|
|
@@ -2666,6 +2882,8 @@ var McpRuntime = class {
|
|
|
2666
2882
|
if (state.connecting) return state.connecting;
|
|
2667
2883
|
const config = resolveServer(this.definition(name), this.cwd);
|
|
2668
2884
|
state.connectionIdentity = identity;
|
|
2885
|
+
const token = {};
|
|
2886
|
+
state.connectionToken = token;
|
|
2669
2887
|
state.connecting = (async () => {
|
|
2670
2888
|
const { client, transport } = await this.connect(
|
|
2671
2889
|
name,
|
|
@@ -2675,9 +2893,28 @@ var McpRuntime = class {
|
|
|
2675
2893
|
this.cwd,
|
|
2676
2894
|
this.lifetime.signal
|
|
2677
2895
|
),
|
|
2678
|
-
this.lifetime.signal
|
|
2896
|
+
this.lifetime.signal,
|
|
2897
|
+
() => {
|
|
2898
|
+
if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token)
|
|
2899
|
+
return;
|
|
2900
|
+
state.catalogGeneration++;
|
|
2901
|
+
state.catalogDirty = true;
|
|
2902
|
+
state.tools = void 0;
|
|
2903
|
+
state.warnings = void 0;
|
|
2904
|
+
const path = join2(this.cacheDir, `${identity}.json`);
|
|
2905
|
+
state.invalidating = withFileMutationQueue2(
|
|
2906
|
+
path,
|
|
2907
|
+
() => rm2(path, { force: true })
|
|
2908
|
+
).catch(() => {
|
|
2909
|
+
state.warnings = [
|
|
2910
|
+
`${name}: stale catalog cache could not be removed.`
|
|
2911
|
+
];
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2679
2914
|
);
|
|
2680
|
-
if (this.lifetime.signal.aborted) {
|
|
2915
|
+
if (this.closing || this.lifetime.signal.aborted) {
|
|
2916
|
+
await client.autoOpenedSubscription?.close().catch(() => {
|
|
2917
|
+
});
|
|
2681
2918
|
await client.close();
|
|
2682
2919
|
throw new Error("MCP session ended.");
|
|
2683
2920
|
}
|
|
@@ -2688,10 +2925,12 @@ var McpRuntime = class {
|
|
|
2688
2925
|
if (state.client === client) {
|
|
2689
2926
|
state.client = void 0;
|
|
2690
2927
|
state.transport = void 0;
|
|
2928
|
+
state.connectionToken = void 0;
|
|
2691
2929
|
}
|
|
2692
2930
|
};
|
|
2693
2931
|
return client;
|
|
2694
2932
|
})().catch((error) => {
|
|
2933
|
+
if (state.connectionToken === token) state.connectionToken = void 0;
|
|
2695
2934
|
throw new DiagnosticError(
|
|
2696
2935
|
diagnose(error, {
|
|
2697
2936
|
server: name,
|
|
@@ -2707,10 +2946,13 @@ var McpRuntime = class {
|
|
|
2707
2946
|
}
|
|
2708
2947
|
async catalog(name, signal, refresh = false) {
|
|
2709
2948
|
signal?.throwIfAborted();
|
|
2949
|
+
if (this.closing) throw new Error("MCP session ended.");
|
|
2710
2950
|
this.lifetime.signal.throwIfAborted();
|
|
2711
2951
|
const state = this.state(name);
|
|
2712
2952
|
const identity = this.identity(name);
|
|
2713
|
-
|
|
2953
|
+
refresh ||= !!state.catalogDirty;
|
|
2954
|
+
if (!refresh && state.tools && state.identity === identity)
|
|
2955
|
+
return state.tools;
|
|
2714
2956
|
if (refresh && state.listing && !state.listingLive) {
|
|
2715
2957
|
await waitFor(state.listing, signal);
|
|
2716
2958
|
return this.catalog(name, signal, true);
|
|
@@ -2718,44 +2960,59 @@ var McpRuntime = class {
|
|
|
2718
2960
|
if (!state.listing) {
|
|
2719
2961
|
state.listingLive = refresh;
|
|
2720
2962
|
state.listing = (async () => {
|
|
2721
|
-
|
|
2722
|
-
const
|
|
2723
|
-
if (
|
|
2724
|
-
|
|
2725
|
-
state.
|
|
2726
|
-
|
|
2963
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
2964
|
+
const generation = state.catalogGeneration;
|
|
2965
|
+
if (!refresh && !state.catalogDirty) {
|
|
2966
|
+
const cached = await this.readCache(name, identity);
|
|
2967
|
+
if (cached && generation === state.catalogGeneration) {
|
|
2968
|
+
state.tools = cached;
|
|
2969
|
+
state.identity = identity;
|
|
2970
|
+
return cached;
|
|
2971
|
+
}
|
|
2727
2972
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2973
|
+
const client = await this.client(name);
|
|
2974
|
+
const listed = await client.listTools(void 0, {
|
|
2975
|
+
signal: this.lifetime.signal,
|
|
2976
|
+
timeout: this.definition(name).timeoutMs ?? 15e3
|
|
2977
|
+
});
|
|
2978
|
+
const tools = [];
|
|
2979
|
+
const warnings = [];
|
|
2980
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2981
|
+
for (const tool of listed.tools) {
|
|
2982
|
+
if (!allowed(tool.name, this.definition(name))) continue;
|
|
2983
|
+
try {
|
|
2984
|
+
const prepared = prepareTool(name, identity, tool);
|
|
2985
|
+
if (seen.has(prepared.nativeName))
|
|
2986
|
+
throw new Error("Duplicate tool name.");
|
|
2987
|
+
seen.add(prepared.nativeName);
|
|
2988
|
+
tools.push(prepared);
|
|
2989
|
+
} catch {
|
|
2990
|
+
warnings.push(
|
|
2991
|
+
`${name}: skipped an invalid, duplicate, or unsupported tool schema.`
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2748
2994
|
}
|
|
2995
|
+
this.lifetime.signal.throwIfAborted();
|
|
2996
|
+
if (generation !== state.catalogGeneration) continue;
|
|
2997
|
+
await state.invalidating;
|
|
2998
|
+
await this.writeCache(
|
|
2999
|
+
identity,
|
|
3000
|
+
tools,
|
|
3001
|
+
() => generation === state.catalogGeneration
|
|
3002
|
+
).catch(() => {
|
|
3003
|
+
warnings.push(`${name}: catalog cache could not be saved.`);
|
|
3004
|
+
});
|
|
3005
|
+
if (generation !== state.catalogGeneration) continue;
|
|
3006
|
+
state.tools = tools;
|
|
3007
|
+
state.identity = identity;
|
|
3008
|
+
state.catalogDirty = false;
|
|
3009
|
+
state.error = void 0;
|
|
3010
|
+
state.warnings = warnings;
|
|
3011
|
+
return tools;
|
|
2749
3012
|
}
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
state.error = void 0;
|
|
2754
|
-
state.warnings = warnings;
|
|
2755
|
-
await this.writeCache(identity, tools).catch(() => {
|
|
2756
|
-
warnings.push(`${name}: catalog cache could not be saved.`);
|
|
2757
|
-
});
|
|
2758
|
-
return tools;
|
|
3013
|
+
throw new ToolContractError(
|
|
3014
|
+
"MCP tool catalog kept changing. Retry discovery or activation."
|
|
3015
|
+
);
|
|
2759
3016
|
})().catch((error) => {
|
|
2760
3017
|
state.error = this.failure(name, error);
|
|
2761
3018
|
throw new DiagnosticError(state.error);
|
|
@@ -2767,8 +3024,8 @@ var McpRuntime = class {
|
|
|
2767
3024
|
return waitFor(state.listing, signal);
|
|
2768
3025
|
}
|
|
2769
3026
|
async discover(server, signal) {
|
|
2770
|
-
if (server) this.definition(server);
|
|
2771
|
-
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();
|
|
2772
3029
|
const result = {
|
|
2773
3030
|
tools: [],
|
|
2774
3031
|
unavailable: [],
|
|
@@ -2805,14 +3062,17 @@ var McpRuntime = class {
|
|
|
2805
3062
|
const found = current.find((candidate) => candidate.name === tool.name);
|
|
2806
3063
|
if (!found || found.schemaHash !== tool.schemaHash)
|
|
2807
3064
|
throw new ToolContractError(
|
|
2808
|
-
"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."
|
|
2809
3066
|
);
|
|
2810
3067
|
const client = await waitFor(this.client(tool.server), signal);
|
|
2811
3068
|
try {
|
|
2812
3069
|
return await client.callTool(
|
|
2813
3070
|
{ name: tool.name, arguments: args },
|
|
2814
3071
|
{
|
|
2815
|
-
signal: AbortSignal.any([
|
|
3072
|
+
signal: AbortSignal.any([
|
|
3073
|
+
this.lifetime.signal,
|
|
3074
|
+
...signal ? [signal] : []
|
|
3075
|
+
]),
|
|
2816
3076
|
timeout: config.timeoutMs ?? 3e4,
|
|
2817
3077
|
onprogress: (event) => progress?.(
|
|
2818
3078
|
event.message ?? `${event.progress}${event.total === void 0 ? "" : `/${event.total}`}`
|
|
@@ -2835,18 +3095,38 @@ var McpRuntime = class {
|
|
|
2835
3095
|
const state = this.state(name);
|
|
2836
3096
|
if (state.connecting || state.listing)
|
|
2837
3097
|
throw failure("busy", { server: name, operation: "reconnect" });
|
|
3098
|
+
await state.client?.autoOpenedSubscription?.close();
|
|
2838
3099
|
await state.client?.close();
|
|
2839
3100
|
state.client = void 0;
|
|
2840
3101
|
await this.catalog(name, void 0, true);
|
|
2841
3102
|
}
|
|
2842
|
-
|
|
3103
|
+
serverStatuses() {
|
|
2843
3104
|
return Object.entries(this.config).map(([name, config]) => {
|
|
2844
3105
|
const state = this.states.get(name);
|
|
2845
|
-
|
|
2846
|
-
|
|
3106
|
+
return {
|
|
3107
|
+
name,
|
|
3108
|
+
state: config.disabled ? "disabled" : state?.connecting || state?.listing ? "connecting" : state?.error ? "failed" : state?.client ? "connected" : "disconnected",
|
|
3109
|
+
catalogSize: state?.tools?.length,
|
|
3110
|
+
error: state?.error
|
|
3111
|
+
};
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
status(server) {
|
|
3115
|
+
return this.serverStatuses().filter(({ name }) => server === void 0 || name === server).map((row) => {
|
|
3116
|
+
const status = row.state === "failed" && row.error ? formatDiagnostic({ ...row.error, server: void 0 }) : row.state;
|
|
3117
|
+
return `${row.name}: ${status} \xB7 ${row.catalogSize ?? "unknown"} catalog tools`;
|
|
2847
3118
|
}).join("\n") || "No MCP servers configured.";
|
|
2848
3119
|
}
|
|
2849
|
-
|
|
3120
|
+
close() {
|
|
3121
|
+
return this.closing ??= this.shutdown();
|
|
3122
|
+
}
|
|
3123
|
+
async shutdown() {
|
|
3124
|
+
await Promise.all(
|
|
3125
|
+
[...this.states.values()].map(
|
|
3126
|
+
(state) => state.client?.autoOpenedSubscription?.close().catch(() => {
|
|
3127
|
+
})
|
|
3128
|
+
)
|
|
3129
|
+
);
|
|
2850
3130
|
this.lifetime.abort(new Error("MCP session ended."));
|
|
2851
3131
|
await Promise.all(
|
|
2852
3132
|
[...this.states.values()].map(async (state) => {
|
|
@@ -2856,6 +3136,7 @@ var McpRuntime = class {
|
|
|
2856
3136
|
});
|
|
2857
3137
|
await state.listing?.catch(() => {
|
|
2858
3138
|
});
|
|
3139
|
+
await state.invalidating;
|
|
2859
3140
|
})
|
|
2860
3141
|
);
|
|
2861
3142
|
}
|
|
@@ -2870,8 +3151,9 @@ var McpRuntime = class {
|
|
|
2870
3151
|
async readCache(name, identity) {
|
|
2871
3152
|
try {
|
|
2872
3153
|
const path = join2(this.cacheDir, `${identity}.json`);
|
|
2873
|
-
const info = await
|
|
2874
|
-
if (info.size > 4 * 1024 * 1024 || Date.now() - info.mtimeMs > 864e5)
|
|
3154
|
+
const info = await stat2(path);
|
|
3155
|
+
if (info.size > 4 * 1024 * 1024 || Date.now() - info.mtimeMs > 864e5)
|
|
3156
|
+
return;
|
|
2875
3157
|
const data = JSON.parse(await readFile2(path, "utf8"));
|
|
2876
3158
|
if (!Array.isArray(data) || data.length > 1e4) return;
|
|
2877
3159
|
return data.filter((tool) => allowed(tool.name, this.definition(name))).map((tool) => prepareTool(name, identity, tool));
|
|
@@ -2879,7 +3161,7 @@ var McpRuntime = class {
|
|
|
2879
3161
|
return;
|
|
2880
3162
|
}
|
|
2881
3163
|
}
|
|
2882
|
-
async writeCache(identity, tools) {
|
|
3164
|
+
async writeCache(identity, tools, isCurrent) {
|
|
2883
3165
|
const text = JSON.stringify(
|
|
2884
3166
|
tools.map(({ name, description, inputSchema }) => ({
|
|
2885
3167
|
name,
|
|
@@ -2890,26 +3172,28 @@ var McpRuntime = class {
|
|
|
2890
3172
|
if (Buffer.byteLength(text) > 4 * 1024 * 1024) return;
|
|
2891
3173
|
await mkdir(this.cacheDir, { recursive: true, mode: 448 });
|
|
2892
3174
|
const path = join2(this.cacheDir, `${identity}.json`);
|
|
2893
|
-
await
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
await
|
|
3175
|
+
await withFileMutationQueue2(path, async () => {
|
|
3176
|
+
if (!isCurrent()) return;
|
|
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 });
|
|
2897
3181
|
});
|
|
2898
3182
|
}
|
|
2899
3183
|
};
|
|
2900
3184
|
|
|
2901
3185
|
// src/exposure.ts
|
|
2902
3186
|
init_config();
|
|
2903
|
-
var
|
|
3187
|
+
var TOOLS_TOOL = "mcp_tools";
|
|
2904
3188
|
function restoredTools(entries) {
|
|
2905
3189
|
const tools = /* @__PURE__ */ new Map();
|
|
2906
3190
|
for (const entry of entries) {
|
|
2907
|
-
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)
|
|
2908
3192
|
continue;
|
|
2909
3193
|
const details = entry.message.details;
|
|
2910
3194
|
if (!object(details) || details.mcpClient !== 1 || !Array.isArray(details.loaded))
|
|
2911
3195
|
continue;
|
|
2912
|
-
for (const raw of details.loaded.slice(0,
|
|
3196
|
+
for (const raw of details.loaded.slice(0, MAX_SEARCH_LIMIT)) {
|
|
2913
3197
|
if (!object(raw) || typeof raw.server !== "string" || typeof raw.identity !== "string" || typeof raw.name !== "string" || typeof raw.description !== "string" || !object(raw.inputSchema))
|
|
2914
3198
|
continue;
|
|
2915
3199
|
try {
|
|
@@ -2972,40 +3256,59 @@ var Exposure = class {
|
|
|
2972
3256
|
|
|
2973
3257
|
// src/output.ts
|
|
2974
3258
|
init_diagnostics();
|
|
2975
|
-
import { mkdtemp, writeFile as
|
|
3259
|
+
import { mkdtemp, writeFile as writeFile3 } from "node:fs/promises";
|
|
2976
3260
|
import { tmpdir } from "node:os";
|
|
2977
3261
|
import { join as join3 } from "node:path";
|
|
2978
3262
|
import {
|
|
2979
3263
|
truncateHead,
|
|
2980
|
-
withFileMutationQueue as
|
|
3264
|
+
withFileMutationQueue as withFileMutationQueue3
|
|
2981
3265
|
} from "@earendil-works/pi-coding-agent";
|
|
2982
3266
|
function textResult(text, details) {
|
|
2983
3267
|
return { content: [{ type: "text", text }], details };
|
|
2984
3268
|
}
|
|
2985
3269
|
async function convertResult(result, label) {
|
|
2986
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
|
+
};
|
|
2987
3279
|
const images = [];
|
|
2988
3280
|
let imageBytes = 0;
|
|
2989
3281
|
let needsSpill = false;
|
|
2990
3282
|
for (const part of result.content ?? []) {
|
|
2991
|
-
if (part.type === "text")
|
|
3283
|
+
if (part.type === "text") append(part.text);
|
|
2992
3284
|
else if (part.type === "image" && ["image/png", "image/jpeg", "image/gif", "image/webp"].includes(part.mimeType) && imageBytes + part.data.length <= 8 * 1024 * 1024) {
|
|
2993
3285
|
images.push({ type: "image", data: part.data, mimeType: part.mimeType });
|
|
2994
3286
|
imageBytes += part.data.length;
|
|
2995
3287
|
} else if (part.type === "resource" && "text" in part.resource)
|
|
2996
|
-
|
|
2997
|
-
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
|
+
});
|
|
2998
3294
|
else {
|
|
2999
|
-
|
|
3295
|
+
append(`[${part.type} content saved in full result file]`, {
|
|
3296
|
+
mimeType: "text/plain"
|
|
3297
|
+
});
|
|
3000
3298
|
needsSpill = true;
|
|
3001
3299
|
}
|
|
3002
3300
|
}
|
|
3003
3301
|
if (result.structuredContent !== void 0)
|
|
3004
|
-
|
|
3302
|
+
append(JSON.stringify(result.structuredContent, null, 2), { structured: true });
|
|
3005
3303
|
const truncated = truncateHead(texts.join("\n\n"));
|
|
3006
3304
|
let text = truncated.content;
|
|
3007
3305
|
const details = {
|
|
3008
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
|
+
})),
|
|
3009
3312
|
failed: result.isError === true,
|
|
3010
3313
|
...result.isError ? {
|
|
3011
3314
|
diagnostics: [
|
|
@@ -3019,9 +3322,9 @@ async function convertResult(result, label) {
|
|
|
3019
3322
|
};
|
|
3020
3323
|
if (truncated.truncated || needsSpill) {
|
|
3021
3324
|
const path = join3(await mkdtemp(join3(tmpdir(), "pi-mcp-client-")), "result.json");
|
|
3022
|
-
await
|
|
3325
|
+
await withFileMutationQueue3(
|
|
3023
3326
|
path,
|
|
3024
|
-
() =>
|
|
3327
|
+
() => writeFile3(path, JSON.stringify(result), { mode: 384 })
|
|
3025
3328
|
);
|
|
3026
3329
|
details.fullOutputPath = path;
|
|
3027
3330
|
text += `
|
|
@@ -3036,9 +3339,104 @@ Full MCP result: ${path}`;
|
|
|
3036
3339
|
|
|
3037
3340
|
// src/render.ts
|
|
3038
3341
|
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
3039
|
-
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3342
|
+
import { Text, truncateToWidth as truncateToWidth2 } from "@earendil-works/pi-tui";
|
|
3040
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
|
|
3041
3437
|
var states = {
|
|
3438
|
+
candidate: { glyph: "\u25CB", color: "dim" },
|
|
3439
|
+
active: { glyph: "\u25CF", color: "dim" },
|
|
3042
3440
|
queued: { glyph: "\u25CF", color: "dim" },
|
|
3043
3441
|
running: { glyph: "\u25B6\uFE0E", color: "muted" },
|
|
3044
3442
|
done: { glyph: "\u2714\uFE0E", color: "success" },
|
|
@@ -3047,16 +3445,16 @@ var states = {
|
|
|
3047
3445
|
};
|
|
3048
3446
|
function renderCall(title, args, theme, expanded) {
|
|
3049
3447
|
const values = object(args) ? args : {};
|
|
3050
|
-
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(" ");
|
|
3051
3449
|
return {
|
|
3052
3450
|
render(width) {
|
|
3053
3451
|
if (width <= 0) return [];
|
|
3054
3452
|
const text = theme.fg("toolTitle", theme.bold(line(title))) + (preview ? theme.fg("dim", ` ${preview}`) : "");
|
|
3055
3453
|
if (expanded)
|
|
3056
|
-
return new Text(text, 0, 0).render(width).map((row) =>
|
|
3454
|
+
return new Text(text, 0, 0).render(width).map((row) => truncateToWidth2(row, width));
|
|
3057
3455
|
const key = keyText("app.tools.expand");
|
|
3058
3456
|
return [
|
|
3059
|
-
|
|
3457
|
+
truncateToWidth2(
|
|
3060
3458
|
text + (key ? theme.fg("muted", ` (${key} to expand)`) : ""),
|
|
3061
3459
|
width
|
|
3062
3460
|
)
|
|
@@ -3066,40 +3464,51 @@ function renderCall(title, args, theme, expanded) {
|
|
|
3066
3464
|
}
|
|
3067
3465
|
};
|
|
3068
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
|
+
}
|
|
3069
3473
|
function renderResult(result, options, theme, isError) {
|
|
3070
3474
|
const details = object(result.details) && result.details.mcpClient === 1 ? result.details : void 0;
|
|
3071
|
-
const text =
|
|
3072
|
-
|
|
3073
|
-
|
|
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;
|
|
3074
3478
|
const rows = details?.rows ?? [
|
|
3075
3479
|
{
|
|
3076
|
-
label: line(text) || "Working\u2026",
|
|
3480
|
+
label: validation ? "Invalid tool arguments" : isError ? "Tool failed" : line(text) || "Working\u2026",
|
|
3077
3481
|
state: isError ? "failed" : options.isPartial ? "running" : "done"
|
|
3078
3482
|
}
|
|
3079
3483
|
];
|
|
3484
|
+
let formattedOutput;
|
|
3080
3485
|
return {
|
|
3081
3486
|
render(width) {
|
|
3082
3487
|
if (width <= 0) return [];
|
|
3083
3488
|
const lines = rows.flatMap((row) => {
|
|
3084
3489
|
const status = states[row.state] ?? states.failed;
|
|
3085
|
-
const value = theme.fg(status.color, status.glyph) + " " + theme.fg("accent", line(row.label));
|
|
3086
|
-
const rendered = options.expanded && (!details?.searchNotes || row.state === "failed") ? new Text(value, 0, 0).render(width).map((x) =>
|
|
3490
|
+
const value = theme.fg(status.color, status.glyph) + " " + theme.fg("accent", line(row.label)) + (row.inlineDescription ? theme.fg("dim", ` ${line(row.inlineDescription)}`) : "");
|
|
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)];
|
|
3087
3492
|
if (options.expanded && row.description)
|
|
3088
|
-
rendered.push(
|
|
3493
|
+
rendered.push(truncateToWidth2(
|
|
3089
3494
|
theme.fg("dim", ` ${line(row.description)}`),
|
|
3090
3495
|
width
|
|
3091
3496
|
));
|
|
3092
3497
|
return rendered;
|
|
3093
3498
|
});
|
|
3094
3499
|
for (const note of details?.searchNotes ?? [])
|
|
3095
|
-
lines.push(...(options.expanded ? new Text(theme.fg("warning", plain(note)), 0, 0).render(width) : [theme.fg("warning", line(note))]).map((row) =>
|
|
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)));
|
|
3096
3501
|
if (options.expanded && !options.isPartial && text && !details?.searchNotes)
|
|
3097
3502
|
lines.push(
|
|
3098
|
-
...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))
|
|
3099
3508
|
);
|
|
3100
3509
|
if (!options.expanded && details?.fullOutputPath)
|
|
3101
3510
|
lines.push(
|
|
3102
|
-
|
|
3511
|
+
truncateToWidth2(
|
|
3103
3512
|
theme.fg("dim", `Full result: ${details.fullOutputPath}`),
|
|
3104
3513
|
width
|
|
3105
3514
|
)
|
|
@@ -3107,6 +3516,7 @@ function renderResult(result, options, theme, isError) {
|
|
|
3107
3516
|
return lines;
|
|
3108
3517
|
},
|
|
3109
3518
|
invalidate() {
|
|
3519
|
+
formattedOutput = void 0;
|
|
3110
3520
|
}
|
|
3111
3521
|
};
|
|
3112
3522
|
}
|
|
@@ -3123,7 +3533,10 @@ function errorResult(error, context) {
|
|
|
3123
3533
|
failed: true,
|
|
3124
3534
|
diagnostics: [value],
|
|
3125
3535
|
rows: [
|
|
3126
|
-
{
|
|
3536
|
+
{
|
|
3537
|
+
label: value.code === "cancelled" ? "Cancelled" : message,
|
|
3538
|
+
state: value.code === "cancelled" ? "cancelled" : "failed"
|
|
3539
|
+
}
|
|
3127
3540
|
]
|
|
3128
3541
|
});
|
|
3129
3542
|
}
|
|
@@ -3132,6 +3545,7 @@ function mcpClient(pi, options = {}) {
|
|
|
3132
3545
|
let runtime;
|
|
3133
3546
|
let config = {};
|
|
3134
3547
|
let configError;
|
|
3548
|
+
let sessionGeneration = 0;
|
|
3135
3549
|
const exposure = new Exposure(pi, registerNative);
|
|
3136
3550
|
const current = () => {
|
|
3137
3551
|
if (!runtime)
|
|
@@ -3192,34 +3606,72 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3192
3606
|
});
|
|
3193
3607
|
exposure.restore(tools);
|
|
3194
3608
|
};
|
|
3609
|
+
async function reloadConfiguration(ctx, toggle) {
|
|
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
|
+
};
|
|
3619
|
+
ctx.signal?.throwIfAborted();
|
|
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);
|
|
3630
|
+
const next = new McpRuntime(
|
|
3631
|
+
nextConfig,
|
|
3632
|
+
ctx.cwd,
|
|
3633
|
+
join4(agentDir, "cache", "pi-mcp-client")
|
|
3634
|
+
);
|
|
3635
|
+
const active = new Set(pi.getActiveTools());
|
|
3636
|
+
const retained = [...exposure.definitions.values()].filter((tool) => {
|
|
3637
|
+
try {
|
|
3638
|
+
return active.has(tool.nativeName) && next.identity(tool.server) === tool.identity && allowed(tool.name, nextConfig[tool.server]);
|
|
3639
|
+
} catch {
|
|
3640
|
+
return false;
|
|
3641
|
+
}
|
|
3642
|
+
});
|
|
3643
|
+
const old = runtime;
|
|
3644
|
+
config = nextConfig;
|
|
3645
|
+
configError = void 0;
|
|
3646
|
+
runtime = next;
|
|
3647
|
+
exposure.restore(retained);
|
|
3648
|
+
await old?.close();
|
|
3649
|
+
return update?.scope;
|
|
3650
|
+
}
|
|
3195
3651
|
pi.on("session_start", async (_event, ctx) => {
|
|
3652
|
+
sessionGeneration++;
|
|
3196
3653
|
await runtime?.close();
|
|
3197
3654
|
runtime = void 0;
|
|
3198
3655
|
config = {};
|
|
3199
3656
|
configError = void 0;
|
|
3200
3657
|
try {
|
|
3201
3658
|
config = await loadConfig(agentDir, ctx.cwd, ctx.isProjectTrusted());
|
|
3202
|
-
runtime = new McpRuntime(
|
|
3203
|
-
config,
|
|
3204
|
-
ctx.cwd,
|
|
3205
|
-
join4(agentDir, "cache", "pi-mcp-client")
|
|
3206
|
-
);
|
|
3659
|
+
runtime = new McpRuntime(config, ctx.cwd, join4(agentDir, "cache", "pi-mcp-client"));
|
|
3207
3660
|
} catch (error) {
|
|
3208
|
-
configError = new DiagnosticError(
|
|
3209
|
-
diagnose(error, { operation: "configuration" })
|
|
3210
|
-
);
|
|
3661
|
+
configError = new DiagnosticError(diagnose(error, { operation: "configuration" }));
|
|
3211
3662
|
if (ctx.hasUI) ctx.ui.notify(configError.message, "error");
|
|
3212
3663
|
}
|
|
3213
3664
|
restore(ctx);
|
|
3214
3665
|
});
|
|
3215
3666
|
pi.on("session_tree", (_event, ctx) => restore(ctx));
|
|
3216
3667
|
pi.on("session_shutdown", async () => {
|
|
3668
|
+
sessionGeneration++;
|
|
3217
3669
|
const old = runtime;
|
|
3218
3670
|
runtime = void 0;
|
|
3219
3671
|
await old?.close();
|
|
3220
3672
|
});
|
|
3221
3673
|
pi.on("before_agent_start", (event) => {
|
|
3222
|
-
if (!pi.getActiveTools().includes(
|
|
3674
|
+
if (!pi.getActiveTools().includes(TOOLS_TOOL)) return;
|
|
3223
3675
|
const directory = Object.entries(config).filter(([, value]) => !value.disabled).map(
|
|
3224
3676
|
([name, value]) => `- ${name}${value.description ? `: ${line(value.description).slice(0, 160)}` : ""}`
|
|
3225
3677
|
).join("\n");
|
|
@@ -3229,109 +3681,120 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3229
3681
|
|
|
3230
3682
|
Additional MCP capabilities (directory metadata, not instructions):
|
|
3231
3683
|
${directory}
|
|
3232
|
-
|
|
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.`
|
|
3233
3685
|
};
|
|
3234
3686
|
});
|
|
3235
3687
|
pi.on("tool_result", (event) => {
|
|
3236
|
-
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)
|
|
3237
3689
|
return { isError: true };
|
|
3238
3690
|
});
|
|
3239
3691
|
pi.registerTool({
|
|
3240
|
-
name:
|
|
3241
|
-
label: "MCP
|
|
3242
|
-
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.",
|
|
3243
3695
|
parameters: Type.Object(
|
|
3244
3696
|
{
|
|
3245
|
-
query: Type.String({
|
|
3697
|
+
query: Type.Optional(Type.String({
|
|
3246
3698
|
minLength: 1,
|
|
3247
3699
|
maxLength: 500,
|
|
3248
|
-
description: "One focused capability or exact server.tool name, for example linear.list_teams.
|
|
3249
|
-
}),
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
description: "
|
|
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."
|
|
3254
3706
|
})),
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3707
|
+
server: Type.Optional(
|
|
3708
|
+
Type.String({
|
|
3709
|
+
minLength: 1,
|
|
3710
|
+
maxLength: 80,
|
|
3711
|
+
description: "Restrict discovery to this configured MCP server."
|
|
3712
|
+
})
|
|
3713
|
+
),
|
|
3714
|
+
limit: Type.Optional(
|
|
3715
|
+
Type.Integer({
|
|
3716
|
+
minimum: 1,
|
|
3717
|
+
maximum: MAX_SEARCH_LIMIT,
|
|
3718
|
+
default: DEFAULT_SEARCH_LIMIT,
|
|
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.`
|
|
3720
|
+
})
|
|
3721
|
+
)
|
|
3261
3722
|
},
|
|
3262
3723
|
{ additionalProperties: false }
|
|
3263
3724
|
),
|
|
3264
|
-
renderCall: (args, theme, context) => renderCall("mcp
|
|
3725
|
+
renderCall: (args, theme, context) => renderCall(args.activate ? "mcp activate" : "mcp discover", args, theme, context.expanded),
|
|
3265
3726
|
renderResult: (result, options2, theme, context) => renderResult(result, options2, theme, context.isError),
|
|
3266
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" }] });
|
|
3267
3732
|
try {
|
|
3268
3733
|
const activeRuntime = current();
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
})
|
|
3274
|
-
);
|
|
3275
|
-
const namedServer = Object.keys(config).sort((a, b) => b.length - a.length).find(
|
|
3276
|
-
(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))
|
|
3277
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
|
+
}));
|
|
3278
3743
|
const discovery = await activeRuntime.discover(
|
|
3279
|
-
args.server ??
|
|
3744
|
+
hasActivate ? identifiers.flatMap(serversFor) : args.server ?? serversFor(args.query)[0],
|
|
3280
3745
|
signal ?? ctx.signal
|
|
3281
3746
|
);
|
|
3282
3747
|
(signal ?? ctx.signal)?.throwIfAborted();
|
|
3283
3748
|
if (runtime !== activeRuntime)
|
|
3284
|
-
throw new Error("MCP session changed during search.");
|
|
3285
|
-
const matches2 = searchTools(
|
|
3286
|
-
discovery.tools,
|
|
3287
|
-
args.query,
|
|
3288
|
-
args.server,
|
|
3289
|
-
args.limit
|
|
3290
|
-
);
|
|
3291
|
-
const { loaded, added, rejected } = exposure.load(matches2);
|
|
3292
|
-
const messages2 = loaded.map(
|
|
3293
|
-
(tool) => `${added.includes(tool.nativeName) ? "Loaded" : "Already loaded"}: ${tool.nativeName} \u2014 ${line(tool.description).slice(0, 180)}`
|
|
3294
|
-
);
|
|
3295
|
-
if (!messages2.length)
|
|
3296
|
-
messages2.push(
|
|
3297
|
-
"No callable matches found. Try a more specific capability, server, or exact tool name."
|
|
3298
|
-
);
|
|
3299
|
-
if (loaded.length)
|
|
3300
|
-
messages2.push(
|
|
3301
|
-
"Call the loaded tools directly. Their full schemas are now available."
|
|
3302
|
-
);
|
|
3303
|
-
messages2.push(
|
|
3304
|
-
...discovery.unavailable.map((message) => `Not searched: ${message}`),
|
|
3305
|
-
...discovery.warnings
|
|
3306
|
-
);
|
|
3307
|
-
if (rejected.length)
|
|
3308
|
-
messages2.push(
|
|
3309
|
-
`Not loaded (name collision or Pi tool restriction): ${rejected.join(", ")}`
|
|
3310
|
-
);
|
|
3749
|
+
throw new Error("MCP session changed during search or activation.");
|
|
3311
3750
|
const details = {
|
|
3312
3751
|
mcpClient: 1,
|
|
3313
|
-
loaded,
|
|
3314
3752
|
searchNotes: discovery.warnings,
|
|
3315
3753
|
diagnostics: discovery.diagnostics,
|
|
3316
|
-
|
|
3317
|
-
rows: [
|
|
3318
|
-
...loaded.map((tool) => ({
|
|
3319
|
-
label: `${tool.server}.${tool.name} \xB7 ${added.includes(tool.nativeName) ? "loaded" : "already loaded"}`,
|
|
3320
|
-
description: tool.description,
|
|
3321
|
-
state: "done"
|
|
3322
|
-
})),
|
|
3323
|
-
...discovery.unavailable.map((label) => ({
|
|
3324
|
-
label,
|
|
3325
|
-
state: "failed"
|
|
3326
|
-
})),
|
|
3327
|
-
...rejected.map((name) => ({
|
|
3328
|
-
label: `${name} \xB7 not loaded`,
|
|
3329
|
-
state: "failed"
|
|
3330
|
-
}))
|
|
3331
|
-
]
|
|
3754
|
+
rows: []
|
|
3332
3755
|
};
|
|
3333
|
-
|
|
3334
|
-
|
|
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" });
|
|
3335
3798
|
return textResult(messages2.join("\n"), details);
|
|
3336
3799
|
} catch (error) {
|
|
3337
3800
|
return errorResult(error, {
|
|
@@ -3344,34 +3807,93 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3344
3807
|
}
|
|
3345
3808
|
});
|
|
3346
3809
|
pi.registerCommand("mcp", {
|
|
3347
|
-
description: "
|
|
3810
|
+
description: "Manage MCP servers: list, status, reload, enable|disable|inspect|tools|auth|reconnect|refresh <server>",
|
|
3348
3811
|
getArgumentCompletions(prefix) {
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
)
|
|
3354
|
-
|
|
3355
|
-
|
|
3812
|
+
const serverActions = ["enable", "disable", "inspect", "tools", "auth", "reconnect", "refresh"];
|
|
3813
|
+
const input = prefix.trimStart();
|
|
3814
|
+
const match = /^(\S+)\s+(.*)$/s.exec(input);
|
|
3815
|
+
if (!match) {
|
|
3816
|
+
return ["list", "status", "reload", ...serverActions].filter((action2) => action2.startsWith(input)).map((action2) => ({ value: action2, label: action2 }));
|
|
3817
|
+
}
|
|
3818
|
+
const [, action, partialServer] = match;
|
|
3819
|
+
if (!serverActions.includes(action) || /\s/.test(partialServer)) return [];
|
|
3820
|
+
return Object.keys(config).filter(
|
|
3821
|
+
(name) => name.startsWith(partialServer) && (action === "inspect" || (action === "enable" ? config[name].disabled : !config[name].disabled))
|
|
3822
|
+
).sort().map((name) => ({ value: `${action} ${name}`, label: name }));
|
|
3356
3823
|
},
|
|
3357
3824
|
async handler(args, ctx) {
|
|
3825
|
+
const generation = sessionGeneration;
|
|
3358
3826
|
await ctx.waitForIdle();
|
|
3359
3827
|
const [action = "status", server, ...extra] = args.trim().split(/\s+/).filter(Boolean);
|
|
3360
3828
|
try {
|
|
3361
|
-
if (
|
|
3362
|
-
|
|
3829
|
+
if (generation !== sessionGeneration)
|
|
3830
|
+
throw new CommandUsageError("The Pi session changed while waiting for idle.");
|
|
3831
|
+
if (action === "reload" && !server) {
|
|
3832
|
+
await reloadConfiguration(ctx);
|
|
3833
|
+
if (ctx.hasUI)
|
|
3834
|
+
ctx.ui.notify(
|
|
3835
|
+
"\u2714\uFE0E MCP configuration reloaded. Connections reopen on demand; tools from changed or removed servers are no longer active.",
|
|
3836
|
+
"info"
|
|
3837
|
+
);
|
|
3838
|
+
return;
|
|
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
|
+
}
|
|
3849
|
+
if (action === "inspect" && server && !extra.length && Object.hasOwn(config, server)) {
|
|
3363
3850
|
if (ctx.hasUI)
|
|
3364
3851
|
ctx.ui.notify(
|
|
3365
|
-
|
|
3366
|
-
${loaded} native MCP tools loaded.`,
|
|
3852
|
+
inspectServer(server, config[server], current().status(server)),
|
|
3367
3853
|
"info"
|
|
3368
3854
|
);
|
|
3369
3855
|
return;
|
|
3370
3856
|
}
|
|
3857
|
+
if ((action === "status" || action === "list") && !server) {
|
|
3858
|
+
const statuses = current().serverStatuses();
|
|
3859
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
3860
|
+
for (const name of pi.getActiveTools()) {
|
|
3861
|
+
const tool = exposure.definitions.get(name);
|
|
3862
|
+
if (tool) loaded.set(tool.server, (loaded.get(tool.server) ?? 0) + 1);
|
|
3863
|
+
}
|
|
3864
|
+
if (ctx.hasUI) ctx.ui.notify(serverMatrix(statuses, loaded), "info");
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3371
3867
|
if (!server || extra.length || !Object.hasOwn(config, server) || config[server].disabled)
|
|
3372
3868
|
throw new CommandUsageError(
|
|
3373
|
-
"Usage: /mcp auth|reconnect|refresh <
|
|
3869
|
+
"Usage: /mcp list|status|reload or /mcp enable|disable|inspect|tools|auth|reconnect|refresh <server>. Disabled servers accept enable, disable, and inspect."
|
|
3374
3870
|
);
|
|
3871
|
+
if (action === "tools") {
|
|
3872
|
+
if (!ctx.hasUI)
|
|
3873
|
+
throw new CommandUsageError("Tool browsing requires an interactive UI.");
|
|
3874
|
+
const tools = await current().catalog(server, ctx.signal, true);
|
|
3875
|
+
if (!tools.length) {
|
|
3876
|
+
ctx.ui.notify(
|
|
3877
|
+
`${server}: no tools available under the configured filters.`,
|
|
3878
|
+
"info"
|
|
3879
|
+
);
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
const choices = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
3883
|
+
const columns = ctx.mode === "tui" ? process.stdout.columns || 80 : 80;
|
|
3884
|
+
const labels = choices.map((tool2, index) => toolPickerLabel(tool2, index, columns));
|
|
3885
|
+
const selected = await ctx.ui.select(
|
|
3886
|
+
`${server}: ${tools.length} tools (select to inspect; none are activated)`,
|
|
3887
|
+
labels
|
|
3888
|
+
);
|
|
3889
|
+
const tool = selected === void 0 ? void 0 : choices[labels.indexOf(selected)];
|
|
3890
|
+
if (tool)
|
|
3891
|
+
ctx.ui.notify(
|
|
3892
|
+
inspectTool(tool),
|
|
3893
|
+
"info"
|
|
3894
|
+
);
|
|
3895
|
+
return;
|
|
3896
|
+
}
|
|
3375
3897
|
if (action === "auth") {
|
|
3376
3898
|
if (!ctx.hasUI)
|
|
3377
3899
|
throw new CommandUsageError(
|
|
@@ -3416,27 +3938,27 @@ ${target}`, "info");
|
|
|
3416
3938
|
} else await authenticate(url, open, ctx.signal);
|
|
3417
3939
|
await current().reconnect(server);
|
|
3418
3940
|
} else if (action === "reconnect") await current().reconnect(server);
|
|
3419
|
-
else if (action === "refresh")
|
|
3420
|
-
await current().catalog(server, ctx.signal, true);
|
|
3941
|
+
else if (action === "refresh") await current().catalog(server, ctx.signal, true);
|
|
3421
3942
|
else
|
|
3422
3943
|
throw new CommandUsageError(
|
|
3423
|
-
"Unknown MCP command. Use /mcp auth|reconnect|refresh <server>."
|
|
3944
|
+
"Unknown MCP command. Use /mcp list|status|reload or /mcp enable|disable|inspect|tools|auth|reconnect|refresh <server>."
|
|
3424
3945
|
);
|
|
3425
3946
|
if (ctx.hasUI)
|
|
3426
3947
|
ctx.ui.notify(
|
|
3427
|
-
|
|
3948
|
+
`\u2714\uFE0E ${server}: ${action} complete. Updated tools are available for the assistant to discover.`,
|
|
3428
3949
|
"info"
|
|
3429
3950
|
);
|
|
3430
3951
|
} catch (error) {
|
|
3431
3952
|
const message = error instanceof CommandUsageError ? error.message : formatDiagnostic(
|
|
3432
3953
|
diagnose(error, {
|
|
3433
3954
|
server,
|
|
3434
|
-
operation: action === "auth" ? "auth" : action === "refresh" ? "refresh" : "reconnect",
|
|
3955
|
+
operation: ["reload", "inspect", "enable", "disable"].includes(action) ? "configuration" : action === "tools" ? "search" : action === "auth" ? "auth" : action === "refresh" ? "refresh" : "reconnect",
|
|
3435
3956
|
oauth: config[server]?.oauth,
|
|
3436
3957
|
signal: ctx.signal
|
|
3437
3958
|
})
|
|
3438
3959
|
);
|
|
3439
3960
|
if (ctx.hasUI) ctx.ui.notify(message, "error");
|
|
3961
|
+
else throw new Error(message);
|
|
3440
3962
|
}
|
|
3441
3963
|
}
|
|
3442
3964
|
});
|