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
package/README.md CHANGED
@@ -365,6 +365,88 @@ LSD discovers and connects to MCP servers configured in:
365
365
 
366
366
  Use `/configs` inside a session to scan for MCP servers from other AI tools (Claude Code, Cursor, Copilot, etc.) and import them.
367
367
 
368
+ ### Adding MCP servers to LSD config
369
+
370
+ LSD supports two transport types: **stdio** (launch a local process) and **HTTP** (connect to a running server).
371
+
372
+ #### Stdio server (local process)
373
+
374
+ Add to `.mcp.json` or `.lsd/mcp.json`:
375
+
376
+ ```json
377
+ {
378
+ "mcpServers": {
379
+ "my-server": {
380
+ "type": "stdio",
381
+ "command": "/absolute/path/to/executable",
382
+ "args": ["arg1", "arg2"],
383
+ "env": {
384
+ "API_KEY": "your-key",
385
+ "DEBUG": "true"
386
+ }
387
+ }
388
+ }
389
+ }
390
+ ```
391
+
392
+ If the server is installed as an npm package:
393
+
394
+ ```json
395
+ {
396
+ "mcpServers": {
397
+ "my-server": {
398
+ "type": "stdio",
399
+ "command": "npx",
400
+ "args": ["@my-org/mcp-server"],
401
+ "env": {
402
+ "API_KEY": "sk-..."
403
+ }
404
+ }
405
+ }
406
+ }
407
+ ```
408
+
409
+ #### HTTP server (remote connection)
410
+
411
+ For MCP servers already running on a network endpoint:
412
+
413
+ ```json
414
+ {
415
+ "mcpServers": {
416
+ "remote-server": {
417
+ "type": "http",
418
+ "url": "http://localhost:8080/mcp",
419
+ "headers": {
420
+ "Authorization": "Bearer ${MCP_TOKEN}"
421
+ }
422
+ }
423
+ }
424
+ }
425
+ ```
426
+
427
+ Environment variables in `headers` and `env` are resolved at startup (use `${VAR_NAME}` syntax).
428
+
429
+ #### File placement
430
+
431
+ - **`.mcp.json`** — repo-shared configuration (commit to git)
432
+ - **`.lsd/mcp.json`** — local-only configuration (git-ignored, not shared)
433
+
434
+ If both files exist, server names are merged and the first definition found wins.
435
+
436
+ #### Managing MCP servers
437
+
438
+ Use the `/mcp` slash command inside a session:
439
+
440
+ | Command | Description |
441
+ |---------|-------------|
442
+ | `/mcp list` | List all configured servers and their status |
443
+ | `/mcp inspect <server>` | Connect and show available tools for a server |
444
+ | `/mcp enable <server>` | Enable a server |
445
+ | `/mcp disable <server>` | Disable a server |
446
+ | `/mcp reload` | Reload config and reconnect enabled servers |
447
+
448
+ MCP servers connect lazily — `/mcp inspect` or the first tool call triggers the connection.
449
+
368
450
  ---
369
451
 
370
452
  ## Sessions
@@ -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
  *
@@ -18,11 +18,14 @@ 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;
25
27
  const toolCache = new Map();
28
+ let warmupPromise = null;
26
29
  const MCP_STATE_PATH = join(process.cwd(), ".lsd", "mcp-state.json");
