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
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 — ${
|
|
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
|
|
610
|
-
|
|
785
|
+
await reloadMcpState();
|
|
786
|
+
await warmupEnabledServers();
|
|
611
787
|
});
|
|
612
788
|
}
|