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.
- package/README.md +82 -0
- package/dist/resources/extensions/mcp-client/index.js +230 -54
- package/dist/resources/extensions/mcp-client/mcp-manager-component.js +220 -0
- package/dist/resources/extensions/slash-commands/plan.js +72 -18
- 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/dist/resources/extensions/usage/index.js +34 -2
- package/dist/resources/extensions/voice/index.js +1 -0
- package/dist/resources/extensions/voice/push-to-talk.js +2 -0
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.js +72 -0
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +4 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +29 -2
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tool-priority.js +1 -1
- package/packages/pi-coding-agent/dist/core/tool-priority.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +1 -0
- package/packages/pi-coding-agent/dist/main.js.map +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-execution.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.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 +147 -9
- 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 +112 -18
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-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 +34 -4
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +3 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.context-usage.test.ts +87 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +40 -2
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +1 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +3 -0
- package/packages/pi-coding-agent/src/core/tool-priority.ts +1 -1
- package/packages/pi-coding-agent/src/main.ts +1 -0
- 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-execution.ts +18 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +164 -10
- 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 +123 -20
- package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -0
- 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 +34 -4
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +4 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/mcp-client/index.ts +259 -58
- 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 +32 -0
- package/src/resources/extensions/slash-commands/plan.ts +76 -19
- 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
- package/src/resources/extensions/usage/index.ts +40 -2
- package/src/resources/extensions/voice/index.ts +1 -0
- package/src/resources/extensions/voice/push-to-talk.ts +3 -0
- 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
|
|
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)
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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 — ${
|
|
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
|
|
747
|
-
|
|
947
|
+
await reloadMcpState();
|
|
948
|
+
await warmupEnabledServers();
|
|
748
949
|
});
|
|
749
950
|
}
|