lsd-pi 1.3.9 → 1.3.10
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/dist/resources/extensions/mcp-client/index.js +191 -83
- package/dist/resources/extensions/mcp-client/mcp-manager-component.js +220 -0
- package/dist/resources/extensions/slash-commands/plan.js +67 -13
- package/dist/resources/extensions/subagent/agents.js +7 -0
- package/dist/resources/extensions/subagent/index.js +25 -8
- package/dist/resources/extensions/subagent/model-resolution.js +1 -0
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +104 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +39 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +135 -18
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +21 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +146 -8
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +51 -13
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +75 -4
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +31 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +129 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +158 -18
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +163 -9
- package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +60 -13
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +86 -5
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/mcp-client/index.ts +212 -90
- package/src/resources/extensions/mcp-client/mcp-manager-component.ts +256 -0
- package/src/resources/extensions/mcp-client/tests/mcp-manager-component.test.ts +141 -0
- package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +18 -2
- package/src/resources/extensions/slash-commands/plan.ts +70 -13
- package/src/resources/extensions/subagent/agents.ts +9 -0
- package/src/resources/extensions/subagent/index.ts +30 -8
- package/src/resources/extensions/subagent/model-resolution.ts +1 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Client Extension — Native MCP server integration for pi
|
|
3
3
|
*
|
|
4
|
-
* Provides on-demand access to MCP servers configured in
|
|
5
|
-
* (.mcp.json, .lsd/mcp.json, with legacy .gsd/mcp.json fallback) using the
|
|
4
|
+
* Provides on-demand access to MCP servers configured in global (~/.lsd/mcp.json)
|
|
5
|
+
* and project files (.mcp.json, .lsd/mcp.json, with legacy .gsd/mcp.json fallback) using the
|
|
6
6
|
* @modelcontextprotocol/sdk Client directly — no external CLI dependency
|
|
7
7
|
* required.
|
|
8
8
|
*
|
|
@@ -18,7 +18,9 @@ import { Client } from "@modelcontextprotocol/sdk/client";
|
|
|
18
18
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
19
19
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
20
20
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
21
22
|
import { basename, dirname, join } from "node:path";
|
|
23
|
+
import { McpManagerComponent } from "./mcp-manager-component.js";
|
|
22
24
|
// ─── Connection Manager ───────────────────────────────────────────────────────
|
|
23
25
|
const connections = new Map();
|
|
24
26
|
let configCache = null;
|
|
@@ -99,6 +101,7 @@ function readConfigs() {
|
|
|
99
101
|
join(process.cwd(), ".mcp.json"),
|
|
100
102
|
join(process.cwd(), ".lsd", "mcp.json"),
|
|
101
103
|
join(process.cwd(), ".gsd", "mcp.json"),
|
|
104
|
+
join(homedir(), ".lsd", "mcp.json"),
|
|
102
105
|
];
|
|
103
106
|
for (const configPath of configPaths) {
|
|
104
107
|
try {
|
|
@@ -177,8 +180,6 @@ async function getOrConnect(name, signal) {
|
|
|
177
180
|
throw new Error(`Unknown MCP server: "${name}". Use mcp_servers to list available servers.`);
|
|
178
181
|
if (!config.enabled)
|
|
179
182
|
throw new Error(`Server "${config.name}" is disabled. Use /mcp enable ${config.name}.`);
|
|
180
|
-
// Always use config.name as the canonical cache key so that variant
|
|
181
|
-
// casing / whitespace still hits the same connection.
|
|
182
183
|
const existing = connections.get(config.name);
|
|
183
184
|
if (existing)
|
|
184
185
|
return existing.client;
|
|
@@ -206,6 +207,97 @@ async function getOrConnect(name, signal) {
|
|
|
206
207
|
connections.set(config.name, { client, transport });
|
|
207
208
|
return client;
|
|
208
209
|
}
|
|
210
|
+
function mapToolSchemas(tools) {
|
|
211
|
+
return tools.map((tool) => ({
|
|
212
|
+
name: tool.name,
|
|
213
|
+
description: tool.description ?? "",
|
|
214
|
+
inputSchema: tool.inputSchema,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
function shouldRetryMcpOperation(error) {
|
|
218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
219
|
+
if (/unknown mcp server/i.test(message))
|
|
220
|
+
return false;
|
|
221
|
+
if (/is disabled/i.test(message))
|
|
222
|
+
return false;
|
|
223
|
+
if (/unsupported transport/i.test(message))
|
|
224
|
+
return false;
|
|
225
|
+
if (/abort|cancel/i.test(message))
|
|
226
|
+
return false;
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
async function listServerTools(name, signal, options) {
|
|
230
|
+
const canonicalName = getCanonicalServerName(name);
|
|
231
|
+
if (options?.useCache !== false) {
|
|
232
|
+
const cached = toolCache.get(canonicalName);
|
|
233
|
+
if (cached) {
|
|
234
|
+
return { canonicalName, tools: cached, cached: true };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
let attempt = 0;
|
|
238
|
+
while (attempt < 2) {
|
|
239
|
+
try {
|
|
240
|
+
if (attempt === 0 && options?.forceReconnect) {
|
|
241
|
+
await closeServerConnection(canonicalName);
|
|
242
|
+
}
|
|
243
|
+
if (attempt > 0) {
|
|
244
|
+
await closeServerConnection(canonicalName);
|
|
245
|
+
}
|
|
246
|
+
const client = await getOrConnect(canonicalName, signal);
|
|
247
|
+
const result = await client.listTools(undefined, { signal, timeout: 30000 });
|
|
248
|
+
const tools = mapToolSchemas(result.tools ?? []);
|
|
249
|
+
toolCache.set(canonicalName, tools);
|
|
250
|
+
return { canonicalName, tools, cached: false };
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
attempt += 1;
|
|
254
|
+
await closeServerConnection(canonicalName);
|
|
255
|
+
if (attempt >= 2 || !shouldRetryMcpOperation(error)) {
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`Failed to list tools for ${canonicalName}`);
|
|
261
|
+
}
|
|
262
|
+
async function callServerTool(serverName, toolName, args, signal) {
|
|
263
|
+
const canonicalServer = getCanonicalServerName(serverName);
|
|
264
|
+
let attempt = 0;
|
|
265
|
+
while (attempt < 2) {
|
|
266
|
+
try {
|
|
267
|
+
if (attempt > 0) {
|
|
268
|
+
await closeServerConnection(canonicalServer);
|
|
269
|
+
}
|
|
270
|
+
const client = await getOrConnect(canonicalServer, signal);
|
|
271
|
+
const result = await client.callTool({ name: toolName, arguments: args }, undefined, { signal, timeout: 60000 });
|
|
272
|
+
return { canonicalServer, result };
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
attempt += 1;
|
|
276
|
+
await closeServerConnection(canonicalServer);
|
|
277
|
+
if (attempt >= 2 || !shouldRetryMcpOperation(error)) {
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
throw new Error(`Failed to call ${canonicalServer}.${toolName}`);
|
|
283
|
+
}
|
|
284
|
+
async function warmupServer(name, signal) {
|
|
285
|
+
try {
|
|
286
|
+
const { canonicalName, tools } = await listServerTools(name, signal, { useCache: false });
|
|
287
|
+
return {
|
|
288
|
+
name: canonicalName,
|
|
289
|
+
status: "connected",
|
|
290
|
+
toolCount: tools.length,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
return {
|
|
295
|
+
name: getCanonicalServerName(name),
|
|
296
|
+
status: "error",
|
|
297
|
+
error: error instanceof Error ? error.message : String(error),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
209
301
|
async function warmupEnabledServers() {
|
|
210
302
|
if (warmupPromise)
|
|
211
303
|
return warmupPromise;
|
|
@@ -213,31 +305,7 @@ async function warmupEnabledServers() {
|
|
|
213
305
|
const enabledServers = readConfigs().filter((server) => server.enabled);
|
|
214
306
|
if (enabledServers.length === 0)
|
|
215
307
|
return [];
|
|
216
|
-
|
|
217
|
-
const client = await getOrConnect(server.name);
|
|
218
|
-
const result = await client.listTools(undefined, { timeout: 30000 });
|
|
219
|
-
const tools = (result.tools ?? []).map((tool) => ({
|
|
220
|
-
name: tool.name,
|
|
221
|
-
description: tool.description ?? "",
|
|
222
|
-
inputSchema: tool.inputSchema,
|
|
223
|
-
}));
|
|
224
|
-
toolCache.set(server.name, tools);
|
|
225
|
-
return {
|
|
226
|
-
name: server.name,
|
|
227
|
-
status: "connected",
|
|
228
|
-
toolCount: tools.length,
|
|
229
|
-
};
|
|
230
|
-
}));
|
|
231
|
-
return results.map((result, index) => {
|
|
232
|
-
if (result.status === "fulfilled") {
|
|
233
|
-
return result.value;
|
|
234
|
-
}
|
|
235
|
-
return {
|
|
236
|
-
name: enabledServers[index]?.name ?? `server-${index + 1}`,
|
|
237
|
-
status: "error",
|
|
238
|
-
error: result.reason instanceof Error ? result.reason.message : String(result.reason),
|
|
239
|
-
};
|
|
240
|
-
});
|
|
308
|
+
return Promise.all(enabledServers.map((server) => warmupServer(server.name)));
|
|
241
309
|
})();
|
|
242
310
|
try {
|
|
243
311
|
return await warmupPromise;
|
|
@@ -264,10 +332,41 @@ async function reloadMcpState() {
|
|
|
264
332
|
await closeAll();
|
|
265
333
|
configCache = null;
|
|
266
334
|
}
|
|
335
|
+
function getSourceLabel(sourcePath) {
|
|
336
|
+
if (!sourcePath)
|
|
337
|
+
return "";
|
|
338
|
+
return sourcePath.startsWith(homedir()) ? "global" : "project";
|
|
339
|
+
}
|
|
340
|
+
function getManagerServerInfo() {
|
|
341
|
+
return readConfigs().map((server) => ({
|
|
342
|
+
name: server.name,
|
|
343
|
+
enabled: server.enabled,
|
|
344
|
+
connected: connections.has(server.name),
|
|
345
|
+
transport: server.transport,
|
|
346
|
+
toolCount: toolCache.get(server.name)?.length ?? 0,
|
|
347
|
+
sourceLabel: getSourceLabel(server.sourcePath),
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
267
350
|
// ─── Formatters ───────────────────────────────────────────────────────────────
|
|
268
351
|
function formatServerList(servers) {
|
|
269
|
-
if (servers.length === 0)
|
|
270
|
-
return
|
|
352
|
+
if (servers.length === 0) {
|
|
353
|
+
return [
|
|
354
|
+
"No MCP servers configured.\n",
|
|
355
|
+
"Configuration guide:",
|
|
356
|
+
" Global (all projects): ~/.lsd/mcp.json",
|
|
357
|
+
" Project-level: .mcp.json or .lsd/mcp.json\n",
|
|
358
|
+
'Example ~/.lsd/mcp.json:',
|
|
359
|
+
'{',
|
|
360
|
+
' "mcpServers": {',
|
|
361
|
+
' "my-server": {',
|
|
362
|
+
' "command": "path/to/server",',
|
|
363
|
+
' "args": ["--working-dir", "."]',
|
|
364
|
+
' }',
|
|
365
|
+
' }',
|
|
366
|
+
'}\n',
|
|
367
|
+
"After editing, run: /mcp reload",
|
|
368
|
+
].join("\n");
|
|
369
|
+
}
|
|
271
370
|
const lines = ["MCP servers\n"];
|
|
272
371
|
for (const s of servers) {
|
|
273
372
|
const connected = connections.has(s.name) ? "yes" : "no";
|
|
@@ -278,11 +377,13 @@ function formatServerList(servers) {
|
|
|
278
377
|
lines.push(` connected: ${connected}`);
|
|
279
378
|
lines.push(` transport: ${s.transport}`);
|
|
280
379
|
lines.push(` tools: ${tools}`);
|
|
281
|
-
if (s.sourcePath)
|
|
282
|
-
lines.push(` source: ${basename(s.sourcePath)}`);
|
|
380
|
+
if (s.sourcePath) {
|
|
381
|
+
lines.push(` source: ${getSourceLabel(s.sourcePath)} — ${basename(s.sourcePath)}`);
|
|
382
|
+
}
|
|
283
383
|
lines.push("");
|
|
284
384
|
}
|
|
285
385
|
lines.push("Hints:");
|
|
386
|
+
lines.push(" /mcp");
|
|
286
387
|
lines.push(" /mcp inspect <server>");
|
|
287
388
|
lines.push(" /mcp enable <server>");
|
|
288
389
|
lines.push(" /mcp disable <server>");
|
|
@@ -319,6 +420,39 @@ function formatMcpCommandHelp() {
|
|
|
319
420
|
" /mcp reload",
|
|
320
421
|
].join("\n");
|
|
321
422
|
}
|
|
423
|
+
async function openMcpManager(ctx) {
|
|
424
|
+
await ctx.ui.custom((tui, theme, _keybindings, done) => new McpManagerComponent({
|
|
425
|
+
getServers: () => getManagerServerInfo(),
|
|
426
|
+
onToggle: async (name) => {
|
|
427
|
+
const config = getServerConfig(name);
|
|
428
|
+
if (!config)
|
|
429
|
+
return null;
|
|
430
|
+
const result = await setServerEnabled(name, !config.enabled);
|
|
431
|
+
const updated = getServerConfig(result.canonicalName);
|
|
432
|
+
if (updated?.enabled) {
|
|
433
|
+
await warmupServer(updated.name);
|
|
434
|
+
}
|
|
435
|
+
return getManagerServerInfo().find((server) => server.name === result.canonicalName) ?? null;
|
|
436
|
+
},
|
|
437
|
+
onInspect: async (name) => {
|
|
438
|
+
const { canonicalName, tools } = await listServerTools(name, undefined, { useCache: true });
|
|
439
|
+
return formatToolList(canonicalName, tools);
|
|
440
|
+
},
|
|
441
|
+
onReconnect: async (name) => {
|
|
442
|
+
const { canonicalName } = await listServerTools(name, undefined, { forceReconnect: true, useCache: false });
|
|
443
|
+
return getManagerServerInfo().find((server) => server.name === canonicalName) ?? null;
|
|
444
|
+
},
|
|
445
|
+
onClose: () => done(undefined),
|
|
446
|
+
requestRender: () => tui.requestRender(),
|
|
447
|
+
}, theme), {
|
|
448
|
+
overlay: true,
|
|
449
|
+
overlayOptions: {
|
|
450
|
+
width: "80%",
|
|
451
|
+
maxHeight: "70%",
|
|
452
|
+
anchor: "center",
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
}
|
|
322
456
|
async function handleMcpCommand(args, ctx) {
|
|
323
457
|
const trimmed = args.trim();
|
|
324
458
|
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
@@ -339,20 +473,8 @@ async function handleMcpCommand(args, ctx) {
|
|
|
339
473
|
return;
|
|
340
474
|
}
|
|
341
475
|
const canonicalName = config.name;
|
|
342
|
-
const cached = toolCache.get(canonicalName);
|
|
343
|
-
if (cached) {
|
|
344
|
-
ctx.ui.notify(formatToolList(canonicalName, cached), "info");
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
476
|
try {
|
|
348
|
-
const
|
|
349
|
-
const result = await client.listTools(undefined, { timeout: 30000 });
|
|
350
|
-
const tools = (result.tools ?? []).map((tool) => ({
|
|
351
|
-
name: tool.name,
|
|
352
|
-
description: tool.description ?? "",
|
|
353
|
-
inputSchema: tool.inputSchema,
|
|
354
|
-
}));
|
|
355
|
-
toolCache.set(canonicalName, tools);
|
|
477
|
+
const { tools } = await listServerTools(canonicalName, undefined, { useCache: true });
|
|
356
478
|
ctx.ui.notify(formatToolList(canonicalName, tools), "info");
|
|
357
479
|
}
|
|
358
480
|
catch (error) {
|
|
@@ -374,9 +496,8 @@ async function handleMcpCommand(args, ctx) {
|
|
|
374
496
|
const changeText = result.changed ? action : `already ${action}`;
|
|
375
497
|
ctx.ui.notify(`MCP server ${result.canonicalName} ${changeText}.`, "info");
|
|
376
498
|
if (enabled) {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
if (warmupResult?.status === "error") {
|
|
499
|
+
const warmupResult = await warmupServer(result.canonicalName);
|
|
500
|
+
if (warmupResult.status === "error") {
|
|
380
501
|
ctx.ui.notify(`Failed to connect ${result.canonicalName}: ${warmupResult.error}`, "error");
|
|
381
502
|
}
|
|
382
503
|
}
|
|
@@ -446,6 +567,10 @@ export default function (pi) {
|
|
|
446
567
|
return [];
|
|
447
568
|
},
|
|
448
569
|
handler: async (args, ctx) => {
|
|
570
|
+
if (!args.trim() && typeof ctx.ui.custom === "function") {
|
|
571
|
+
await openMcpManager(ctx);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
449
574
|
await handleMcpCommand(args, ctx);
|
|
450
575
|
},
|
|
451
576
|
});
|
|
@@ -453,7 +578,8 @@ export default function (pi) {
|
|
|
453
578
|
pi.registerTool({
|
|
454
579
|
name: "mcp_servers",
|
|
455
580
|
label: "MCP Servers",
|
|
456
|
-
description: "List all available MCP servers
|
|
581
|
+
description: "List all available MCP servers from global (~/.lsd/mcp.json) and project-level " +
|
|
582
|
+
"(.mcp.json, .lsd/mcp.json, legacy .gsd/mcp.json) config files. " +
|
|
457
583
|
"Shows server names, transport type, and connection status. Use mcp_discover to get full tool schemas for a server.",
|
|
458
584
|
promptSnippet: "List available MCP servers from project configuration",
|
|
459
585
|
promptGuidelines: [
|
|
@@ -510,29 +636,7 @@ export default function (pi) {
|
|
|
510
636
|
}),
|
|
511
637
|
async execute(_id, params, signal) {
|
|
512
638
|
try {
|
|
513
|
-
const canonicalServer =
|
|
514
|
-
// Return cached tools if available
|
|
515
|
-
const cached = toolCache.get(canonicalServer);
|
|
516
|
-
if (cached) {
|
|
517
|
-
const text = formatToolList(canonicalServer, cached);
|
|
518
|
-
const truncation = truncateHead(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
519
|
-
let finalText = truncation.content;
|
|
520
|
-
if (truncation.truncated) {
|
|
521
|
-
finalText += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
|
|
522
|
-
}
|
|
523
|
-
return {
|
|
524
|
-
content: [{ type: "text", text: finalText }],
|
|
525
|
-
details: { server: canonicalServer, toolCount: cached.length, cached: true },
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
const client = await getOrConnect(canonicalServer, signal);
|
|
529
|
-
const result = await client.listTools(undefined, { signal, timeout: 30000 });
|
|
530
|
-
const tools = (result.tools ?? []).map((t) => ({
|
|
531
|
-
name: t.name,
|
|
532
|
-
description: t.description ?? "",
|
|
533
|
-
inputSchema: t.inputSchema,
|
|
534
|
-
}));
|
|
535
|
-
toolCache.set(canonicalServer, tools);
|
|
639
|
+
const { canonicalName: canonicalServer, tools, cached } = await listServerTools(params.server, signal, { useCache: true });
|
|
536
640
|
const text = formatToolList(canonicalServer, tools);
|
|
537
641
|
const truncation = truncateHead(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
538
642
|
let finalText = truncation.content;
|
|
@@ -541,7 +645,7 @@ export default function (pi) {
|
|
|
541
645
|
}
|
|
542
646
|
return {
|
|
543
647
|
content: [{ type: "text", text: finalText }],
|
|
544
|
-
details: { server: canonicalServer, toolCount: tools.length, cached
|
|
648
|
+
details: { server: canonicalServer, toolCount: tools.length, cached },
|
|
545
649
|
};
|
|
546
650
|
}
|
|
547
651
|
catch (err) {
|
|
@@ -588,10 +692,7 @@ export default function (pi) {
|
|
|
588
692
|
}),
|
|
589
693
|
async execute(_id, params, signal) {
|
|
590
694
|
try {
|
|
591
|
-
const canonicalServer =
|
|
592
|
-
const client = await getOrConnect(canonicalServer, signal);
|
|
593
|
-
const result = await client.callTool({ name: params.tool, arguments: params.args ?? {} }, undefined, { signal, timeout: 60000 });
|
|
594
|
-
// Serialize result content to text
|
|
695
|
+
const { canonicalServer, result } = await callServerTool(params.server, params.tool, params.args ?? {}, signal);
|
|
595
696
|
const contentItems = result.content;
|
|
596
697
|
const raw = contentItems
|
|
597
698
|
.map((c) => (c.type === "text" ? c.text ?? "" : JSON.stringify(c)))
|
|
@@ -654,20 +755,27 @@ export default function (pi) {
|
|
|
654
755
|
const servers = readConfigs();
|
|
655
756
|
const enabledServers = servers.filter((server) => server.enabled);
|
|
656
757
|
if (servers.length > 0) {
|
|
657
|
-
ctx.ui.notify(`MCP client ready — ${enabledServers.length}/${servers.length} server(s) enabled
|
|
758
|
+
ctx.ui.notify(`MCP client ready — ${enabledServers.length}/${servers.length} server(s) enabled, warming up…`, "info");
|
|
658
759
|
}
|
|
659
760
|
if (enabledServers.length === 0)
|
|
660
761
|
return;
|
|
661
|
-
|
|
762
|
+
try {
|
|
763
|
+
const warmupTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("warmup timed out after 30s")), 30_000));
|
|
764
|
+
const results = await Promise.race([warmupEnabledServers(), warmupTimeout]);
|
|
765
|
+
const succeeded = results.filter((entry) => entry.status === "connected");
|
|
662
766
|
const failed = results.filter((entry) => entry.status === "error");
|
|
767
|
+
if (succeeded.length > 0) {
|
|
768
|
+
ctx.ui.notify(`MCP autoconnect complete — ${succeeded.length} server(s) connected`, "success");
|
|
769
|
+
}
|
|
663
770
|
if (failed.length > 0) {
|
|
664
771
|
const failureSummary = failed.map((entry) => `${entry.name}: ${entry.error}`).join("; ");
|
|
665
772
|
ctx.ui.notify(`MCP autoconnect partial failure — ${failureSummary}`, "warning");
|
|
666
773
|
}
|
|
667
|
-
}
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
668
776
|
const message = error instanceof Error ? error.message : String(error);
|
|
669
777
|
ctx.ui.notify(`MCP autoconnect failed: ${message}`, "warning");
|
|
670
|
-
}
|
|
778
|
+
}
|
|
671
779
|
});
|
|
672
780
|
pi.on("session_shutdown", async () => {
|
|
673
781
|
warmupPromise = null;
|
|
@@ -675,6 +783,6 @@ export default function (pi) {
|
|
|
675
783
|
});
|
|
676
784
|
pi.on("session_switch", async () => {
|
|
677
785
|
await reloadMcpState();
|
|
678
|
-
|
|
786
|
+
await warmupEnabledServers();
|
|
679
787
|
});
|
|
680
788
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { Key, SelectList, matchesKey, truncateToWidth } from "@gsd/pi-tui";
|
|
2
|
+
function getSelectListTheme(theme) {
|
|
3
|
+
return {
|
|
4
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
5
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
6
|
+
description: (text) => theme.fg("muted", text),
|
|
7
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
8
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function serversToItems(servers) {
|
|
12
|
+
return servers.map((server) => ({
|
|
13
|
+
value: server.name,
|
|
14
|
+
label: server.name,
|
|
15
|
+
description: [
|
|
16
|
+
server.enabled ? "enabled" : "disabled",
|
|
17
|
+
server.transport,
|
|
18
|
+
server.connected ? "● connected" : "○ offline",
|
|
19
|
+
`${server.toolCount} tools`,
|
|
20
|
+
server.sourceLabel || undefined,
|
|
21
|
+
].filter(Boolean).join(" "),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
export class McpManagerComponent {
|
|
25
|
+
theme;
|
|
26
|
+
callbacks;
|
|
27
|
+
selectList;
|
|
28
|
+
mode = "list";
|
|
29
|
+
inspectServerName = "";
|
|
30
|
+
inspectLines = [];
|
|
31
|
+
inspectScrollOffset = 0;
|
|
32
|
+
statusMessage = "";
|
|
33
|
+
busy = false;
|
|
34
|
+
statusTimeout = null;
|
|
35
|
+
constructor(callbacks, theme) {
|
|
36
|
+
this.callbacks = callbacks;
|
|
37
|
+
this.theme = theme;
|
|
38
|
+
this.selectList = new SelectList([], 8, getSelectListTheme(theme));
|
|
39
|
+
this.bindSelectList();
|
|
40
|
+
this.refreshList();
|
|
41
|
+
}
|
|
42
|
+
invalidate() {
|
|
43
|
+
this.selectList.invalidate();
|
|
44
|
+
}
|
|
45
|
+
dispose() {
|
|
46
|
+
if (this.statusTimeout) {
|
|
47
|
+
clearTimeout(this.statusTimeout);
|
|
48
|
+
this.statusTimeout = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
getMode() {
|
|
52
|
+
return this.mode;
|
|
53
|
+
}
|
|
54
|
+
handleInput(data) {
|
|
55
|
+
if (this.mode === "inspect") {
|
|
56
|
+
this.handleInspectInput(data);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
60
|
+
this.callbacks.onClose();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (data === "i") {
|
|
64
|
+
void this.handleInspect();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (data === "r") {
|
|
68
|
+
void this.handleReconnect();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.selectList.handleInput(data);
|
|
72
|
+
this.callbacks.requestRender();
|
|
73
|
+
}
|
|
74
|
+
render(width) {
|
|
75
|
+
const lines = [];
|
|
76
|
+
const add = (line = "") => lines.push(truncateToWidth(line, width));
|
|
77
|
+
const divider = this.theme.fg("border", "─".repeat(Math.max(width, 1)));
|
|
78
|
+
add(divider);
|
|
79
|
+
if (this.mode === "inspect") {
|
|
80
|
+
add(this.theme.bold(this.theme.fg("toolTitle", ` MCP Tools · ${this.inspectServerName}`)) +
|
|
81
|
+
this.theme.fg("dim", " esc/q: back ↑↓/pgup/pgdn/home/end: scroll"));
|
|
82
|
+
add("");
|
|
83
|
+
const bodyHeight = Math.max(8, width > 0 ? 18 : 8);
|
|
84
|
+
const maxOffset = Math.max(0, this.inspectLines.length - bodyHeight);
|
|
85
|
+
this.inspectScrollOffset = Math.max(0, Math.min(this.inspectScrollOffset, maxOffset));
|
|
86
|
+
const visibleLines = this.inspectLines.slice(this.inspectScrollOffset, this.inspectScrollOffset + bodyHeight);
|
|
87
|
+
for (const line of visibleLines)
|
|
88
|
+
add(line);
|
|
89
|
+
if (visibleLines.length === 0)
|
|
90
|
+
add(this.theme.fg("dim", " No tool information"));
|
|
91
|
+
add("");
|
|
92
|
+
add(divider);
|
|
93
|
+
add(this.theme.fg("dim", ` ${this.inspectLines.length} lines`));
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
add(this.theme.bold(this.theme.fg("toolTitle", " MCP Servers")) +
|
|
97
|
+
this.theme.fg("dim", " ↑↓ navigate enter: toggle i: inspect r: reconnect esc: close"));
|
|
98
|
+
add("");
|
|
99
|
+
lines.push(...this.selectList.render(width));
|
|
100
|
+
add("");
|
|
101
|
+
add(divider);
|
|
102
|
+
const servers = this.callbacks.getServers();
|
|
103
|
+
const enabled = servers.filter((server) => server.enabled).length;
|
|
104
|
+
let footer = this.theme.fg("dim", ` ${servers.length} servers · ${enabled} enabled`);
|
|
105
|
+
if (this.busy)
|
|
106
|
+
footer += this.theme.fg("accent", " · working…");
|
|
107
|
+
if (this.statusMessage)
|
|
108
|
+
footer += this.theme.fg("accent", ` — ${this.statusMessage}`);
|
|
109
|
+
add(footer);
|
|
110
|
+
return lines;
|
|
111
|
+
}
|
|
112
|
+
bindSelectList() {
|
|
113
|
+
this.selectList.onSelect = () => {
|
|
114
|
+
void this.handleToggle();
|
|
115
|
+
};
|
|
116
|
+
this.selectList.onCancel = () => {
|
|
117
|
+
this.callbacks.onClose();
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
refreshList(preferredName) {
|
|
121
|
+
const currentSelected = preferredName ?? this.selectList.getSelectedItem()?.value;
|
|
122
|
+
this.selectList = new SelectList(serversToItems(this.callbacks.getServers()), 8, getSelectListTheme(this.theme));
|
|
123
|
+
this.bindSelectList();
|
|
124
|
+
if (currentSelected) {
|
|
125
|
+
const items = this.callbacks.getServers();
|
|
126
|
+
const index = items.findIndex((item) => item.name === currentSelected);
|
|
127
|
+
if (index >= 0)
|
|
128
|
+
this.selectList.setSelectedIndex(index);
|
|
129
|
+
}
|
|
130
|
+
this.callbacks.requestRender();
|
|
131
|
+
}
|
|
132
|
+
setStatus(message) {
|
|
133
|
+
this.statusMessage = message;
|
|
134
|
+
this.callbacks.requestRender();
|
|
135
|
+
if (this.statusTimeout)
|
|
136
|
+
clearTimeout(this.statusTimeout);
|
|
137
|
+
if (!message)
|
|
138
|
+
return;
|
|
139
|
+
this.statusTimeout = setTimeout(() => {
|
|
140
|
+
this.statusMessage = "";
|
|
141
|
+
this.callbacks.requestRender();
|
|
142
|
+
}, 3000);
|
|
143
|
+
this.statusTimeout.unref?.();
|
|
144
|
+
}
|
|
145
|
+
getSelectedName() {
|
|
146
|
+
return this.selectList.getSelectedItem()?.value;
|
|
147
|
+
}
|
|
148
|
+
async runBusy(task) {
|
|
149
|
+
if (this.busy)
|
|
150
|
+
return;
|
|
151
|
+
this.busy = true;
|
|
152
|
+
this.callbacks.requestRender();
|
|
153
|
+
try {
|
|
154
|
+
await task();
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
this.busy = false;
|
|
158
|
+
this.callbacks.requestRender();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async handleToggle() {
|
|
162
|
+
const name = this.getSelectedName();
|
|
163
|
+
if (!name)
|
|
164
|
+
return;
|
|
165
|
+
await this.runBusy(async () => {
|
|
166
|
+
this.setStatus(`Toggling ${name}...`);
|
|
167
|
+
const updated = await this.callbacks.onToggle(name);
|
|
168
|
+
this.refreshList(updated?.name ?? name);
|
|
169
|
+
if (updated) {
|
|
170
|
+
this.setStatus(`${updated.name}: ${updated.enabled ? "enabled" : "disabled"}`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async handleInspect() {
|
|
175
|
+
const name = this.getSelectedName();
|
|
176
|
+
if (!name)
|
|
177
|
+
return;
|
|
178
|
+
await this.runBusy(async () => {
|
|
179
|
+
this.setStatus(`Loading tools for ${name}...`);
|
|
180
|
+
const text = await this.callbacks.onInspect(name);
|
|
181
|
+
this.inspectServerName = name;
|
|
182
|
+
this.inspectLines = text.split("\n");
|
|
183
|
+
this.inspectScrollOffset = 0;
|
|
184
|
+
this.mode = "inspect";
|
|
185
|
+
this.setStatus("");
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async handleReconnect() {
|
|
189
|
+
const name = this.getSelectedName();
|
|
190
|
+
if (!name)
|
|
191
|
+
return;
|
|
192
|
+
await this.runBusy(async () => {
|
|
193
|
+
this.setStatus(`Reconnecting ${name}...`);
|
|
194
|
+
const updated = await this.callbacks.onReconnect(name);
|
|
195
|
+
this.refreshList(updated?.name ?? name);
|
|
196
|
+
this.setStatus(updated ? `${updated.name}: reconnected` : `${name}: reconnect failed`);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
handleInspectInput(data) {
|
|
200
|
+
if (matchesKey(data, Key.escape) || data === "q") {
|
|
201
|
+
this.mode = "list";
|
|
202
|
+
this.callbacks.requestRender();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const page = 12;
|
|
206
|
+
if (matchesKey(data, Key.up))
|
|
207
|
+
this.inspectScrollOffset -= 1;
|
|
208
|
+
else if (matchesKey(data, Key.down))
|
|
209
|
+
this.inspectScrollOffset += 1;
|
|
210
|
+
else if (matchesKey(data, Key.pageUp))
|
|
211
|
+
this.inspectScrollOffset -= page;
|
|
212
|
+
else if (matchesKey(data, Key.pageDown))
|
|
213
|
+
this.inspectScrollOffset += page;
|
|
214
|
+
else if (matchesKey(data, Key.home))
|
|
215
|
+
this.inspectScrollOffset = 0;
|
|
216
|
+
else if (matchesKey(data, Key.end))
|
|
217
|
+
this.inspectScrollOffset = Number.MAX_SAFE_INTEGER;
|
|
218
|
+
this.callbacks.requestRender();
|
|
219
|
+
}
|
|
220
|
+
}
|