pi-mcp-adapter 3.3.0 → 5.0.0
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/CHANGELOG.md +95 -1
- package/README.md +78 -974
- package/cli.js +210 -0
- package/commands.ts +55 -4
- package/config.ts +308 -33
- package/direct-tool-surface.ts +9 -7
- package/direct-tools.ts +43 -29
- package/dist/config.d.ts +15 -4
- package/dist/config.js +317 -30
- package/dist/config.js.map +1 -1
- package/dist/elicitation-handler.d.ts +4 -0
- package/dist/elicitation-handler.js +14 -1
- package/dist/elicitation-handler.js.map +1 -1
- package/dist/mcp-auth-flow.d.ts +2 -0
- package/dist/mcp-auth-flow.js +12 -3
- package/dist/mcp-auth-flow.js.map +1 -1
- package/dist/mcp-auth.js +3 -1
- package/dist/mcp-auth.js.map +1 -1
- package/dist/metadata-cache.d.ts +15 -1
- package/dist/metadata-cache.js +76 -8
- package/dist/metadata-cache.js.map +1 -1
- package/dist/output-shape.d.ts +18 -5
- package/dist/output-shape.js +162 -18
- package/dist/output-shape.js.map +1 -1
- package/dist/package-mcp-loader.js +9 -5
- package/dist/package-mcp-loader.js.map +1 -1
- package/dist/project-server-trust.d.ts +17 -0
- package/dist/project-server-trust.js +209 -0
- package/dist/project-server-trust.js.map +1 -0
- package/dist/server-manager.d.ts +5 -0
- package/dist/server-manager.js +98 -26
- package/dist/server-manager.js.map +1 -1
- package/dist/session-recovery.js +4 -3
- package/dist/session-recovery.js.map +1 -1
- package/dist/state.d.ts +2 -0
- package/dist/types.d.ts +29 -2
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +5 -1
- package/dist/utils.js +16 -0
- package/dist/utils.js.map +1 -1
- package/{OAUTH.md → docs/auth.md} +66 -11
- package/docs/configuration.md +221 -0
- package/docs/extension-api.md +246 -0
- package/docs/pi-builtin-comparison.md +113 -0
- package/docs/prompts-and-ui.md +84 -0
- package/docs/scripting.md +131 -0
- package/docs/servers.md +142 -0
- package/docs/tools.md +259 -0
- package/elicitation-handler.ts +13 -1
- package/index.ts +268 -37
- package/init.ts +97 -46
- package/mcp-auth-flow.ts +13 -2
- package/mcp-auth.ts +3 -1
- package/mcp-code.ts +125 -77
- package/mcp-output-guard.ts +24 -5
- package/mcp-panel.ts +52 -4
- package/mcp-script-worker.mjs +6 -2
- package/mcp-setup-panel.ts +587 -303
- package/metadata-cache.ts +79 -8
- package/onboarding-state.ts +20 -0
- package/output-shape.ts +161 -22
- package/package-mcp-loader.ts +8 -5
- package/package.json +8 -6
- package/pi-builtin-mcp.ts +32 -0
- package/pi-signin-import.ts +147 -0
- package/project-server-trust.ts +8 -4
- package/prompts.ts +11 -3
- package/proxy-modes.ts +163 -138
- package/search-ranking.ts +12 -3
- package/server-manager.ts +101 -24
- package/session-recovery.ts +4 -3
- package/skills/mcp-scripting/SKILL.md +24 -30
- package/skills/mcp-scripting/references/jev.md +3 -0
- package/state.ts +2 -0
- package/types.ts +18 -2
- package/ui-server.ts +34 -32
- package/utils.ts +16 -1
package/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
6
7
|
import { pathToFileURL } from "node:url";
|
|
7
8
|
import stripJsonComments from "strip-json-comments";
|
|
8
9
|
|
|
@@ -74,6 +75,10 @@ function printHelp(log = console.log) {
|
|
|
74
75
|
log(" pi-mcp-adapter init --dry-run");
|
|
75
76
|
log(" pi-mcp-adapter init --discover-host-configs Opt in to host config fallback discovery");
|
|
76
77
|
log("");
|
|
78
|
+
log("Check configured servers (exits 1 when an enabled server fails):");
|
|
79
|
+
log(" pi-mcp-adapter doctor Connect each enabled server and report its state and tool count");
|
|
80
|
+
log(" pi-mcp-adapter doctor --json Print the same report as a JSON array");
|
|
81
|
+
log("");
|
|
77
82
|
log("Bearer token storage (servers configured with auth: \"bearer\" and bearerTokenStore: true):");
|
|
78
83
|
log(" pi-mcp-adapter token set <server> Store a token read from stdin (masked prompt or pipe; never argv)");
|
|
79
84
|
log(" pi-mcp-adapter token status <server> Report whether a stored token matches the configured URL");
|
|
@@ -413,6 +418,207 @@ async function runKey(argv, log, error, stdin) {
|
|
|
413
418
|
return 0;
|
|
414
419
|
}
|
|
415
420
|
|
|
421
|
+
const DOCTOR_CONNECT_TIMEOUT_MS = 15_000;
|
|
422
|
+
|
|
423
|
+
// A second layer behind describeConnectError: messages can still name configured values.
|
|
424
|
+
// Values under 4 characters are flags such as DEBUG=1, and redacting them would garble the text.
|
|
425
|
+
function redactDoctorMessage(text, definition, utils) {
|
|
426
|
+
const configured = [definition.bearerToken, definition.oauth?.clientSecret, ...Object.values(definition.headers ?? {}), ...Object.values(definition.env ?? {})];
|
|
427
|
+
if (definition.bearerTokenEnv) configured.push(process.env[definition.bearerTokenEnv]);
|
|
428
|
+
if (typeof definition.url === "string") {
|
|
429
|
+
try {
|
|
430
|
+
configured.push(...new URL(utils.interpolateEnvVars(definition.url)).searchParams.values());
|
|
431
|
+
} catch {
|
|
432
|
+
// An invalid URL is reported without its value.
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const secrets = configured
|
|
436
|
+
.filter((value) => typeof value === "string")
|
|
437
|
+
.flatMap((value) => [value, utils.interpolateEnvVars(value)])
|
|
438
|
+
.flatMap((value) => [value, ...value.split(/\s+/)])
|
|
439
|
+
.filter((value) => value.length >= 4)
|
|
440
|
+
.sort((left, right) => right.length - left.length);
|
|
441
|
+
let redacted = text;
|
|
442
|
+
for (const secret of secrets) redacted = redacted.replaceAll(secret, "***");
|
|
443
|
+
return redacted;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Doctor prints only facts the adapter composes, never server-originated text such as HTTP
|
|
447
|
+
// response bodies or child stderr: those can carry secrets that no configured value reveals.
|
|
448
|
+
function describeConnectError(err, definition, url) {
|
|
449
|
+
if (!definition.url) {
|
|
450
|
+
// server-manager appends the child's stderr as `${message} (${stderr})`, keeping the original as cause.
|
|
451
|
+
const base = err.cause instanceof Error && err.message.startsWith(`${err.cause.message} (`) ? err.cause : err;
|
|
452
|
+
const message = typeof base.code === "number" ? `server returned JSON-RPC error ${base.code}` : base.message;
|
|
453
|
+
return definition.command ? `${message} — run the command directly to see its output` : message;
|
|
454
|
+
}
|
|
455
|
+
// Adapter-composed failures of secret commands and CA files, thrown before any response.
|
|
456
|
+
if (/^(Failed to resolve MCP server|HTTP request headers command|MCP caFile|Missing environment variable in MCP caFile)/.test(err.message)) {
|
|
457
|
+
return err.message;
|
|
458
|
+
}
|
|
459
|
+
let status;
|
|
460
|
+
const codes = new Set();
|
|
461
|
+
const pending = [err];
|
|
462
|
+
const seen = new Set();
|
|
463
|
+
while (pending.length > 0) {
|
|
464
|
+
const current = pending.pop();
|
|
465
|
+
if (typeof current !== "object" || current === null || seen.has(current)) continue;
|
|
466
|
+
seen.add(current);
|
|
467
|
+
if (typeof current.status === "number") status ??= current.status;
|
|
468
|
+
status ??= /\(HTTP (\d{3})\)/.exec(current.message ?? "")?.[1];
|
|
469
|
+
if (typeof current.code === "string" && /^E[A-Z]+$/.test(current.code)) codes.add(current.code);
|
|
470
|
+
pending.push(current.cause, current.data?.cause, ...(current instanceof AggregateError ? current.errors : []));
|
|
471
|
+
}
|
|
472
|
+
const parts = [...(status ? [`HTTP ${status}`] : []), ...codes];
|
|
473
|
+
// server-manager appends the probe classification to the original message, keeping it as cause.
|
|
474
|
+
const suffix = err.cause instanceof Error && err.message.startsWith(err.cause.message) ? err.message.slice(err.cause.message.length) : "";
|
|
475
|
+
if (suffix.startsWith(" — probe: ")) parts.push(suffix.slice(3));
|
|
476
|
+
if (codes.has("ECONNREFUSED") && ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname)) {
|
|
477
|
+
const configured = definition.url.replace(/[?#].*$/s, "").replace(/^([a-z][a-z\d+.-]*:\/\/)[^/]*@/i, "$1");
|
|
478
|
+
parts.push(`Nothing is listening at ${configured}. Start the app or local process that serves this MCP server.`);
|
|
479
|
+
}
|
|
480
|
+
return parts.length > 0 ? parts.join(" — ") : "connection failed";
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function runDoctor(argv, log, error) {
|
|
484
|
+
const json = argv[0] === "--json";
|
|
485
|
+
if (argv.length > (json ? 1 : 0)) {
|
|
486
|
+
error("Usage: pi-mcp-adapter doctor [--json]");
|
|
487
|
+
return 1;
|
|
488
|
+
}
|
|
489
|
+
let config, trust, manager, auth, authFlow, utils, agentDir;
|
|
490
|
+
try {
|
|
491
|
+
[config, trust, manager, auth, authFlow, utils, agentDir] = await Promise.all([
|
|
492
|
+
import("./dist/config.js"),
|
|
493
|
+
import("./dist/project-server-trust.js"),
|
|
494
|
+
import("./dist/server-manager.js"),
|
|
495
|
+
import("./dist/mcp-auth.js"),
|
|
496
|
+
import("./dist/mcp-auth-flow.js"),
|
|
497
|
+
import("./dist/utils.js"),
|
|
498
|
+
import("./dist/agent-dir.js"),
|
|
499
|
+
]);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
error("Unable to load doctor command modules.");
|
|
502
|
+
error(`Import failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
503
|
+
return 1;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const cwd = process.cwd();
|
|
507
|
+
// Pi 0.99+ owns ~/.pi/agent/mcp.json and .pi/mcp.json; older Pi used those names for adapter config.
|
|
508
|
+
const piVersion = spawnSync("pi", ["--version"], { encoding: "utf8", timeout: 5_000, shell: process.platform === "win32" }).stdout ?? "";
|
|
509
|
+
const [major, minor] = piVersion.trim().split(".").map(Number);
|
|
510
|
+
const piSupportsMcp = major > 0 || minor >= 99;
|
|
511
|
+
config.setPiMcpConfigEnabled(piSupportsMcp);
|
|
512
|
+
if (!piSupportsMcp && [config.getPiMcpGlobalConfigPath(), config.getProjectPiMcpConfigPath(cwd)].some((file) => fs.existsSync(file))) {
|
|
513
|
+
error("Pi 0.99 or later wasn't found on PATH, so Pi's own mcp.json files were not checked.");
|
|
514
|
+
}
|
|
515
|
+
const loaded = config.loadMcpConfigWithSources(undefined, cwd);
|
|
516
|
+
let projectTrusted = false;
|
|
517
|
+
if (loaded.projectServers.size > 0) {
|
|
518
|
+
try {
|
|
519
|
+
// Pi is a peer dependency and may not resolve from where this CLI is installed.
|
|
520
|
+
const { ProjectTrustStore } = await import("@earendil-works/pi-coding-agent");
|
|
521
|
+
projectTrusted = new ProjectTrustStore(agentDir.getAgentDir()).get(cwd) === true;
|
|
522
|
+
} catch (err) {
|
|
523
|
+
error(`Could not read Pi's project trust, so project servers are treated as untrusted: ${utils.formatTerminalError(err)}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const { config: effective, blockedServers } = await trust.applyProjectServerTrust(loaded, {
|
|
527
|
+
cwd,
|
|
528
|
+
hasUI: false,
|
|
529
|
+
isProjectTrusted: () => projectTrusted,
|
|
530
|
+
});
|
|
531
|
+
const authOptions = auth.getAuthStorageOptions(effective.settings?.oauthDir, cwd, effective.settings?.oauthCredentialStore);
|
|
532
|
+
const serverManager = new manager.McpServerManager(cwd);
|
|
533
|
+
serverManager.setDefaultRequestTimeoutMs(effective.settings?.requestTimeoutMs);
|
|
534
|
+
serverManager.setAuthStorageOptions(authOptions);
|
|
535
|
+
|
|
536
|
+
const check = async (name, definition, signal) => {
|
|
537
|
+
const block = blockedServers.get(name);
|
|
538
|
+
if (block) return { state: "blocked", message: trust.describeProjectServerBlock(block.reason) };
|
|
539
|
+
if (definition.disabled === true) return { state: "disabled" };
|
|
540
|
+
// The doctor has no Pi model registry to read the provider token from, so it never connects these servers.
|
|
541
|
+
if (definition.url && typeof definition.auth === "object") {
|
|
542
|
+
return { state: "needs-auth", message: `signs in with a Pi provider: run /login ${definition.auth.provider} in Pi` };
|
|
543
|
+
}
|
|
544
|
+
const needsSignIn = { state: "needs-auth", message: `sign-in required: run /mcp-auth ${name} in Pi` };
|
|
545
|
+
let anonymous = false;
|
|
546
|
+
let url;
|
|
547
|
+
if (definition.url) {
|
|
548
|
+
try {
|
|
549
|
+
url = utils.resolveServerUrl(definition);
|
|
550
|
+
} catch {
|
|
551
|
+
const missing = typeof definition.url === "string" ? utils.getMissingEnvVars(definition.url) : [];
|
|
552
|
+
return { state: "failed", message: missing.length > 0 ? `URL uses unset environment variables: ${missing.join(", ")}` : "URL is invalid" };
|
|
553
|
+
}
|
|
554
|
+
// Report a missing sign-in from stored credentials instead of starting OAuth.
|
|
555
|
+
if (authFlow.supportsOAuth(definition)) {
|
|
556
|
+
const stored = auth.inspectAuthForUrl(name, url, authOptions);
|
|
557
|
+
if (stored.status === "unavailable") return { state: "failed", message: stored.message };
|
|
558
|
+
// A record without tokens is an unfinished sign-in.
|
|
559
|
+
if (stored.status === "absent" || !(stored.entry.tokens?.accessToken || stored.entry.tokens?.refreshToken)) {
|
|
560
|
+
if (definition.auth === "oauth") return needsSignIn;
|
|
561
|
+
anonymous = true;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Anonymous attempts get no OAuth provider, and debug stderr would bypass redaction.
|
|
566
|
+
const connectDefinition = { ...definition, debug: false, ...(anonymous ? { oauth: false } : {}) };
|
|
567
|
+
try {
|
|
568
|
+
const connection = await serverManager.connect(name, connectDefinition, signal);
|
|
569
|
+
return connection.status === "needs-auth" ? needsSignIn : { state: "ok", tools: connection.tools.length };
|
|
570
|
+
} catch (err) {
|
|
571
|
+
if (anonymous && [err, err?.cause].some(manager.isUnauthorizedHttpError)) return needsSignIn;
|
|
572
|
+
return { state: "failed", message: describeConnectError(err, definition, url) };
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const results = await Promise.all(Object.entries(effective.mcpServers).map(async ([name, definition]) => {
|
|
577
|
+
// The deadline covers the whole check, including server-manager's follow-up probe.
|
|
578
|
+
const controller = new AbortController();
|
|
579
|
+
let timer;
|
|
580
|
+
const timedOut = new Promise((resolve) => {
|
|
581
|
+
timer = setTimeout(() => {
|
|
582
|
+
controller.abort();
|
|
583
|
+
resolve({ state: "failed", message: `no response within ${DOCTOR_CONNECT_TIMEOUT_MS / 1000}s` });
|
|
584
|
+
}, DOCTOR_CONNECT_TIMEOUT_MS);
|
|
585
|
+
});
|
|
586
|
+
const result = await Promise.race([
|
|
587
|
+
check(name, definition, controller.signal).catch((err) => ({ state: "failed", message: utils.formatTerminalError(err) })),
|
|
588
|
+
timedOut,
|
|
589
|
+
]);
|
|
590
|
+
clearTimeout(timer);
|
|
591
|
+
return {
|
|
592
|
+
name,
|
|
593
|
+
state: result.state,
|
|
594
|
+
tools: result.tools ?? null,
|
|
595
|
+
message: result.message ? utils.sanitizeTerminalText(redactDoctorMessage(result.message, definition, utils)) : null,
|
|
596
|
+
};
|
|
597
|
+
}));
|
|
598
|
+
|
|
599
|
+
// Report first: closing waits for timed-out attempts to settle and can fail.
|
|
600
|
+
let closed = true;
|
|
601
|
+
try {
|
|
602
|
+
if (json) {
|
|
603
|
+
log(JSON.stringify(results, null, 2));
|
|
604
|
+
} else {
|
|
605
|
+
if (results.length === 0) log("No MCP servers configured.");
|
|
606
|
+
for (const result of results) {
|
|
607
|
+
const tools = result.tools === null ? "" : `, ${result.tools} tool${result.tools === 1 ? "" : "s"}`;
|
|
608
|
+
log(`${utils.sanitizeTerminalText(result.name)}: ${result.state}${tools}${result.message ? ` — ${result.message}` : ""}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
} finally {
|
|
612
|
+
try {
|
|
613
|
+
await serverManager.closeAll();
|
|
614
|
+
} catch (err) {
|
|
615
|
+
closed = false;
|
|
616
|
+
error(`Some MCP connections did not close cleanly: ${utils.formatTerminalError(err)}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return !closed || results.some((result) => result.state === "failed" || result.state === "needs-auth") ? 1 : 0;
|
|
620
|
+
}
|
|
621
|
+
|
|
416
622
|
export async function main(argv = process.argv.slice(2), log = console.log, error = console.error, stdin = process.stdin) {
|
|
417
623
|
const [command, ...rest] = argv;
|
|
418
624
|
|
|
@@ -429,6 +635,10 @@ export async function main(argv = process.argv.slice(2), log = console.log, erro
|
|
|
429
635
|
return runKey(rest, log, error, stdin);
|
|
430
636
|
}
|
|
431
637
|
|
|
638
|
+
if (command === "doctor") {
|
|
639
|
+
return runDoctor(rest, log, error);
|
|
640
|
+
}
|
|
641
|
+
|
|
432
642
|
if (command === "install") {
|
|
433
643
|
error("The custom downloader has been retired.");
|
|
434
644
|
error("Use `pi install npm:pi-mcp-adapter` instead, then optionally run `pi-mcp-adapter init`.");
|
package/commands.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createConnection } from "node:net";
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
5
|
import type { OverlayHandle } from "@earendil-works/pi-tui";
|
|
4
6
|
import type { McpExtensionState } from "./state.ts";
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
type KnownServerPreset,
|
|
12
14
|
type SharedConfigTarget,
|
|
13
15
|
getServerProvenance,
|
|
16
|
+
loadMcpConfig,
|
|
14
17
|
previewCompatibilityImports,
|
|
15
18
|
previewSharedServerEntry,
|
|
16
19
|
previewStarterSharedConfig,
|
|
@@ -29,7 +32,8 @@ import { supportsOAuth, authenticate, removeAuth, type McpOAuthRuntime } from ".
|
|
|
29
32
|
import { getAuthStorageOptions, inspectAuthForUrl } from "./mcp-auth.ts";
|
|
30
33
|
import { inspectBearerTokenForUrl, removeBearerToken } from "./mcp-bearer-store.ts";
|
|
31
34
|
import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.ts";
|
|
32
|
-
import {
|
|
35
|
+
import { findPiSignInImports, importPiSignIn } from "./pi-signin-import.ts";
|
|
36
|
+
import { formatTerminalError, openPath, providerSignInMessage, resolveServerUrl, sanitizeTerminalText } from "./utils.ts";
|
|
33
37
|
import { isAbortError } from "./runtime-owner.ts";
|
|
34
38
|
import { resolveJevCredential } from "./jev-key-store.ts";
|
|
35
39
|
import { describeProjectServerBlock } from "./project-server-trust.ts";
|
|
@@ -302,7 +306,9 @@ export async function reconnectServer(
|
|
|
302
306
|
state.owner?.throwIfInactive();
|
|
303
307
|
if (connection.status === "needs-auth") {
|
|
304
308
|
if (ui) {
|
|
305
|
-
ui.notify(
|
|
309
|
+
ui.notify(typeof definition.auth === "object"
|
|
310
|
+
? `MCP: ${providerSignInMessage(name, definition.auth.provider)}`
|
|
311
|
+
: `MCP: ${name} requires OAuth. Run /mcp-auth ${name} first.`, "warning");
|
|
306
312
|
}
|
|
307
313
|
updateStatusBar(state);
|
|
308
314
|
return false;
|
|
@@ -392,6 +398,11 @@ export async function authenticateServer(
|
|
|
392
398
|
return { ok: false, message };
|
|
393
399
|
}
|
|
394
400
|
|
|
401
|
+
if (typeof definition.auth === "object") {
|
|
402
|
+
const message = providerSignInMessage(serverName, definition.auth.provider);
|
|
403
|
+
ui.notify(message, "info");
|
|
404
|
+
return { ok: false, message };
|
|
405
|
+
}
|
|
395
406
|
if (!supportsOAuth(definition)) {
|
|
396
407
|
const message = `Server "${serverName}" does not use OAuth authentication. Set "auth": "oauth" or omit auth for auto-detection.`;
|
|
397
408
|
ui.notify(
|
|
@@ -587,6 +598,21 @@ function buildSharedConfigNoticeLines(configOverridePath: string | undefined, cw
|
|
|
587
598
|
};
|
|
588
599
|
}
|
|
589
600
|
|
|
601
|
+
// Any accepted TCP connection counts as reachable; failures resolve false so the add still succeeds.
|
|
602
|
+
function isLocalServerReachable(url: string, timeoutMs = 1_500): Promise<boolean> {
|
|
603
|
+
const { hostname, port } = new URL(url);
|
|
604
|
+
return new Promise((resolve) => {
|
|
605
|
+
const socket = createConnection({ host: hostname, port: Number(port) });
|
|
606
|
+
const finish = (reachable: boolean) => {
|
|
607
|
+
socket.destroy();
|
|
608
|
+
resolve(reachable);
|
|
609
|
+
};
|
|
610
|
+
socket.setTimeout(timeoutMs, () => finish(false));
|
|
611
|
+
socket.once("connect", () => finish(true));
|
|
612
|
+
socket.once("error", () => finish(false));
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
590
616
|
export async function openMcpSetup(
|
|
591
617
|
state: McpExtensionState,
|
|
592
618
|
pi: ExtensionAPI,
|
|
@@ -641,7 +667,16 @@ export async function openMcpSetup(
|
|
|
641
667
|
addKnownServer: async (preset: KnownServerPreset, target: SharedConfigTarget) => {
|
|
642
668
|
const path = writeSharedServerEntry(getSharedConfigPath(target, ctx.cwd), preset.id, preset.entry);
|
|
643
669
|
configChanged = true;
|
|
644
|
-
|
|
670
|
+
// Merging is per field, so the entry is in effect when every preset field survives it and nothing disables it.
|
|
671
|
+
const active = loadMcpConfig(configOverridePath, ctx.cwd).mcpServers[preset.id];
|
|
672
|
+
const ignoredBecause = !active
|
|
673
|
+
? `the current config mode doesn't read ${path}`
|
|
674
|
+
: Object.entries(preset.entry).some(([field, value]) => !isDeepStrictEqual(active[field as keyof typeof active], value))
|
|
675
|
+
? `another config file also defines ${preset.id} and takes precedence`
|
|
676
|
+
: isServerDisabled(active) ? `another config file disables ${preset.id}` : undefined;
|
|
677
|
+
const result = { path, serverName: preset.name, ...(ignoredBecause ? { ignoredBecause } : {}) };
|
|
678
|
+
if (!preset.desktopApp) return result;
|
|
679
|
+
return { ...result, reachable: await isLocalServerReachable(preset.entry.url!) };
|
|
645
680
|
},
|
|
646
681
|
openPath: async (targetPath: string) => {
|
|
647
682
|
await openPath(pi, targetPath);
|
|
@@ -659,7 +694,8 @@ export async function openMcpSetup(
|
|
|
659
694
|
resolve({ configChanged });
|
|
660
695
|
});
|
|
661
696
|
},
|
|
662
|
-
|
|
697
|
+
// The panel sizes its height from tui.terminal.rows minus this margin, so it always fits.
|
|
698
|
+
{ overlay: true, overlayOptions: { anchor: "center", width: 92, maxHeight: "100%", margin: { top: 1, bottom: 1 } } },
|
|
663
699
|
);
|
|
664
700
|
});
|
|
665
701
|
}
|
|
@@ -766,6 +802,21 @@ export async function openMcpPanel(
|
|
|
766
802
|
|
|
767
803
|
const { createMcpPanel } = await import("./mcp-panel.ts");
|
|
768
804
|
let configChanged = false;
|
|
805
|
+
const authStorageOptions = state.authStorageOptions ?? {};
|
|
806
|
+
if (findPiSignInImports(config, authStorageOptions).length > 0) {
|
|
807
|
+
callbacks.importPiSignIns = () => {
|
|
808
|
+
const imported: string[] = [];
|
|
809
|
+
const failed: { server: string; error: string }[] = [];
|
|
810
|
+
for (const candidate of findPiSignInImports(config, authStorageOptions)) {
|
|
811
|
+
try {
|
|
812
|
+
if (importPiSignIn(candidate, authStorageOptions)) imported.push(candidate.serverName);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
failed.push({ server: candidate.serverName, error: error instanceof Error ? error.message : String(error) });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return { imported, failed };
|
|
818
|
+
};
|
|
819
|
+
}
|
|
769
820
|
|
|
770
821
|
await new Promise<void>((resolve) => {
|
|
771
822
|
ctx.ui.custom(
|