lsd-pi 1.3.7 → 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.
Files changed (92) hide show
  1. package/README.md +82 -0
  2. package/dist/resources/extensions/mcp-client/index.js +230 -54
  3. package/dist/resources/extensions/mcp-client/mcp-manager-component.js +220 -0
  4. package/dist/resources/extensions/slash-commands/plan.js +72 -18
  5. package/dist/resources/extensions/subagent/agents.js +7 -0
  6. package/dist/resources/extensions/subagent/index.js +25 -8
  7. package/dist/resources/extensions/subagent/model-resolution.js +1 -0
  8. package/dist/resources/extensions/usage/index.js +34 -2
  9. package/dist/resources/extensions/voice/index.js +1 -0
  10. package/dist/resources/extensions/voice/push-to-talk.js +2 -0
  11. package/package.json +1 -1
  12. package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.d.ts +2 -0
  13. package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.d.ts.map +1 -0
  14. package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.js +72 -0
  15. package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.js.map +1 -0
  16. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +4 -0
  17. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  18. package/packages/pi-coding-agent/dist/core/agent-session.js +29 -2
  19. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  20. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  21. package/packages/pi-coding-agent/dist/core/extensions/runner.js +1 -0
  22. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  23. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
  24. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  25. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  26. package/packages/pi-coding-agent/dist/core/tool-priority.js +1 -1
  27. package/packages/pi-coding-agent/dist/core/tool-priority.js.map +1 -1
  28. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  29. package/packages/pi-coding-agent/dist/main.js +1 -0
  30. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  31. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +104 -2
  32. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -1
  33. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +39 -2
  34. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  35. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +135 -18
  36. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  37. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +2 -0
  38. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  39. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -1
  40. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  41. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +21 -2
  42. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -1
  43. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +147 -9
  44. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -1
  45. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +51 -13
  46. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +112 -18
  49. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
  51. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -0
  52. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  53. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +4 -0
  54. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  55. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  56. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  57. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  58. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +34 -4
  59. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  60. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  61. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +3 -0
  62. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  63. package/packages/pi-coding-agent/package.json +1 -1
  64. package/packages/pi-coding-agent/src/core/agent-session.context-usage.test.ts +87 -0
  65. package/packages/pi-coding-agent/src/core/agent-session.ts +40 -2
  66. package/packages/pi-coding-agent/src/core/extensions/runner.ts +1 -0
  67. package/packages/pi-coding-agent/src/core/extensions/types.ts +3 -0
  68. package/packages/pi-coding-agent/src/core/tool-priority.ts +1 -1
  69. package/packages/pi-coding-agent/src/main.ts +1 -0
  70. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +129 -2
  71. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +158 -18
  72. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -1
  73. package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +164 -10
  74. package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +60 -13
  75. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +123 -20
  76. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -0
  77. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
  78. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +34 -4
  79. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +4 -0
  80. package/pkg/package.json +1 -1
  81. package/src/resources/extensions/mcp-client/index.ts +259 -58
  82. package/src/resources/extensions/mcp-client/mcp-manager-component.ts +256 -0
  83. package/src/resources/extensions/mcp-client/tests/mcp-manager-component.test.ts +141 -0
  84. package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +32 -0
  85. package/src/resources/extensions/slash-commands/plan.ts +76 -19
  86. package/src/resources/extensions/subagent/agents.ts +9 -0
  87. package/src/resources/extensions/subagent/index.ts +30 -8
  88. package/src/resources/extensions/subagent/model-resolution.ts +1 -0
  89. package/src/resources/extensions/usage/index.ts +40 -2
  90. package/src/resources/extensions/voice/index.ts +1 -0
  91. package/src/resources/extensions/voice/push-to-talk.ts +3 -0
  92. package/src/resources/extensions/voice/tests/push-to-talk.test.ts +6 -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 project files
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
  *
@@ -25,7 +25,9 @@ import { Client } from "@modelcontextprotocol/sdk/client";
25
25
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
26
26
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
27
27
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
28
+ import { homedir } from "node:os";
28
29
  import { basename, dirname, join } from "node:path";
