pi-mcp-client 0.1.0 → 0.2.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 +33 -3
- package/dist/index.js +393 -113
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,11 +50,20 @@ remain available as the conversation continues.
|
|
|
50
50
|
|
|
51
51
|
| Command | Purpose |
|
|
52
52
|
| --- | --- |
|
|
53
|
-
| `/mcp` | Show server
|
|
53
|
+
| `/mcp`, `/mcp list`, `/mcp status` | Show a server status matrix with catalog and loaded-tool counts. |
|
|
54
|
+
| `/mcp inspect <server>` | Inspect status and configuration, including disabled servers. Connection values are hidden. |
|
|
55
|
+
| `/mcp tools <server>` | Browse the server's tools and inspect descriptions without activating tools. |
|
|
56
|
+
| `/mcp reload` | Apply configuration changes without restarting Pi. |
|
|
54
57
|
| `/mcp auth <server>` | Authenticate an OAuth-enabled HTTP server. |
|
|
55
58
|
| `/mcp reconnect <server>` | Replace a connection and refresh its catalog. |
|
|
56
59
|
| `/mcp refresh <server>` | Refresh a server's catalog without loading additional tools. |
|
|
57
60
|
|
|
61
|
+
The status matrix uses glyphs to distinguish idle (`○`), connected (`●`),
|
|
62
|
+
connecting (`▶︎`), disabled (`○`), and failed (`✘︎`) servers. Idle is normal:
|
|
63
|
+
connections open on demand. A dash (`—`) means the catalog hasn't been fetched,
|
|
64
|
+
not that the server has no tools. The **Loaded** column counts tools currently
|
|
65
|
+
active for the assistant.
|
|
66
|
+
|
|
58
67
|
After refreshing a changed schema, search for the tool again to load its current
|
|
59
68
|
definition. Calls validate the live catalog before execution and refuse removed
|
|
60
69
|
or changed tools. The extension does not retry failed tool invocations; after an
|
|
@@ -185,8 +194,23 @@ Every definition must include a `url` or `command`, even when `disabled` is true
|
|
|
185
194
|
These options are specific to Pi MCP Client, not standardized MCP connection
|
|
186
195
|
fields. Other clients may reject them when you copy a definition.
|
|
187
196
|
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
After editing your configuration, run `/mcp reload` to apply it without restarting
|
|
198
|
+
Pi. Reload validates the new configuration before replacing the current setup;
|
|
199
|
+
invalid configuration leaves the previous setup intact. It closes existing
|
|
200
|
+
connections, which reopen on demand, and deactivates tools from changed, removed,
|
|
201
|
+
or disabled server definitions. Unchanged active tools remain available.
|
|
202
|
+
|
|
203
|
+
Use `/mcp inspect <server>` to check the effective transport, protocol, filters,
|
|
204
|
+
and connection status without connecting or running secret commands. Connection
|
|
205
|
+
values—including commands, arguments, URLs, headers, and environment variables—
|
|
206
|
+
are hidden because any of them can contain credentials.
|
|
207
|
+
|
|
208
|
+
Use `/mcp tools <server>` to fetch the current catalog and browse a scrollable
|
|
209
|
+
list. Each row shows the tool name and description, trimmed to the terminal width
|
|
210
|
+
with an ellipsis. Select a tool to see a multiline signature and parameter details,
|
|
211
|
+
with each parameter in a separate paragraph. Browsing respects your include and
|
|
212
|
+
exclude filters and doesn't activate tools or add their schemas to the assistant's
|
|
213
|
+
context. This command requires an interactive UI.
|
|
190
214
|
|
|
191
215
|
### Discovery and caching
|
|
192
216
|
|
|
@@ -201,6 +225,12 @@ They contain tool metadata, not configured credentials. Cached search needs no
|
|
|
201
225
|
connection; invocation refreshes the live catalog before calling the tool.
|
|
202
226
|
Connections remain open until shutdown or explicit reconnection.
|
|
203
227
|
|
|
228
|
+
When a connected server reports a tool-list change, the extension invalidates its
|
|
229
|
+
memory and disk catalogs. The next search fetches the current list, including new
|
|
230
|
+
or removed tools. Notifications don't replace active tool definitions: changed
|
|
231
|
+
schemas require another `mcp_search` before use. Disconnected, cache-only searches
|
|
232
|
+
can't receive notifications and still use the 24-hour disk-cache expiry.
|
|
233
|
+
|
|
204
234
|
### OAuth
|
|
205
235
|
|
|
206
236
|
Set `"oauth": true` under `mcpServers.<server>` in `mcp.json`, without an
|
package/dist/index.js
CHANGED
|
@@ -2354,6 +2354,116 @@ function searchTools(tools, query, server, limit = DEFAULT_SEARCH_LIMIT) {
|
|
|
2354
2354
|
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
2355
|
}
|
|
2356
2356
|
|
|
2357
|
+
// src/management.ts
|
|
2358
|
+
init_config();
|
|
2359
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2360
|
+
var serverStates = {
|
|
2361
|
+
disconnected: { glyph: "\u25CB", label: "idle" },
|
|
2362
|
+
connected: { glyph: "\u25CF", label: "connected" },
|
|
2363
|
+
connecting: { glyph: "\u25B6\uFE0E", label: "connecting" },
|
|
2364
|
+
failed: { glyph: "\u2718\uFE0E", label: "error" },
|
|
2365
|
+
disabled: { glyph: "\u25CB", label: "disabled" }
|
|
2366
|
+
};
|
|
2367
|
+
function serverMatrix(servers, loaded, width = 80) {
|
|
2368
|
+
if (width <= 0) return "";
|
|
2369
|
+
const fit = (text) => plain(truncateToWidth(text, width));
|
|
2370
|
+
if (!servers.length) return fit("No MCP servers configured.");
|
|
2371
|
+
const nameWidth = Math.min(40, Math.max(6, ...servers.map(({ name }) => name.length)));
|
|
2372
|
+
const heading = ` ${"Server".padEnd(nameWidth)} ${"State".padEnd(10)} ${"Tools".padStart(5)} ${"Loaded".padStart(6)}`;
|
|
2373
|
+
const rows = servers.map((server) => {
|
|
2374
|
+
const state = serverStates[server.state];
|
|
2375
|
+
const name = plain(truncateToWidth(line(server.name), nameWidth)).padEnd(nameWidth);
|
|
2376
|
+
return `${state.glyph} ${name} ${state.label.padEnd(10)} ${String(server.catalogSize ?? "\u2014").padStart(5)} ${String(loaded.get(server.name) ?? 0).padStart(6)}`;
|
|
2377
|
+
});
|
|
2378
|
+
const errors = servers.filter((server) => server.state === "failed" && server.error).map((server) => `\u2718\uFE0E ${line(server.name)}: [${server.error.code}] ${line(server.error.message)}`);
|
|
2379
|
+
return [
|
|
2380
|
+
heading,
|
|
2381
|
+
...rows,
|
|
2382
|
+
"",
|
|
2383
|
+
"Connections open on demand. \u2014 = catalog not fetched.",
|
|
2384
|
+
...errors
|
|
2385
|
+
].map(fit).join("\n");
|
|
2386
|
+
}
|
|
2387
|
+
function toolPickerLabel(tool, index, columns = 80) {
|
|
2388
|
+
const width = Math.max(0, columns - 4);
|
|
2389
|
+
const text = `${index + 1}. ${line(tool.name)}: ${line(tool.description) || "No description."}`;
|
|
2390
|
+
return plain(truncateToWidth(text, width, "\u2026"));
|
|
2391
|
+
}
|
|
2392
|
+
function schemaType(schema, depth = 0) {
|
|
2393
|
+
if (!object(schema) || depth > 2) return "unknown";
|
|
2394
|
+
const alternatives = schema.anyOf ?? schema.oneOf;
|
|
2395
|
+
if (Array.isArray(alternatives)) {
|
|
2396
|
+
if (alternatives.length > 4) return "union";
|
|
2397
|
+
return [...new Set(alternatives.map((part) => schemaType(part, depth + 1)))].join(" | ") || "unknown";
|
|
2398
|
+
}
|
|
2399
|
+
if (schema.$ref || schema.allOf) return "unknown";
|
|
2400
|
+
const type = schema.type;
|
|
2401
|
+
if (Array.isArray(type))
|
|
2402
|
+
return type.slice(0, 4).map((part) => schemaType({ ...schema, type: part }, depth + 1)).join(" | ");
|
|
2403
|
+
if (type === "array") return `Array<${schemaType(schema.items, depth + 1)}>`;
|
|
2404
|
+
if (type === "integer") return "integer";
|
|
2405
|
+
if (["string", "number", "boolean", "object", "null"].includes(String(type))) return String(type);
|
|
2406
|
+
return "unknown";
|
|
2407
|
+
}
|
|
2408
|
+
function toolParameters(tool) {
|
|
2409
|
+
const schema = tool.inputSchema;
|
|
2410
|
+
if (!object(schema)) return [];
|
|
2411
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
2412
|
+
return Object.entries(object(schema.properties) ? schema.properties : {}).map(([name, value]) => ({
|
|
2413
|
+
name: line(name).slice(0, 80),
|
|
2414
|
+
required: required.has(name),
|
|
2415
|
+
type: schemaType(value),
|
|
2416
|
+
description: object(value) && typeof value.description === "string" ? line(value.description) : ""
|
|
2417
|
+
}));
|
|
2418
|
+
}
|
|
2419
|
+
function toolSignature(tool, limit = 40) {
|
|
2420
|
+
const parameters = toolParameters(tool);
|
|
2421
|
+
const args = parameters.slice(0, limit).map(
|
|
2422
|
+
(parameter) => `${parameter.name}${parameter.required ? "" : "?"}: ${parameter.type}`
|
|
2423
|
+
);
|
|
2424
|
+
if (parameters.length > limit) args.push(`\u2026 +${parameters.length - limit} more`);
|
|
2425
|
+
const schema = tool.inputSchema;
|
|
2426
|
+
if (object(schema) && schema.additionalProperties !== false) args.push("\u2026");
|
|
2427
|
+
const name = line(tool.name).slice(0, 160);
|
|
2428
|
+
return args.length ? `${name}(
|
|
2429
|
+
${args.map((arg) => ` ${arg},`).join("\n")}
|
|
2430
|
+
)` : `${name}()`;
|
|
2431
|
+
}
|
|
2432
|
+
function inspectTool(tool) {
|
|
2433
|
+
const parameters = toolParameters(tool);
|
|
2434
|
+
return [
|
|
2435
|
+
toolSignature(tool),
|
|
2436
|
+
line(tool.description) || "No description.",
|
|
2437
|
+
...parameters.length ? ["Parameters:"] : [],
|
|
2438
|
+
...parameters.slice(0, 40).map(
|
|
2439
|
+
(parameter) => `${parameter.name}: ${parameter.type} (${parameter.required ? "required" : "optional"})${parameter.description ? `
|
|
2440
|
+
${parameter.description}` : ""}`
|
|
2441
|
+
),
|
|
2442
|
+
...parameters.length > 40 ? [`\u2026 ${parameters.length - 40} more parameters`] : [],
|
|
2443
|
+
"Types are summaries; the full schema may impose additional constraints."
|
|
2444
|
+
].join("\n\n");
|
|
2445
|
+
}
|
|
2446
|
+
function inspectServer(name, config, status) {
|
|
2447
|
+
return [
|
|
2448
|
+
status,
|
|
2449
|
+
`Server: ${name}`,
|
|
2450
|
+
`Transport: ${config.command ? "stdio" : "HTTP"}`,
|
|
2451
|
+
`Protocol: ${config.protocol ?? "auto"}`,
|
|
2452
|
+
`OAuth: ${config.oauth ? "enabled" : "disabled"}`,
|
|
2453
|
+
`Timeout: ${config.timeoutMs ? `${config.timeoutMs} ms` : "default"}`,
|
|
2454
|
+
...config.command ? [
|
|
2455
|
+
"Command and working directory: hidden",
|
|
2456
|
+
`Arguments: ${config.args?.length ?? 0} (values hidden)`,
|
|
2457
|
+
`Environment overrides: ${Object.keys(config.env ?? {}).length} (names and values hidden)`
|
|
2458
|
+
] : [
|
|
2459
|
+
"URL: hidden",
|
|
2460
|
+
`Headers: ${Object.keys(config.headers ?? {}).length} (names and values hidden)`
|
|
2461
|
+
],
|
|
2462
|
+
`Include tools: ${config.includeTools ? config.includeTools.map(line).join(", ") : "all"}`,
|
|
2463
|
+
`Exclude tools: ${config.excludeTools?.map(line).join(", ") || "none"}`
|
|
2464
|
+
].join("\n");
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2357
2467
|
// src/auth.ts
|
|
2358
2468
|
init_config();
|
|
2359
2469
|
init_diagnostics();
|
|
@@ -2531,7 +2641,7 @@ async function authenticate(url, open, signal, store) {
|
|
|
2531
2641
|
|
|
2532
2642
|
// src/runtime.ts
|
|
2533
2643
|
init_config();
|
|
2534
|
-
import { mkdir, readFile as readFile2, rename, stat, writeFile } from "node:fs/promises";
|
|
2644
|
+
import { mkdir, readFile as readFile2, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2535
2645
|
import { join as join2 } from "node:path";
|
|
2536
2646
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2537
2647
|
import {
|
|
@@ -2565,11 +2675,18 @@ function waitFor(promise, signal) {
|
|
|
2565
2675
|
if (signal.aborted) abort();
|
|
2566
2676
|
});
|
|
2567
2677
|
}
|
|
2568
|
-
var connectSdk = async (_name, config, signal) => {
|
|
2678
|
+
var connectSdk = async (_name, config, signal, onToolsChanged) => {
|
|
2569
2679
|
const timeout = config.timeoutMs ?? 15e3;
|
|
2570
2680
|
const client = new Client(
|
|
2571
2681
|
{ name: "pi-mcp-client", version: "0.1.0" },
|
|
2572
2682
|
{
|
|
2683
|
+
listChanged: {
|
|
2684
|
+
tools: {
|
|
2685
|
+
autoRefresh: false,
|
|
2686
|
+
debounceMs: 0,
|
|
2687
|
+
onChanged: () => onToolsChanged?.()
|
|
2688
|
+
}
|
|
2689
|
+
},
|
|
2573
2690
|
versionNegotiation: {
|
|
2574
2691
|
mode: config.protocol ?? "auto",
|
|
2575
2692
|
probe: { timeoutMs: timeout }
|
|
@@ -2585,21 +2702,39 @@ var connectSdk = async (_name, config, signal) => {
|
|
|
2585
2702
|
}) : new StreamableHTTPClientTransport(new URL(config.url), {
|
|
2586
2703
|
requestInit: { headers: config.headers },
|
|
2587
2704
|
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
|
-
|
|
2705
|
+
// Bound HTTP responses (including OAuth), but not established SSE streams.
|
|
2706
|
+
// The SDK bounds ordinary MCP requests with their request timeout.
|
|
2707
|
+
fetch: async (input, init) => {
|
|
2708
|
+
const deadline = new AbortController();
|
|
2709
|
+
const timer = setTimeout(
|
|
2710
|
+
() => deadline.abort(new Error("HTTP response timed out.")),
|
|
2711
|
+
timeout
|
|
2712
|
+
);
|
|
2713
|
+
timer.unref();
|
|
2714
|
+
try {
|
|
2715
|
+
const response = await fetch(input, {
|
|
2716
|
+
...init,
|
|
2717
|
+
signal: AbortSignal.any([
|
|
2718
|
+
signal,
|
|
2719
|
+
deadline.signal,
|
|
2720
|
+
...init?.signal ? [init.signal] : []
|
|
2721
|
+
])
|
|
2722
|
+
});
|
|
2723
|
+
if (response.headers.get("content-type")?.split(";")[0].trim() === "text/event-stream")
|
|
2724
|
+
clearTimeout(timer);
|
|
2725
|
+
return response;
|
|
2726
|
+
} catch (error) {
|
|
2727
|
+
clearTimeout(timer);
|
|
2728
|
+
throw error;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2599
2731
|
});
|
|
2732
|
+
if (transport instanceof StdioClientTransport)
|
|
2733
|
+
transport.stderr?.on("data", () => {
|
|
2734
|
+
});
|
|
2600
2735
|
try {
|
|
2601
2736
|
await waitFor(
|
|
2602
|
-
client.connect(transport),
|
|
2737
|
+
client.connect(transport, { signal, timeout }),
|
|
2603
2738
|
AbortSignal.any([signal, AbortSignal.timeout(timeout)])
|
|
2604
2739
|
);
|
|
2605
2740
|
signal.throwIfAborted();
|
|
@@ -2621,6 +2756,7 @@ var McpRuntime = class {
|
|
|
2621
2756
|
}
|
|
2622
2757
|
states = /* @__PURE__ */ new Map();
|
|
2623
2758
|
lifetime = new AbortController();
|
|
2759
|
+
closing;
|
|
2624
2760
|
identity(name) {
|
|
2625
2761
|
const config = this.definition(name);
|
|
2626
2762
|
let resolved;
|
|
@@ -2643,18 +2779,21 @@ var McpRuntime = class {
|
|
|
2643
2779
|
definition(name) {
|
|
2644
2780
|
const config = Object.hasOwn(this.config, name) ? this.config[name] : void 0;
|
|
2645
2781
|
if (!config || config.disabled)
|
|
2646
|
-
throw new ToolContractError(
|
|
2782
|
+
throw new ToolContractError(
|
|
2783
|
+
"MCP server is not configured or is disabled."
|
|
2784
|
+
);
|
|
2647
2785
|
return config;
|
|
2648
2786
|
}
|
|
2649
2787
|
state(name) {
|
|
2650
2788
|
let state = this.states.get(name);
|
|
2651
2789
|
if (!state) {
|
|
2652
|
-
state = {};
|
|
2790
|
+
state = { catalogGeneration: 0 };
|
|
2653
2791
|
this.states.set(name, state);
|
|
2654
2792
|
}
|
|
2655
2793
|
return state;
|
|
2656
2794
|
}
|
|
2657
2795
|
async client(name) {
|
|
2796
|
+
if (this.closing) throw new Error("MCP session ended.");
|
|
2658
2797
|
this.lifetime.signal.throwIfAborted();
|
|
2659
2798
|
const state = this.state(name);
|
|
2660
2799
|
const identity = this.identity(name);
|
|
@@ -2666,6 +2805,8 @@ var McpRuntime = class {
|
|
|
2666
2805
|
if (state.connecting) return state.connecting;
|
|
2667
2806
|
const config = resolveServer(this.definition(name), this.cwd);
|
|
2668
2807
|
state.connectionIdentity = identity;
|
|
2808
|
+
const token = {};
|
|
2809
|
+
state.connectionToken = token;
|
|
2669
2810
|
state.connecting = (async () => {
|
|
2670
2811
|
const { client, transport } = await this.connect(
|
|
2671
2812
|
name,
|
|
@@ -2675,9 +2816,28 @@ var McpRuntime = class {
|
|
|
2675
2816
|
this.cwd,
|
|
2676
2817
|
this.lifetime.signal
|
|
2677
2818
|
),
|
|
2678
|
-
this.lifetime.signal
|
|
2819
|
+
this.lifetime.signal,
|
|
2820
|
+
() => {
|
|
2821
|
+
if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token)
|
|
2822
|
+
return;
|
|
2823
|
+
state.catalogGeneration++;
|
|
2824
|
+
state.catalogDirty = true;
|
|
2825
|
+
state.tools = void 0;
|
|
2826
|
+
state.warnings = void 0;
|
|
2827
|
+
const path = join2(this.cacheDir, `${identity}.json`);
|
|
2828
|
+
state.invalidating = withFileMutationQueue(
|
|
2829
|
+
path,
|
|
2830
|
+
() => rm(path, { force: true })
|
|
2831
|
+
).catch(() => {
|
|
2832
|
+
state.warnings = [
|
|
2833
|
+
`${name}: stale catalog cache could not be removed.`
|
|
2834
|
+
];
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2679
2837
|
);
|
|
2680
|
-
if (this.lifetime.signal.aborted) {
|
|
2838
|
+
if (this.closing || this.lifetime.signal.aborted) {
|
|
2839
|
+
await client.autoOpenedSubscription?.close().catch(() => {
|
|
2840
|
+
});
|
|
2681
2841
|
await client.close();
|
|
2682
2842
|
throw new Error("MCP session ended.");
|
|
2683
2843
|
}
|
|
@@ -2688,10 +2848,12 @@ var McpRuntime = class {
|
|
|
2688
2848
|
if (state.client === client) {
|
|
2689
2849
|
state.client = void 0;
|
|
2690
2850
|
state.transport = void 0;
|
|
2851
|
+
state.connectionToken = void 0;
|
|
2691
2852
|
}
|
|
2692
2853
|
};
|
|
2693
2854
|
return client;
|
|
2694
2855
|
})().catch((error) => {
|
|
2856
|
+
if (state.connectionToken === token) state.connectionToken = void 0;
|
|
2695
2857
|
throw new DiagnosticError(
|
|
2696
2858
|
diagnose(error, {
|
|
2697
2859
|
server: name,
|
|
@@ -2707,10 +2869,13 @@ var McpRuntime = class {
|
|
|
2707
2869
|
}
|
|
2708
2870
|
async catalog(name, signal, refresh = false) {
|
|
2709
2871
|
signal?.throwIfAborted();
|
|
2872
|
+
if (this.closing) throw new Error("MCP session ended.");
|
|
2710
2873
|
this.lifetime.signal.throwIfAborted();
|
|
2711
2874
|
const state = this.state(name);
|
|
2712
2875
|
const identity = this.identity(name);
|
|
2713
|
-
|
|
2876
|
+
refresh ||= !!state.catalogDirty;
|
|
2877
|
+
if (!refresh && state.tools && state.identity === identity)
|
|
2878
|
+
return state.tools;
|
|
2714
2879
|
if (refresh && state.listing && !state.listingLive) {
|
|
2715
2880
|
await waitFor(state.listing, signal);
|
|
2716
2881
|
return this.catalog(name, signal, true);
|
|
@@ -2718,44 +2883,59 @@ var McpRuntime = class {
|
|
|
2718
2883
|
if (!state.listing) {
|
|
2719
2884
|
state.listingLive = refresh;
|
|
2720
2885
|
state.listing = (async () => {
|
|
2721
|
-
|
|
2722
|
-
const
|
|
2723
|
-
if (
|
|
2724
|
-
|
|
2725
|
-
state.
|
|
2726
|
-
|
|
2886
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
2887
|
+
const generation = state.catalogGeneration;
|
|
2888
|
+
if (!refresh && !state.catalogDirty) {
|
|
2889
|
+
const cached = await this.readCache(name, identity);
|
|
2890
|
+
if (cached && generation === state.catalogGeneration) {
|
|
2891
|
+
state.tools = cached;
|
|
2892
|
+
state.identity = identity;
|
|
2893
|
+
return cached;
|
|
2894
|
+
}
|
|
2727
2895
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2896
|
+
const client = await this.client(name);
|
|
2897
|
+
const listed = await client.listTools(void 0, {
|
|
2898
|
+
signal: this.lifetime.signal,
|
|
2899
|
+
timeout: this.definition(name).timeoutMs ?? 15e3
|
|
2900
|
+
});
|
|
2901
|
+
const tools = [];
|
|
2902
|
+
const warnings = [];
|
|
2903
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2904
|
+
for (const tool of listed.tools) {
|
|
2905
|
+
if (!allowed(tool.name, this.definition(name))) continue;
|
|
2906
|
+
try {
|
|
2907
|
+
const prepared = prepareTool(name, identity, tool);
|
|
2908
|
+
if (seen.has(prepared.nativeName))
|
|
2909
|
+
throw new Error("Duplicate tool name.");
|
|
2910
|
+
seen.add(prepared.nativeName);
|
|
2911
|
+
tools.push(prepared);
|
|
2912
|
+
} catch {
|
|
2913
|
+
warnings.push(
|
|
2914
|
+
`${name}: skipped an invalid, duplicate, or unsupported tool schema.`
|
|
2915
|
+
);
|
|
2916
|
+
}
|
|
2748
2917
|
}
|
|
2918
|
+
this.lifetime.signal.throwIfAborted();
|
|
2919
|
+
if (generation !== state.catalogGeneration) continue;
|
|
2920
|
+
await state.invalidating;
|
|
2921
|
+
await this.writeCache(
|
|
2922
|
+
identity,
|
|
2923
|
+
tools,
|
|
2924
|
+
() => generation === state.catalogGeneration
|
|
2925
|
+
).catch(() => {
|
|
2926
|
+
warnings.push(`${name}: catalog cache could not be saved.`);
|
|
2927
|
+
});
|
|
2928
|
+
if (generation !== state.catalogGeneration) continue;
|
|
2929
|
+
state.tools = tools;
|
|
2930
|
+
state.identity = identity;
|
|
2931
|
+
state.catalogDirty = false;
|
|
2932
|
+
state.error = void 0;
|
|
2933
|
+
state.warnings = warnings;
|
|
2934
|
+
return tools;
|
|
2749
2935
|
}
|
|
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;
|
|
2936
|
+
throw new ToolContractError(
|
|
2937
|
+
"MCP tool catalog kept changing. Search again."
|
|
2938
|
+
);
|
|
2759
2939
|
})().catch((error) => {
|
|
2760
2940
|
state.error = this.failure(name, error);
|
|
2761
2941
|
throw new DiagnosticError(state.error);
|
|
@@ -2812,7 +2992,10 @@ var McpRuntime = class {
|
|
|
2812
2992
|
return await client.callTool(
|
|
2813
2993
|
{ name: tool.name, arguments: args },
|
|
2814
2994
|
{
|
|
2815
|
-
signal: AbortSignal.any([
|
|
2995
|
+
signal: AbortSignal.any([
|
|
2996
|
+
this.lifetime.signal,
|
|
2997
|
+
...signal ? [signal] : []
|
|
2998
|
+
]),
|
|
2816
2999
|
timeout: config.timeoutMs ?? 3e4,
|
|
2817
3000
|
onprogress: (event) => progress?.(
|
|
2818
3001
|
event.message ?? `${event.progress}${event.total === void 0 ? "" : `/${event.total}`}`
|
|
@@ -2835,18 +3018,38 @@ var McpRuntime = class {
|
|
|
2835
3018
|
const state = this.state(name);
|
|
2836
3019
|
if (state.connecting || state.listing)
|
|
2837
3020
|
throw failure("busy", { server: name, operation: "reconnect" });
|
|
3021
|
+
await state.client?.autoOpenedSubscription?.close();
|
|
2838
3022
|
await state.client?.close();
|
|
2839
3023
|
state.client = void 0;
|
|
2840
3024
|
await this.catalog(name, void 0, true);
|
|
2841
3025
|
}
|
|
2842
|
-
|
|
3026
|
+
serverStatuses() {
|
|
2843
3027
|
return Object.entries(this.config).map(([name, config]) => {
|
|
2844
3028
|
const state = this.states.get(name);
|
|
2845
|
-
|
|
2846
|
-
|
|
3029
|
+
return {
|
|
3030
|
+
name,
|
|
3031
|
+
state: config.disabled ? "disabled" : state?.connecting || state?.listing ? "connecting" : state?.error ? "failed" : state?.client ? "connected" : "disconnected",
|
|
3032
|
+
catalogSize: state?.tools?.length,
|
|
3033
|
+
error: state?.error
|
|
3034
|
+
};
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
status(server) {
|
|
3038
|
+
return this.serverStatuses().filter(({ name }) => server === void 0 || name === server).map((row) => {
|
|
3039
|
+
const status = row.state === "failed" && row.error ? formatDiagnostic({ ...row.error, server: void 0 }) : row.state;
|
|
3040
|
+
return `${row.name}: ${status} \xB7 ${row.catalogSize ?? "unknown"} catalog tools`;
|
|
2847
3041
|
}).join("\n") || "No MCP servers configured.";
|
|
2848
3042
|
}
|
|
2849
|
-
|
|
3043
|
+
close() {
|
|
3044
|
+
return this.closing ??= this.shutdown();
|
|
3045
|
+
}
|
|
3046
|
+
async shutdown() {
|
|
3047
|
+
await Promise.all(
|
|
3048
|
+
[...this.states.values()].map(
|
|
3049
|
+
(state) => state.client?.autoOpenedSubscription?.close().catch(() => {
|
|
3050
|
+
})
|
|
3051
|
+
)
|
|
3052
|
+
);
|
|
2850
3053
|
this.lifetime.abort(new Error("MCP session ended."));
|
|
2851
3054
|
await Promise.all(
|
|
2852
3055
|
[...this.states.values()].map(async (state) => {
|
|
@@ -2856,6 +3059,7 @@ var McpRuntime = class {
|
|
|
2856
3059
|
});
|
|
2857
3060
|
await state.listing?.catch(() => {
|
|
2858
3061
|
});
|
|
3062
|
+
await state.invalidating;
|
|
2859
3063
|
})
|
|
2860
3064
|
);
|
|
2861
3065
|
}
|
|
@@ -2871,7 +3075,8 @@ var McpRuntime = class {
|
|
|
2871
3075
|
try {
|
|
2872
3076
|
const path = join2(this.cacheDir, `${identity}.json`);
|
|
2873
3077
|
const info = await stat(path);
|
|
2874
|
-
if (info.size > 4 * 1024 * 1024 || Date.now() - info.mtimeMs > 864e5)
|
|
3078
|
+
if (info.size > 4 * 1024 * 1024 || Date.now() - info.mtimeMs > 864e5)
|
|
3079
|
+
return;
|
|
2875
3080
|
const data = JSON.parse(await readFile2(path, "utf8"));
|
|
2876
3081
|
if (!Array.isArray(data) || data.length > 1e4) return;
|
|
2877
3082
|
return data.filter((tool) => allowed(tool.name, this.definition(name))).map((tool) => prepareTool(name, identity, tool));
|
|
@@ -2879,7 +3084,7 @@ var McpRuntime = class {
|
|
|
2879
3084
|
return;
|
|
2880
3085
|
}
|
|
2881
3086
|
}
|
|
2882
|
-
async writeCache(identity, tools) {
|
|
3087
|
+
async writeCache(identity, tools, isCurrent) {
|
|
2883
3088
|
const text = JSON.stringify(
|
|
2884
3089
|
tools.map(({ name, description, inputSchema }) => ({
|
|
2885
3090
|
name,
|
|
@@ -2891,9 +3096,11 @@ var McpRuntime = class {
|
|
|
2891
3096
|
await mkdir(this.cacheDir, { recursive: true, mode: 448 });
|
|
2892
3097
|
const path = join2(this.cacheDir, `${identity}.json`);
|
|
2893
3098
|
await withFileMutationQueue(path, async () => {
|
|
3099
|
+
if (!isCurrent()) return;
|
|
2894
3100
|
const temp = `${path}.${randomUUID2()}.tmp`;
|
|
2895
3101
|
await writeFile(temp, text, { mode: 384 });
|
|
2896
|
-
await rename(temp, path);
|
|
3102
|
+
if (isCurrent()) await rename(temp, path);
|
|
3103
|
+
else await rm(temp, { force: true });
|
|
2897
3104
|
});
|
|
2898
3105
|
}
|
|
2899
3106
|
};
|
|
@@ -3036,7 +3243,7 @@ Full MCP result: ${path}`;
|
|
|
3036
3243
|
|
|
3037
3244
|
// src/render.ts
|
|
3038
3245
|
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
3039
|
-
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3246
|
+
import { Text, truncateToWidth as truncateToWidth2 } from "@earendil-works/pi-tui";
|
|
3040
3247
|
init_config();
|
|
3041
3248
|
var states = {
|
|
3042
3249
|
queued: { glyph: "\u25CF", color: "dim" },
|
|
@@ -3053,10 +3260,10 @@ function renderCall(title, args, theme, expanded) {
|
|
|
3053
3260
|
if (width <= 0) return [];
|
|
3054
3261
|
const text = theme.fg("toolTitle", theme.bold(line(title))) + (preview ? theme.fg("dim", ` ${preview}`) : "");
|
|
3055
3262
|
if (expanded)
|
|
3056
|
-
return new Text(text, 0, 0).render(width).map((row) =>
|
|
3263
|
+
return new Text(text, 0, 0).render(width).map((row) => truncateToWidth2(row, width));
|
|
3057
3264
|
const key = keyText("app.tools.expand");
|
|
3058
3265
|
return [
|
|
3059
|
-
|
|
3266
|
+
truncateToWidth2(
|
|
3060
3267
|
text + (key ? theme.fg("muted", ` (${key} to expand)`) : ""),
|
|
3061
3268
|
width
|
|
3062
3269
|
)
|
|
@@ -3083,23 +3290,23 @@ function renderResult(result, options, theme, isError) {
|
|
|
3083
3290
|
const lines = rows.flatMap((row) => {
|
|
3084
3291
|
const status = states[row.state] ?? states.failed;
|
|
3085
3292
|
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) =>
|
|
3293
|
+
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
3294
|
if (options.expanded && row.description)
|
|
3088
|
-
rendered.push(
|
|
3295
|
+
rendered.push(truncateToWidth2(
|
|
3089
3296
|
theme.fg("dim", ` ${line(row.description)}`),
|
|
3090
3297
|
width
|
|
3091
3298
|
));
|
|
3092
3299
|
return rendered;
|
|
3093
3300
|
});
|
|
3094
3301
|
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) =>
|
|
3302
|
+
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
3303
|
if (options.expanded && !options.isPartial && text && !details?.searchNotes)
|
|
3097
3304
|
lines.push(
|
|
3098
|
-
...new Text(text, 0, 0).render(width).map((x) =>
|
|
3305
|
+
...new Text(text, 0, 0).render(width).map((x) => truncateToWidth2(x, width))
|
|
3099
3306
|
);
|
|
3100
3307
|
if (!options.expanded && details?.fullOutputPath)
|
|
3101
3308
|
lines.push(
|
|
3102
|
-
|
|
3309
|
+
truncateToWidth2(
|
|
3103
3310
|
theme.fg("dim", `Full result: ${details.fullOutputPath}`),
|
|
3104
3311
|
width
|
|
3105
3312
|
)
|
|
@@ -3132,6 +3339,7 @@ function mcpClient(pi, options = {}) {
|
|
|
3132
3339
|
let runtime;
|
|
3133
3340
|
let config = {};
|
|
3134
3341
|
let configError;
|
|
3342
|
+
let sessionGeneration = 0;
|
|
3135
3343
|
const exposure = new Exposure(pi, registerNative);
|
|
3136
3344
|
const current = () => {
|
|
3137
3345
|
if (!runtime)
|
|
@@ -3192,28 +3400,54 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
3192
3400
|
});
|
|
3193
3401
|
exposure.restore(tools);
|
|
3194
3402
|
};
|
|
3403
|
+
async function reloadConfiguration(ctx) {
|
|
3404
|
+
const generation = sessionGeneration;
|
|
3405
|
+
ctx.signal?.throwIfAborted();
|
|
3406
|
+
const nextConfig = await loadConfig(agentDir, ctx.cwd, ctx.isProjectTrusted());
|
|
3407
|
+
ctx.signal?.throwIfAborted();
|
|
3408
|
+
if (generation !== sessionGeneration)
|
|
3409
|
+
throw new CommandUsageError("The Pi session changed during configuration reload.");
|
|
3410
|
+
for (const definition of Object.values(nextConfig)) {
|
|
3411
|
+
if (!definition.disabled) resolveServer(definition, ctx.cwd);
|
|
3412
|
+
}
|
|
3413
|
+
const next = new McpRuntime(
|
|
3414
|
+
nextConfig,
|
|
3415
|
+
ctx.cwd,
|
|
3416
|
+
join4(agentDir, "cache", "pi-mcp-client")
|
|
3417
|
+
);
|
|
3418
|
+
const active = new Set(pi.getActiveTools());
|
|
3419
|
+
const retained = [...exposure.definitions.values()].filter((tool) => {
|
|
3420
|
+
try {
|
|
3421
|
+
return active.has(tool.nativeName) && next.identity(tool.server) === tool.identity && allowed(tool.name, nextConfig[tool.server]);
|
|
3422
|
+
} catch {
|
|
3423
|
+
return false;
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
3426
|
+
const old = runtime;
|
|
3427
|
+
config = nextConfig;
|
|
3428
|
+
configError = void 0;
|
|
3429
|
+
runtime = next;
|
|
3430
|
+
exposure.restore(retained);
|
|
3431
|
+
await old?.close();
|
|
3432
|
+
}
|
|
3195
3433
|
pi.on("session_start", async (_event, ctx) => {
|
|
3434
|
+
sessionGeneration++;
|
|
3196
3435
|
await runtime?.close();
|
|
3197
3436
|
runtime = void 0;
|
|
3198
3437
|
config = {};
|
|
3199
3438
|
configError = void 0;
|
|
3200
3439
|
try {
|
|
3201
3440
|
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
|
-
);
|
|
3441
|
+
runtime = new McpRuntime(config, ctx.cwd, join4(agentDir, "cache", "pi-mcp-client"));
|
|
3207
3442
|
} catch (error) {
|
|
3208
|
-
configError = new DiagnosticError(
|
|
3209
|
-
diagnose(error, { operation: "configuration" })
|
|
3210
|
-
);
|
|
3443
|
+
configError = new DiagnosticError(diagnose(error, { operation: "configuration" }));
|
|
3211
3444
|
if (ctx.hasUI) ctx.ui.notify(configError.message, "error");
|
|
3212
3445
|
}
|
|
3213
3446
|
restore(ctx);
|
|
3214
3447
|
});
|
|
3215
3448
|
pi.on("session_tree", (_event, ctx) => restore(ctx));
|
|
3216
3449
|
pi.on("session_shutdown", async () => {
|
|
3450
|
+
sessionGeneration++;
|
|
3217
3451
|
const old = runtime;
|
|
3218
3452
|
runtime = void 0;
|
|
3219
3453
|
await old?.close();
|
|
@@ -3247,17 +3481,21 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3247
3481
|
maxLength: 500,
|
|
3248
3482
|
description: "One focused capability or exact server.tool name, for example linear.list_teams. Do not enumerate every capability of a server."
|
|
3249
3483
|
}),
|
|
3250
|
-
server: Type.Optional(
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3484
|
+
server: Type.Optional(
|
|
3485
|
+
Type.String({
|
|
3486
|
+
minLength: 1,
|
|
3487
|
+
maxLength: 80,
|
|
3488
|
+
description: "Restrict discovery to this configured MCP server."
|
|
3489
|
+
})
|
|
3490
|
+
),
|
|
3491
|
+
limit: Type.Optional(
|
|
3492
|
+
Type.Integer({
|
|
3493
|
+
minimum: 1,
|
|
3494
|
+
maximum: MAX_SEARCH_LIMIT,
|
|
3495
|
+
default: DEFAULT_SEARCH_LIMIT,
|
|
3496
|
+
description: `Maximum number of tools to load: 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
|
+
})
|
|
3498
|
+
)
|
|
3261
3499
|
},
|
|
3262
3500
|
{ additionalProperties: false }
|
|
3263
3501
|
),
|
|
@@ -3282,12 +3520,7 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3282
3520
|
(signal ?? ctx.signal)?.throwIfAborted();
|
|
3283
3521
|
if (runtime !== activeRuntime)
|
|
3284
3522
|
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
|
-
);
|
|
3523
|
+
const matches2 = searchTools(discovery.tools, args.query, args.server, args.limit);
|
|
3291
3524
|
const { loaded, added, rejected } = exposure.load(matches2);
|
|
3292
3525
|
const messages2 = loaded.map(
|
|
3293
3526
|
(tool) => `${added.includes(tool.nativeName) ? "Loaded" : "Already loaded"}: ${tool.nativeName} \u2014 ${line(tool.description).slice(0, 180)}`
|
|
@@ -3344,34 +3577,81 @@ Use mcp_search to load relevant tools, then call them directly. Loaded tools rem
|
|
|
3344
3577
|
}
|
|
3345
3578
|
});
|
|
3346
3579
|
pi.registerCommand("mcp", {
|
|
3347
|
-
description: "
|
|
3580
|
+
description: "Manage MCP servers: list, status, reload, inspect|tools|auth|reconnect|refresh <server>",
|
|
3348
3581
|
getArgumentCompletions(prefix) {
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
)
|
|
3354
|
-
|
|
3355
|
-
|
|
3582
|
+
const serverActions = ["inspect", "tools", "auth", "reconnect", "refresh"];
|
|
3583
|
+
const input = prefix.trimStart();
|
|
3584
|
+
const match = /^(\S+)\s+(.*)$/s.exec(input);
|
|
3585
|
+
if (!match) {
|
|
3586
|
+
return ["list", "status", "reload", ...serverActions].filter((action2) => action2.startsWith(input)).map((action2) => ({ value: action2, label: action2 }));
|
|
3587
|
+
}
|
|
3588
|
+
const [, action, partialServer] = match;
|
|
3589
|
+
if (!serverActions.includes(action) || /\s/.test(partialServer)) return [];
|
|
3590
|
+
return Object.keys(config).filter(
|
|
3591
|
+
(name) => name.startsWith(partialServer) && (action === "inspect" || !config[name].disabled)
|
|
3592
|
+
).sort().map((name) => ({ value: `${action} ${name}`, label: name }));
|
|
3356
3593
|
},
|
|
3357
3594
|
async handler(args, ctx) {
|
|
3358
3595
|
await ctx.waitForIdle();
|
|
3359
3596
|
const [action = "status", server, ...extra] = args.trim().split(/\s+/).filter(Boolean);
|
|
3360
3597
|
try {
|
|
3361
|
-
if (action === "
|
|
3362
|
-
|
|
3598
|
+
if (action === "reload" && !server) {
|
|
3599
|
+
await reloadConfiguration(ctx);
|
|
3363
3600
|
if (ctx.hasUI)
|
|
3364
3601
|
ctx.ui.notify(
|
|
3365
|
-
|
|
3366
|
-
${loaded} native MCP tools loaded.`,
|
|
3602
|
+
"\u2714\uFE0E MCP configuration reloaded. Connections reopen on demand; tools from changed or removed servers are no longer active.",
|
|
3367
3603
|
"info"
|
|
3368
3604
|
);
|
|
3369
3605
|
return;
|
|
3370
3606
|
}
|
|
3607
|
+
if (action === "inspect" && server && !extra.length && Object.hasOwn(config, server)) {
|
|
3608
|
+
if (ctx.hasUI)
|
|
3609
|
+
ctx.ui.notify(
|
|
3610
|
+
inspectServer(server, config[server], current().status(server)),
|
|
3611
|
+
"info"
|
|
3612
|
+
);
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
if ((action === "status" || action === "list") && !server) {
|
|
3616
|
+
const statuses = current().serverStatuses();
|
|
3617
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
3618
|
+
for (const name of pi.getActiveTools()) {
|
|
3619
|
+
const tool = exposure.definitions.get(name);
|
|
3620
|
+
if (tool) loaded.set(tool.server, (loaded.get(tool.server) ?? 0) + 1);
|
|
3621
|
+
}
|
|
3622
|
+
if (ctx.hasUI) ctx.ui.notify(serverMatrix(statuses, loaded), "info");
|
|
3623
|
+
return;
|
|
3624
|
+
}
|
|
3371
3625
|
if (!server || extra.length || !Object.hasOwn(config, server) || config[server].disabled)
|
|
3372
3626
|
throw new CommandUsageError(
|
|
3373
|
-
"Usage: /mcp auth|reconnect|refresh <
|
|
3627
|
+
"Usage: /mcp list|status|reload or /mcp inspect|tools|auth|reconnect|refresh <server>. Only inspect accepts a disabled server."
|
|
3374
3628
|
);
|
|
3629
|
+
if (action === "tools") {
|
|
3630
|
+
if (!ctx.hasUI)
|
|
3631
|
+
throw new CommandUsageError("Tool browsing requires an interactive UI.");
|
|
3632
|
+
const tools = await current().catalog(server, ctx.signal, true);
|
|
3633
|
+
if (!tools.length) {
|
|
3634
|
+
ctx.ui.notify(
|
|
3635
|
+
`${server}: no tools available under the configured filters.`,
|
|
3636
|
+
"info"
|
|
3637
|
+
);
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
const choices = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
3641
|
+
const columns = ctx.mode === "tui" ? process.stdout.columns || 80 : 80;
|
|
3642
|
+
const labels = choices.map((tool2, index) => toolPickerLabel(tool2, index, columns));
|
|
3643
|
+
const selected = await ctx.ui.select(
|
|
3644
|
+
`${server}: ${tools.length} tools (select to inspect; none are activated)`,
|
|
3645
|
+
labels
|
|
3646
|
+
);
|
|
3647
|
+
const tool = selected === void 0 ? void 0 : choices[labels.indexOf(selected)];
|
|
3648
|
+
if (tool)
|
|
3649
|
+
ctx.ui.notify(
|
|
3650
|
+
inspectTool(tool),
|
|
3651
|
+
"info"
|
|
3652
|
+
);
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3375
3655
|
if (action === "auth") {
|
|
3376
3656
|
if (!ctx.hasUI)
|
|
3377
3657
|
throw new CommandUsageError(
|
|
@@ -3416,27 +3696,27 @@ ${target}`, "info");
|
|
|
3416
3696
|
} else await authenticate(url, open, ctx.signal);
|
|
3417
3697
|
await current().reconnect(server);
|
|
3418
3698
|
} else if (action === "reconnect") await current().reconnect(server);
|
|
3419
|
-
else if (action === "refresh")
|
|
3420
|
-
await current().catalog(server, ctx.signal, true);
|
|
3699
|
+
else if (action === "refresh") await current().catalog(server, ctx.signal, true);
|
|
3421
3700
|
else
|
|
3422
3701
|
throw new CommandUsageError(
|
|
3423
|
-
"Unknown MCP command. Use /mcp auth|reconnect|refresh <server>."
|
|
3702
|
+
"Unknown MCP command. Use /mcp list|status|reload or /mcp inspect|tools|auth|reconnect|refresh <server>."
|
|
3424
3703
|
);
|
|
3425
3704
|
if (ctx.hasUI)
|
|
3426
3705
|
ctx.ui.notify(
|
|
3427
|
-
|
|
3706
|
+
`\u2714\uFE0E ${server}: ${action} complete. Updated tools are available for the assistant to discover.`,
|
|
3428
3707
|
"info"
|
|
3429
3708
|
);
|
|
3430
3709
|
} catch (error) {
|
|
3431
3710
|
const message = error instanceof CommandUsageError ? error.message : formatDiagnostic(
|
|
3432
3711
|
diagnose(error, {
|
|
3433
3712
|
server,
|
|
3434
|
-
operation: action === "auth" ? "auth" : action === "refresh" ? "refresh" : "reconnect",
|
|
3713
|
+
operation: action === "reload" || action === "inspect" ? "configuration" : action === "tools" ? "search" : action === "auth" ? "auth" : action === "refresh" ? "refresh" : "reconnect",
|
|
3435
3714
|
oauth: config[server]?.oauth,
|
|
3436
3715
|
signal: ctx.signal
|
|
3437
3716
|
})
|
|
3438
3717
|
);
|
|
3439
3718
|
if (ctx.hasUI) ctx.ui.notify(message, "error");
|
|
3719
|
+
else throw new Error(message);
|
|
3440
3720
|
}
|
|
3441
3721
|
}
|
|
3442
3722
|
});
|