27
30
  function normalizeServerName(name) {
28
31
  return name.trim().toLowerCase();
@@ -98,6 +101,7 @@ function readConfigs() {
98
101
  join(process.cwd(), ".mcp.json"),
99
102
  join(process.cwd(), ".lsd", "mcp.json"),
100
103
  join(process.cwd(), ".gsd", "mcp.json"),
104
+ join(homedir(), ".lsd", "mcp.json"),
101
105
  ];
102
106
  for (const configPath of configPaths) {
103
107
  try {
@@ -176,8 +180,6 @@ async function getOrConnect(name, signal) {
176
180
  throw new Error(`Unknown MCP server: "${name}". Use mcp_servers to list available servers.`);
177
181
  if (!config.enabled)
178
182
  throw new Error(`Server "${config.name}" is disabled. Use /mcp enable ${config.name}.`);
179
- // Always use config.name as the canonical cache key so that variant
180
- // casing / whitespace still hits the same connection.
181
183
  const existing = connections.get(config.name);
182
184
  if (existing)
183
185
  return existing.client;
@@ -205,6 +207,113 @@ async function getOrConnect(name, signal) {
205
207
  connections.set(config.name, { client, transport });
206
208
  return client;
207
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
+ }
301
+ async function warmupEnabledServers() {
302
+ if (warmupPromise)
303
+ return warmupPromise;
304
+ warmupPromise = (async () => {
305
+ const enabledServers = readConfigs().filter((server) => server.enabled);
306
+ if (enabledServers.length === 0)
307
+ return [];
308
+ return Promise.all(enabledServers.map((server) => warmupServer(server.name)));
309
+ })();
310
+ try {
311
+ return await warmupPromise;
312
+ }
313
+ finally {
314
+ warmupPromise = null;
315
+ }
316
+ }
208
317
  async function closeAll() {
209
318
  const closing = Array.from(connections.entries()).map(async ([name, conn]) => {
210
319
  try {
@@ -219,13 +328,45 @@ async function closeAll() {
219
328
  toolCache.clear();
220
329
  }
221
330
  async function reloadMcpState() {
331
+ warmupPromise = null;
222
332
  await closeAll();
223
333
  configCache = null;
224
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
+ }
225
350
  // ─── Formatters ───────────────────────────────────────────────────────────────
226
351
  function formatServerList(servers) {
227
- if (servers.length === 0)
228
- return "No MCP servers configured. Add servers to .mcp.json or .lsd/mcp.json.";
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
+ }
229
370
  const lines = ["MCP servers\n"];
230
371
  for (const s of servers) {
231
372
  const connected = connections.has(s.name) ? "yes" : "no";
@@ -236,11 +377,13 @@ function formatServerList(servers) {
236
377
  lines.push(` connected: ${connected}`);
237
378
  lines.push(` transport: ${s.transport}`);
238
379
  lines.push(` tools: ${tools}`);
239
- if (s.sourcePath)
240
- lines.push(` source: ${basename(s.sourcePath)}`);
380
+ if (s.sourcePath) {
381
+ lines.push(` source: ${getSourceLabel(s.sourcePath)} — ${basename(s.sourcePath)}`);
382
+ }
241
383
  lines.push("");
242
384
  }
243
385
  lines.push("Hints:");
386
+ lines.push(" /mcp");
244
387
  lines.push(" /mcp inspect <server>");
245
388
  lines.push(" /mcp enable <server>");
246
389
  lines.push(" /mcp disable <server>");
@@ -277,6 +420,39 @@ function formatMcpCommandHelp() {
277
420
  " /mcp reload",
278
421
  ].join("\n");
279
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
+ }
280
456
  async function handleMcpCommand(args, ctx) {
281
457
  const trimmed = args.trim();
282
458
  const parts = trimmed.split(/\s+/).filter(Boolean);
@@ -297,20 +473,8 @@ async function handleMcpCommand(args, ctx) {
297
473
  return;
298
474
  }
299
475
  const canonicalName = config.name;
300
- const cached = toolCache.get(canonicalName);
301
- if (cached) {
302
- ctx.ui.notify(formatToolList(canonicalName, cached), "info");
303
- return;
304
- }
305
476
  try {
306
- const client = await getOrConnect(canonicalName);
307
- const result = await client.listTools(undefined, { timeout: 30000 });
308
- const tools = (result.tools ?? []).map((tool) => ({
309
- name: tool.name,
310
- description: tool.description ?? "",
311
- inputSchema: tool.inputSchema,
312
- }));
313
- toolCache.set(canonicalName, tools);
477
+ const { tools } = await listServerTools(canonicalName, undefined, { useCache: true });
314
478
  ctx.ui.notify(formatToolList(canonicalName, tools), "info");
315
479
  }
316
480
  catch (error) {
@@ -331,6 +495,12 @@ async function handleMcpCommand(args, ctx) {
331
495
  const action = enabled ? "enabled" : "disabled";
332
496
  const changeText = result.changed ? action : `already ${action}`;
333
497
  ctx.ui.notify(`MCP server ${result.canonicalName} ${changeText}.`, "info");
498
+ if (enabled) {
499
+ const warmupResult = await warmupServer(result.canonicalName);
500
+ if (warmupResult.status === "error") {
501
+ ctx.ui.notify(`Failed to connect ${result.canonicalName}: ${warmupResult.error}`, "error");
502
+ }
503
+ }
334
504
  }
335
505
  catch (error) {
336
506
  const message = error instanceof Error ? error.message : String(error);
@@ -341,7 +511,12 @@ async function handleMcpCommand(args, ctx) {
341
511
  if (subcommand === "reload") {
342
512
  await reloadMcpState();
343
513
  const servers = readConfigs();
344
- ctx.ui.notify(`Reloaded MCP config ${servers.length} server(s) available.`, "info");
514
+ const warmupResults = await warmupEnabledServers();
515
+ const failed = warmupResults.filter((entry) => entry.status === "error");
516
+ const summary = failed.length > 0
517
+ ? `Reloaded MCP config — ${servers.length} server(s) available, ${failed.length} failed to connect.`
518
+ : `Reloaded MCP config — ${servers.length} server(s) available.`;
519
+ ctx.ui.notify(summary, failed.length > 0 ? "warning" : "info");
345
520
  return;
346
521
  }
347
522
  if (subcommand === "help") {
@@ -392,6 +567,10 @@ export default function (pi) {
392
567
  return [];
393
568
  },
394
569
  handler: async (args, ctx) => {
570
+ if (!args.trim() && typeof ctx.ui.custom === "function") {
571
+ await openMcpManager(ctx);
572
+ return;
573
+ }
395
574
  await handleMcpCommand(args, ctx);
396
575
  },
397
576
  });
@@ -399,7 +578,8 @@ export default function (pi) {
399
578
  pi.registerTool({
400
579
  name: "mcp_servers",
401
580
  label: "MCP Servers",
402
- description: "List all available MCP servers configured in project files (.mcp.json, .lsd/mcp.json, legacy .gsd/mcp.json). " +
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. " +
403
583
  "Shows server names, transport type, and connection status. Use mcp_discover to get full tool schemas for a server.",
404
584
  promptSnippet: "List available MCP servers from project configuration",
405
585
  promptGuidelines: [
@@ -456,29 +636,7 @@ export default function (pi) {
456
636
  }),
457
637
  async execute(_id, params, signal) {
458
638
  try {
459
- const canonicalServer = getCanonicalServerName(params.server);
460
- // Return cached tools if available
461
- const cached = toolCache.get(canonicalServer);
462
- if (cached) {
463
- const text = formatToolList(canonicalServer, cached);
464
- const truncation = truncateHead(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
465
- let finalText = truncation.content;
466
- if (truncation.truncated) {
467
- finalText += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
468
- }
469
- return {
470
- content: [{ type: "text", text: finalText }],
471
- details: { server: canonicalServer, toolCount: cached.length, cached: true },
472
- };
473
- }
474
- const client = await getOrConnect(canonicalServer, signal);
475
- const result = await client.listTools(undefined, { signal, timeout: 30000 });
476
- const tools = (result.tools ?? []).map((t) => ({
477
- name: t.name,
478
- description: t.description ?? "",
479
- inputSchema: t.inputSchema,
480
- }));
481
- toolCache.set(canonicalServer, tools);
639
+ const { canonicalName: canonicalServer, tools, cached } = await listServerTools(params.server, signal, { useCache: true });
482
640
  const text = formatToolList(canonicalServer, tools);
483
641
  const truncation = truncateHead(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
484
642
  let finalText = truncation.content;
@@ -487,7 +645,7 @@ export default function (pi) {
487
645
  }
488
646
  return {
489
647
  content: [{ type: "text", text: finalText }],
490
- details: { server: canonicalServer, toolCount: tools.length, cached: false },
648
+ details: { server: canonicalServer, toolCount: tools.length, cached },
491
649
  };
492
650
  }
493
651
  catch (err) {
@@ -534,10 +692,7 @@ export default function (pi) {
534
692
  }),
535
693
  async execute(_id, params, signal) {
536
694
  try {
537
- const canonicalServer = getCanonicalServerName(params.server);
538
- const client = await getOrConnect(canonicalServer, signal);
539
- const result = await client.callTool({ name: params.tool, arguments: params.args ?? {} }, undefined, { signal, timeout: 60000 });
540
- // Serialize result content to text
695
+ const { canonicalServer, result } = await callServerTool(params.server, params.tool, params.args ?? {}, signal);
541
696
  const contentItems = result.content;
542
697
  const raw = contentItems
543
698
  .map((c) => (c.type === "text" ? c.text ?? "" : JSON.stringify(c)))
@@ -598,15 +753,36 @@ export default function (pi) {
598
753
  // ── Lifecycle ─────────────────────────────────────────────────────────────
599
754
  pi.on("session_start", async (_event, ctx) => {
600
755
  const servers = readConfigs();
756
+ const enabledServers = servers.filter((server) => server.enabled);
601
757
  if (servers.length > 0) {
602
- ctx.ui.notify(`MCP client ready — ${servers.filter((server) => server.enabled).length}/${servers.length} server(s) enabled`, "info");
758
+ ctx.ui.notify(`MCP client ready — ${enabledServers.length}/${servers.length} server(s) enabled, warming up…`, "info");
759
+ }
760
+ if (enabledServers.length === 0)
761
+ return;
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");
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
+ }
770
+ if (failed.length > 0) {
771
+ const failureSummary = failed.map((entry) => `${entry.name}: ${entry.error}`).join("; ");
772
+ ctx.ui.notify(`MCP autoconnect partial failure — ${failureSummary}`, "warning");
773
+ }
774
+ }
775
+ catch (error) {
776
+ const message = error instanceof Error ? error.message : String(error);
777
+ ctx.ui.notify(`MCP autoconnect failed: ${message}`, "warning");
603
778
  }
604
779
  });
605
780
  pi.on("session_shutdown", async () => {
781
+ warmupPromise = null;
606
782
  await closeAll();
607
783
  });
608
784
  pi.on("session_switch", async () => {
609
- await closeAll();
610
- configCache = null;
785
+ await reloadMcpState();
786
+ await warmupEnabledServers();
611
787
  });
612
788
  }