30
+ import { McpManagerComponent, type McpManagerServerInfo } from "./mcp-manager-component.js";
29
31
 
30
32
  // ─── Types ────────────────────────────────────────────────────────────────────
31
33
 
@@ -62,6 +64,14 @@ interface McpState {
62
64
  const connections = new Map<string, ManagedConnection>();
63
65
  let configCache: McpServerConfig[] | null = null;
64
66
  const toolCache = new Map<string, McpToolSchema[]>();
67
+ let warmupPromise: Promise<McpWarmupResult[]> | null = null;
68
+
69
+ interface McpWarmupResult {
70
+ name: string;
71
+ status: "connected" | "error";
72
+ toolCount?: number;
73
+ error?: string;
74
+ }
65
75
 
66
76
  const MCP_STATE_PATH = join(process.cwd(), ".lsd", "mcp-state.json");
67
77
 
@@ -146,6 +156,7 @@ function readConfigs(): McpServerConfig[] {
146
156
  join(process.cwd(), ".mcp.json"),
147
157
  join(process.cwd(), ".lsd", "mcp.json"),
148
158
  join(process.cwd(), ".gsd", "mcp.json"),
159
+ join(homedir(), ".lsd", "mcp.json"),
149
160
  ];
150
161
 
151
162
  for (const configPath of configPaths) {
@@ -233,8 +244,6 @@ async function getOrConnect(name: string, signal?: AbortSignal): Promise<Client>
233
244
  if (!config) throw new Error(`Unknown MCP server: "${name}". Use mcp_servers to list available servers.`);
234
245
  if (!config.enabled) throw new Error(`Server "${config.name}" is disabled. Use /mcp enable ${config.name}.`);
235
246
 
236
- // Always use config.name as the canonical cache key so that variant
237
- // casing / whitespace still hits the same connection.
238
247
  const existing = connections.get(config.name);
239
248
  if (existing) return existing.client;
240
249
 
@@ -263,6 +272,127 @@ async function getOrConnect(name: string, signal?: AbortSignal): Promise<Client>
263
272
  return client;
264
273
  }
265
274
 
275
+ function mapToolSchemas(tools: Array<{ name: string; description?: string; inputSchema?: unknown }>): McpToolSchema[] {
276
+ return tools.map((tool) => ({
277
+ name: tool.name,
278
+ description: tool.description ?? "",
279
+ inputSchema: tool.inputSchema as Record<string, unknown> | undefined,
280
+ }));
281
+ }
282
+
283
+ function shouldRetryMcpOperation(error: unknown): boolean {
284
+ const message = error instanceof Error ? error.message : String(error);
285
+ if (/unknown mcp server/i.test(message)) return false;
286
+ if (/is disabled/i.test(message)) return false;
287
+ if (/unsupported transport/i.test(message)) return false;
288
+ if (/abort|cancel/i.test(message)) return false;
289
+ return true;
290
+ }
291
+
292
+ async function listServerTools(
293
+ name: string,
294
+ signal?: AbortSignal,
295
+ options?: { forceReconnect?: boolean; useCache?: boolean },
296
+ ): Promise<{ canonicalName: string; tools: McpToolSchema[]; cached: boolean }> {
297
+ const canonicalName = getCanonicalServerName(name);
298
+ if (options?.useCache !== false) {
299
+ const cached = toolCache.get(canonicalName);
300
+ if (cached) {
301
+ return { canonicalName, tools: cached, cached: true };
302
+ }
303
+ }
304
+
305
+ let attempt = 0;
306
+ while (attempt < 2) {
307
+ try {
308
+ if (attempt === 0 && options?.forceReconnect) {
309
+ await closeServerConnection(canonicalName);
310
+ }
311
+ if (attempt > 0) {
312
+ await closeServerConnection(canonicalName);
313
+ }
314
+ const client = await getOrConnect(canonicalName, signal);
315
+ const result = await client.listTools(undefined, { signal, timeout: 30000 });
316
+ const tools = mapToolSchemas(result.tools ?? []);
317
+ toolCache.set(canonicalName, tools);
318
+ return { canonicalName, tools, cached: false };
319
+ } catch (error) {
320
+ attempt += 1;
321
+ await closeServerConnection(canonicalName);
322
+ if (attempt >= 2 || !shouldRetryMcpOperation(error)) {
323
+ throw error;
324
+ }
325
+ }
326
+ }
327
+
328
+ throw new Error(`Failed to list tools for ${canonicalName}`);
329
+ }
330
+
331
+ async function callServerTool(
332
+ serverName: string,
333
+ toolName: string,
334
+ args: Record<string, unknown>,
335
+ signal?: AbortSignal,
336
+ ): Promise<{ canonicalServer: string; result: Awaited<ReturnType<Client["callTool"]>> }> {
337
+ const canonicalServer = getCanonicalServerName(serverName);
338
+ let attempt = 0;
339
+ while (attempt < 2) {
340
+ try {
341
+ if (attempt > 0) {
342
+ await closeServerConnection(canonicalServer);
343
+ }
344
+ const client = await getOrConnect(canonicalServer, signal);
345
+ const result = await client.callTool(
346
+ { name: toolName, arguments: args },
347
+ undefined,
348
+ { signal, timeout: 60000 },
349
+ );
350
+ return { canonicalServer, result };
351
+ } catch (error) {
352
+ attempt += 1;
353
+ await closeServerConnection(canonicalServer);
354
+ if (attempt >= 2 || !shouldRetryMcpOperation(error)) {
355
+ throw error;
356
+ }
357
+ }
358
+ }
359
+
360
+ throw new Error(`Failed to call ${canonicalServer}.${toolName}`);
361
+ }
362
+
363
+ async function warmupServer(name: string, signal?: AbortSignal): Promise<McpWarmupResult> {
364
+ try {
365
+ const { canonicalName, tools } = await listServerTools(name, signal, { useCache: false });
366
+ return {
367
+ name: canonicalName,
368
+ status: "connected",
369
+ toolCount: tools.length,
370
+ };
371
+ } catch (error) {
372
+ return {
373
+ name: getCanonicalServerName(name),
374
+ status: "error",
375
+ error: error instanceof Error ? error.message : String(error),
376
+ };
377
+ }
378
+ }
379
+
380
+ async function warmupEnabledServers(): Promise<McpWarmupResult[]> {
381
+ if (warmupPromise) return warmupPromise;
382
+
383
+ warmupPromise = (async () => {
384
+ const enabledServers = readConfigs().filter((server) => server.enabled);
385
+ if (enabledServers.length === 0) return [];
386
+ return Promise.all(enabledServers.map((server) => warmupServer(server.name)));
387
+ })();
388
+
389
+ try {
390
+ return await warmupPromise;
391
+ } finally {
392
+ warmupPromise = null;
393
+ }
394
+ }
395
+
266
396
  async function closeAll(): Promise<void> {
267
397
  const closing = Array.from(connections.entries()).map(async ([name, conn]) => {
268
398
  try {
@@ -277,14 +407,48 @@ async function closeAll(): Promise<void> {
277
407
  }
278
408
 
279
409
  async function reloadMcpState(): Promise<void> {
410
+ warmupPromise = null;
280
411
  await closeAll();
281
412
  configCache = null;
282
413
  }
283
414
 
415
+ function getSourceLabel(sourcePath?: string): string {
416
+ if (!sourcePath) return "";
417
+ return sourcePath.startsWith(homedir()) ? "global" : "project";
418
+ }
419
+
420
+ function getManagerServerInfo(): McpManagerServerInfo[] {
421
+ return readConfigs().map((server) => ({
422
+ name: server.name,
423
+ enabled: server.enabled,
424
+ connected: connections.has(server.name),
425
+ transport: server.transport,
426
+ toolCount: toolCache.get(server.name)?.length ?? 0,
427
+ sourceLabel: getSourceLabel(server.sourcePath),
428
+ }));
429
+ }
430
+
284
431
  // ─── Formatters ───────────────────────────────────────────────────────────────
285
432
 
286
433
  function formatServerList(servers: McpServerConfig[]): string {
287
- if (servers.length === 0) return "No MCP servers configured. Add servers to .mcp.json or .lsd/mcp.json.";
434
+ if (servers.length === 0) {
435
+ return [
436
+ "No MCP servers configured.\n",
437
+ "Configuration guide:",
438
+ " Global (all projects): ~/.lsd/mcp.json",
439
+ " Project-level: .mcp.json or .lsd/mcp.json\n",
440
+ 'Example ~/.lsd/mcp.json:',
441
+ '{',
442
+ ' "mcpServers": {',
443
+ ' "my-server": {',
444
+ ' "command": "path/to/server",',
445
+ ' "args": ["--working-dir", "."]',
446
+ ' }',
447
+ ' }',
448
+ '}\n',
449
+ "After editing, run: /mcp reload",
450
+ ].join("\n");
451
+ }
288
452
 
289
453
  const lines: string[] = ["MCP servers\n"];
290
454
 
@@ -297,11 +461,14 @@ function formatServerList(servers: McpServerConfig[]): string {
297
461
  lines.push(` connected: ${connected}`);
298
462
  lines.push(` transport: ${s.transport}`);
299
463
  lines.push(` tools: ${tools}`);
300
- if (s.sourcePath) lines.push(` source: ${basename(s.sourcePath)}`);
464
+ if (s.sourcePath) {
465
+ lines.push(` source: ${getSourceLabel(s.sourcePath)} — ${basename(s.sourcePath)}`);
466
+ }
301
467
  lines.push("");
302
468
  }
303
469
 
304
470
  lines.push("Hints:");
471
+ lines.push(" /mcp");
305
472
  lines.push(" /mcp inspect <server>");
306
473
  lines.push(" /mcp enable <server>");
307
474
  lines.push(" /mcp disable <server>");
@@ -342,6 +509,42 @@ function formatMcpCommandHelp(): string {
342
509
  ].join("\n");
343
510
  }
344
511
 
512
+ async function openMcpManager(ctx: ExtensionCommandContext): Promise<void> {
513
+ await ctx.ui.custom<void>(
514
+ (tui, theme, _keybindings, done) => new McpManagerComponent({
515
+ getServers: () => getManagerServerInfo(),
516
+ onToggle: async (name) => {
517
+ const config = getServerConfig(name);
518
+ if (!config) return null;
519
+ const result = await setServerEnabled(name, !config.enabled);
520
+ const updated = getServerConfig(result.canonicalName);
521
+ if (updated?.enabled) {
522
+ await warmupServer(updated.name);
523
+ }
524
+ return getManagerServerInfo().find((server) => server.name === result.canonicalName) ?? null;
525
+ },
526
+ onInspect: async (name) => {
527
+ const { canonicalName, tools } = await listServerTools(name, undefined, { useCache: true });
528
+ return formatToolList(canonicalName, tools);
529
+ },
530
+ onReconnect: async (name) => {
531
+ const { canonicalName } = await listServerTools(name, undefined, { forceReconnect: true, useCache: false });
532
+ return getManagerServerInfo().find((server) => server.name === canonicalName) ?? null;
533
+ },
534
+ onClose: () => done(undefined),
535
+ requestRender: () => tui.requestRender(),
536
+ }, theme),
537
+ {
538
+ overlay: true,
539
+ overlayOptions: {
540
+ width: "80%",
541
+ maxHeight: "70%",
542
+ anchor: "center",
543
+ },
544
+ },
545
+ );
546
+ }
547
+
345
548
  async function handleMcpCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
346
549
  const trimmed = args.trim();
347
550
  const parts = trimmed.split(/\s+/).filter(Boolean);
@@ -366,21 +569,9 @@ async function handleMcpCommand(args: string, ctx: ExtensionCommandContext): Pro
366
569
  }
367
570
 
368
571
  const canonicalName = config.name;
369
- const cached = toolCache.get(canonicalName);
370
- if (cached) {
371
- ctx.ui.notify(formatToolList(canonicalName, cached), "info");
372
- return;
373
- }
374
572
 
375
573
  try {
376
- const client = await getOrConnect(canonicalName);
377
- const result = await client.listTools(undefined, { timeout: 30000 });
378
- const tools: McpToolSchema[] = (result.tools ?? []).map((tool) => ({
379
- name: tool.name,
380
- description: tool.description ?? "",
381
- inputSchema: tool.inputSchema as Record<string, unknown> | undefined,
382
- }));
383
- toolCache.set(canonicalName, tools);
574
+ const { tools } = await listServerTools(canonicalName, undefined, { useCache: true });
384
575
  ctx.ui.notify(formatToolList(canonicalName, tools), "info");
385
576
  } catch (error) {
386
577
  const message = error instanceof Error ? error.message : String(error);
@@ -402,6 +593,13 @@ async function handleMcpCommand(args: string, ctx: ExtensionCommandContext): Pro
402
593
  const action = enabled ? "enabled" : "disabled";
403
594
  const changeText = result.changed ? action : `already ${action}`;
404
595
  ctx.ui.notify(`MCP server ${result.canonicalName} ${changeText}.`, "info");
596
+
597
+ if (enabled) {
598
+ const warmupResult = await warmupServer(result.canonicalName);
599
+ if (warmupResult.status === "error") {
600
+ ctx.ui.notify(`Failed to connect ${result.canonicalName}: ${warmupResult.error}`, "error");
601
+ }
602
+ }
405
603
  } catch (error) {
406
604
  const message = error instanceof Error ? error.message : String(error);
407
605
  ctx.ui.notify(message, "error");
@@ -412,7 +610,12 @@ async function handleMcpCommand(args: string, ctx: ExtensionCommandContext): Pro
412
610
  if (subcommand === "reload") {
413
611
  await reloadMcpState();
414
612
  const servers = readConfigs();
415
- ctx.ui.notify(`Reloaded MCP config ${servers.length} server(s) available.`, "info");
613
+ const warmupResults = await warmupEnabledServers();
614
+ const failed = warmupResults.filter((entry) => entry.status === "error");
615
+ const summary = failed.length > 0
616
+ ? `Reloaded MCP config — ${servers.length} server(s) available, ${failed.length} failed to connect.`
617
+ : `Reloaded MCP config — ${servers.length} server(s) available.`;
618
+ ctx.ui.notify(summary, failed.length > 0 ? "warning" : "info");
416
619
  return;
417
620
  }
418
621
 
@@ -477,6 +680,10 @@ export default function(pi: ExtensionAPI) {
477
680
  return [];
478
681
  },
479
682
  handler: async (args, ctx) => {
683
+ if (!args.trim() && typeof ctx.ui.custom === "function") {
684
+ await openMcpManager(ctx);
685
+ return;
686
+ }
480
687
  await handleMcpCommand(args, ctx);
481
688
  },
482
689
  });
@@ -487,7 +694,8 @@ export default function(pi: ExtensionAPI) {
487
694
  name: "mcp_servers",
488
695
  label: "MCP Servers",
489
696
  description:
490
- "List all available MCP servers configured in project files (.mcp.json, .lsd/mcp.json, legacy .gsd/mcp.json). " +
697
+ "List all available MCP servers from global (~/.lsd/mcp.json) and project-level " +
698
+ "(.mcp.json, .lsd/mcp.json, legacy .gsd/mcp.json) config files. " +
491
699
  "Shows server names, transport type, and connection status. Use mcp_discover to get full tool schemas for a server.",
492
700
  promptSnippet:
493
701
  "List available MCP servers from project configuration",
@@ -558,32 +766,7 @@ export default function(pi: ExtensionAPI) {
558
766
 
559
767
  async execute(_id, params, signal) {
560
768
  try {
561
- const canonicalServer = getCanonicalServerName(params.server);
562
-
563
- // Return cached tools if available
564
- const cached = toolCache.get(canonicalServer);
565
- if (cached) {
566
- const text = formatToolList(canonicalServer, cached);
567
- const truncation = truncateHead(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
568
- let finalText = truncation.content;
569
- if (truncation.truncated) {
570
- finalText += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
571
- }
572
- return {
573
- content: [{ type: "text", text: finalText }],
574
- details: { server: canonicalServer, toolCount: cached.length, cached: true },
575
- };
576
- }
577
-
578
- const client = await getOrConnect(canonicalServer, signal);
579
- const result = await client.listTools(undefined, { signal, timeout: 30000 });
580
- const tools: McpToolSchema[] = (result.tools ?? []).map((t) => ({
581
- name: t.name,
582
- description: t.description ?? "",
583
- inputSchema: t.inputSchema as Record<string, unknown> | undefined,
584
- }));
585
- toolCache.set(canonicalServer, tools);
586
-
769
+ const { canonicalName: canonicalServer, tools, cached } = await listServerTools(params.server, signal, { useCache: true });
587
770
  const text = formatToolList(canonicalServer, tools);
588
771
  const truncation = truncateHead(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
589
772
  let finalText = truncation.content;
@@ -593,7 +776,7 @@ export default function(pi: ExtensionAPI) {
593
776
 
594
777
  return {
595
778
  content: [{ type: "text", text: finalText }],
596
- details: { server: canonicalServer, toolCount: tools.length, cached: false },
779
+ details: { server: canonicalServer, toolCount: tools.length, cached },
597
780
  };
598
781
  } catch (err: unknown) {
599
782
  const msg = err instanceof Error ? err.message : String(err);
@@ -652,15 +835,13 @@ export default function(pi: ExtensionAPI) {
652
835
 
653
836
  async execute(_id, params, signal) {
654
837
  try {
655
- const canonicalServer = getCanonicalServerName(params.server);
656
- const client = await getOrConnect(canonicalServer, signal);
657
- const result = await client.callTool(
658
- { name: params.tool, arguments: params.args ?? {} },
659
- undefined,
660
- { signal, timeout: 60000 },
838
+ const { canonicalServer, result } = await callServerTool(
839
+ params.server,
840
+ params.tool,
841
+ params.args ?? {},
842
+ signal,
661
843
  );
662
844
 
663
- // Serialize result content to text
664
845
  const contentItems = result.content as Array<{ type: string; text?: string }>;
665
846
  const raw = contentItems
666
847
  .map((c) => (c.type === "text" ? c.text ?? "" : JSON.stringify(c)))
@@ -733,17 +914,37 @@ export default function(pi: ExtensionAPI) {
733
914
 
734
915
  pi.on("session_start", async (_event, ctx) => {
735
916
  const servers = readConfigs();
917
+ const enabledServers = servers.filter((server) => server.enabled);
736
918
  if (servers.length > 0) {
737
- ctx.ui.notify(`MCP client ready — ${servers.filter((server) => server.enabled).length}/${servers.length} server(s) enabled`, "info");
919
+ ctx.ui.notify(`MCP client ready — ${enabledServers.length}/${servers.length} server(s) enabled, warming up…`, "info");
920
+ }
921
+ if (enabledServers.length === 0) return;
922
+
923
+ try {
924
+ const warmupTimeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error("warmup timed out after 30s")), 30_000));
925
+ const results = await Promise.race([warmupEnabledServers(), warmupTimeout]);
926
+ const succeeded = results.filter((entry) => entry.status === "connected");
927
+ const failed = results.filter((entry) => entry.status === "error");
928
+ if (succeeded.length > 0) {
929
+ ctx.ui.notify(`MCP autoconnect complete — ${succeeded.length} server(s) connected`, "success");
930
+ }
931
+ if (failed.length > 0) {
932
+ const failureSummary = failed.map((entry) => `${entry.name}: ${entry.error}`).join("; ");
933
+ ctx.ui.notify(`MCP autoconnect partial failure — ${failureSummary}`, "warning");
934
+ }
935
+ } catch (error) {
936
+ const message = error instanceof Error ? error.message : String(error);
937
+ ctx.ui.notify(`MCP autoconnect failed: ${message}`, "warning");
738
938
  }
739
939
  });
740
940
 
741
941
  pi.on("session_shutdown", async () => {
942
+ warmupPromise = null;
742
943
  await closeAll();
743
944
  });
744
945
 
745
946
  pi.on("session_switch", async () => {
746
- await closeAll();
747
- configCache = null;
947
+ await reloadMcpState();
948
+ await warmupEnabledServers();
748
949
  });
749
950
  }