pi-soly 1.9.3 → 1.11.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/ask/index.ts +12 -11
- package/ask/picker.ts +356 -76
- package/ask/prompt.ts +6 -2
- package/ask/tests/picker.test.ts +273 -82
- package/codemap.ts +276 -0
- package/hotreload.ts +239 -0
- package/init.ts +302 -0
- package/mcp/CHANGELOG.md +384 -0
- package/mcp/LICENSE +21 -0
- package/mcp/OAUTH.md +355 -0
- package/mcp/README.md +410 -0
- package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
- package/mcp/__tests__/cli.upstream-test.ts +97 -0
- package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
- package/mcp/__tests__/config.upstream-test.ts +303 -0
- package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
- package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
- package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
- package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
- package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
- package/mcp/__tests__/errors.upstream-test.ts +218 -0
- package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
- package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
- package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
- package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
- package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
- package/mcp/__tests__/logger.upstream-test.ts +175 -0
- package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
- package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
- package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
- package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
- package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
- package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
- package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
- package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
- package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
- package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
- package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
- package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
- package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
- package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
- package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
- package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
- package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
- package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
- package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
- package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
- package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
- package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
- package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
- package/mcp/agent-dir.ts +20 -0
- package/mcp/app-bridge.bundle.js +67 -0
- package/mcp/cli.js +184 -0
- package/mcp/commands.ts +422 -0
- package/mcp/config.ts +666 -0
- package/mcp/consent-manager.ts +64 -0
- package/mcp/direct-tools.ts +439 -0
- package/mcp/elicitation-handler.ts +347 -0
- package/mcp/errors.ts +219 -0
- package/mcp/glimpse-ui.ts +80 -0
- package/mcp/host-html-template.ts +427 -0
- package/mcp/index.ts +362 -0
- package/mcp/init.ts +362 -0
- package/mcp/lifecycle.ts +93 -0
- package/mcp/logger.ts +169 -0
- package/mcp/mcp-auth-flow.ts +559 -0
- package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
- package/mcp/mcp-auth.ts +302 -0
- package/mcp/mcp-auth.upstream-test.ts +373 -0
- package/mcp/mcp-callback-server.ts +372 -0
- package/mcp/mcp-callback-server.upstream-test.ts +416 -0
- package/mcp/mcp-oauth-provider.ts +369 -0
- package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
- package/mcp/mcp-panel.ts +829 -0
- package/mcp/mcp-setup-panel.ts +580 -0
- package/mcp/metadata-cache.ts +201 -0
- package/mcp/notify.ts +111 -0
- package/mcp/npx-resolver.ts +424 -0
- package/mcp/oauth-handler.ts +57 -0
- package/mcp/onboarding-state.ts +68 -0
- package/mcp/package.json +106 -0
- package/mcp/panel-keys.ts +37 -0
- package/mcp/proxy-modes.ts +949 -0
- package/mcp/resource-tools.ts +17 -0
- package/mcp/sampling-handler.ts +268 -0
- package/mcp/server-manager.ts +545 -0
- package/mcp/state.ts +41 -0
- package/mcp/tool-metadata.ts +216 -0
- package/mcp/tool-registrar.ts +46 -0
- package/mcp/tool-result-renderer.ts +161 -0
- package/mcp/types.ts +448 -0
- package/mcp/ui-resource-handler.ts +146 -0
- package/mcp/ui-server.ts +623 -0
- package/mcp/ui-session.ts +386 -0
- package/mcp/ui-stream-types.ts +89 -0
- package/mcp/utils.ts +129 -0
- package/mcp/vitest.config.ts +14 -0
- package/migrate.ts +258 -0
- package/notification.ts +218 -0
- package/notifications-log.ts +83 -0
- package/package.json +20 -3
- package/status.ts +140 -0
package/mcp/cli.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const HOME = os.homedir();
|
|
9
|
+
|
|
10
|
+
function expandHome(input) {
|
|
11
|
+
if (input === "~") return HOME;
|
|
12
|
+
if (input.startsWith("~/")) return path.resolve(HOME, input.slice(2));
|
|
13
|
+
return path.resolve(input);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const AGENT_DIR = process.env.PI_CODING_AGENT_DIR?.trim()
|
|
17
|
+
? expandHome(process.env.PI_CODING_AGENT_DIR.trim())
|
|
18
|
+
: path.join(HOME, ".pi", "agent");
|
|
19
|
+
const PI_CONFIG_PATH = path.join(AGENT_DIR, "mcp.json");
|
|
20
|
+
const GENERIC_GLOBAL_CONFIG_PATH = path.join(HOME, ".config", "mcp", "mcp.json");
|
|
21
|
+
const PROJECT_CONFIG_PATH = path.resolve(process.cwd(), ".mcp.json");
|
|
22
|
+
const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), ".pi", "mcp.json");
|
|
23
|
+
|
|
24
|
+
const IMPORT_PATHS = {
|
|
25
|
+
cursor: [path.join(HOME, ".cursor", "mcp.json")],
|
|
26
|
+
"claude-code": [
|
|
27
|
+
path.join(HOME, ".claude", "mcp.json"),
|
|
28
|
+
path.join(HOME, ".claude.json"),
|
|
29
|
+
path.join(HOME, ".claude", "claude_desktop_config.json"),
|
|
30
|
+
],
|
|
31
|
+
"claude-desktop": [path.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json")],
|
|
32
|
+
codex: [path.join(HOME, ".codex", "config.json")],
|
|
33
|
+
windsurf: [path.join(HOME, ".windsurf", "mcp.json")],
|
|
34
|
+
vscode: [path.resolve(process.cwd(), ".vscode", "mcp.json")],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function printHelp(log = console.log) {
|
|
38
|
+
log("pi-mcp-adapter helper\n");
|
|
39
|
+
log("Install the package with:");
|
|
40
|
+
log(" pi install npm:pi-mcp-adapter\n");
|
|
41
|
+
log("Then optionally run:");
|
|
42
|
+
log(" pi-mcp-adapter init Detect host configs and scaffold Pi imports");
|
|
43
|
+
log(" pi-mcp-adapter init --dry-run");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJsonFile(filePath) {
|
|
47
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadPiConfig() {
|
|
51
|
+
if (!fs.existsSync(PI_CONFIG_PATH)) {
|
|
52
|
+
return { mcpServers: {} };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const raw = readJsonFile(PI_CONFIG_PATH);
|
|
56
|
+
const mcpServers = raw.mcpServers ?? raw["mcp-servers"] ?? {};
|
|
57
|
+
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
58
|
+
throw new Error(`Invalid MCP config at ${PI_CONFIG_PATH}: expected \"mcpServers\" to be an object`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const normalized = { ...raw };
|
|
62
|
+
delete normalized["mcp-servers"];
|
|
63
|
+
|
|
64
|
+
const imports = Array.isArray(raw.imports) ? raw.imports.filter((value) => typeof value === "string") : undefined;
|
|
65
|
+
return {
|
|
66
|
+
...normalized,
|
|
67
|
+
mcpServers,
|
|
68
|
+
imports,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findAvailableImports() {
|
|
73
|
+
const found = [];
|
|
74
|
+
|
|
75
|
+
for (const [kind, candidates] of Object.entries(IMPORT_PATHS)) {
|
|
76
|
+
const existing = candidates.find((candidate) => fs.existsSync(candidate));
|
|
77
|
+
if (existing) {
|
|
78
|
+
found.push({ kind, path: existing });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return found;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function printDiscovery(log, imports) {
|
|
86
|
+
log("Config discovery:\n");
|
|
87
|
+
|
|
88
|
+
const paths = [
|
|
89
|
+
["User-global standard MCP", GENERIC_GLOBAL_CONFIG_PATH],
|
|
90
|
+
["Pi global override", PI_CONFIG_PATH],
|
|
91
|
+
["Project standard MCP", PROJECT_CONFIG_PATH],
|
|
92
|
+
["Project Pi override", PROJECT_PI_CONFIG_PATH],
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const [label, filePath] of paths) {
|
|
96
|
+
const prefix = fs.existsSync(filePath) ? "✓" : "-";
|
|
97
|
+
log(`${prefix} ${label}: ${filePath}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
log("\nCompatibility imports:\n");
|
|
101
|
+
if (imports.length === 0) {
|
|
102
|
+
log("- No host-specific MCP configs detected");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const entry of imports) {
|
|
107
|
+
log(`✓ ${entry.kind}: ${entry.path}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function writePiConfig(config) {
|
|
112
|
+
fs.mkdirSync(path.dirname(PI_CONFIG_PATH), { recursive: true });
|
|
113
|
+
fs.writeFileSync(PI_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function runInit(argv, log = console.log) {
|
|
117
|
+
const dryRun = argv.includes("--dry-run");
|
|
118
|
+
const foundImports = findAvailableImports();
|
|
119
|
+
const existingConfig = loadPiConfig();
|
|
120
|
+
const existingImports = new Set(existingConfig.imports ?? []);
|
|
121
|
+
const importsToAdd = foundImports
|
|
122
|
+
.map((entry) => entry.kind)
|
|
123
|
+
.filter((kind) => !existingImports.has(kind));
|
|
124
|
+
|
|
125
|
+
printDiscovery(log, foundImports);
|
|
126
|
+
|
|
127
|
+
if (importsToAdd.length === 0) {
|
|
128
|
+
log("\nNo Pi config changes needed.");
|
|
129
|
+
log("Standard MCP configs are discovered automatically, and host-specific imports are already configured or unavailable.");
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const nextConfig = {
|
|
134
|
+
...existingConfig,
|
|
135
|
+
imports: [...existingImports, ...importsToAdd],
|
|
136
|
+
mcpServers: existingConfig.mcpServers ?? {},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
log(`\nDetected host configs to import into Pi: ${importsToAdd.join(", ")}`);
|
|
140
|
+
|
|
141
|
+
if (dryRun) {
|
|
142
|
+
log(`Dry run: would update ${PI_CONFIG_PATH}`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
writePiConfig(nextConfig);
|
|
147
|
+
log(`Updated ${PI_CONFIG_PATH}`);
|
|
148
|
+
log("Pi will now keep reading standard MCP configs automatically, while these imports cover host-specific config formats.");
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function main(argv = process.argv.slice(2), log = console.log, error = console.error) {
|
|
153
|
+
const [command, ...rest] = argv;
|
|
154
|
+
|
|
155
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
156
|
+
printHelp(log);
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (command === "install") {
|
|
161
|
+
error("The custom downloader has been retired.");
|
|
162
|
+
error("Use `pi install npm:pi-mcp-adapter` instead, then optionally run `pi-mcp-adapter init`.");
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (command === "init") {
|
|
167
|
+
return runInit(rest, log);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
error(`Unknown command: ${command}`);
|
|
171
|
+
printHelp(log);
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const isEntrypoint = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
176
|
+
|
|
177
|
+
if (isEntrypoint) {
|
|
178
|
+
main().then((code) => {
|
|
179
|
+
process.exitCode = code;
|
|
180
|
+
}).catch((err) => {
|
|
181
|
+
console.error(`\nHelper failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
});
|
|
184
|
+
}
|
package/mcp/commands.ts
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { McpExtensionState } from "./state.ts";
|
|
3
|
+
import type { McpAuthResult, McpConfig, ServerEntry, McpPanelCallbacks, McpPanelResult, ImportKind } from "./types.ts";
|
|
4
|
+
import {
|
|
5
|
+
ensureCompatibilityImports,
|
|
6
|
+
getMcpDiscoverySummary,
|
|
7
|
+
getServerProvenance,
|
|
8
|
+
previewCompatibilityImports,
|
|
9
|
+
previewSharedServerEntry,
|
|
10
|
+
previewStarterProjectConfig,
|
|
11
|
+
writeDirectToolsConfig,
|
|
12
|
+
writeSharedServerEntry,
|
|
13
|
+
writeStarterProjectConfig,
|
|
14
|
+
} from "./config.ts";
|
|
15
|
+
import { lazyConnect, updateMetadataCache, updateStatusBar, getFailureAgeSeconds } from "./init.ts";
|
|
16
|
+
import { loadMetadataCache } from "./metadata-cache.ts";
|
|
17
|
+
import { buildToolMetadata } from "./tool-metadata.ts";
|
|
18
|
+
import { supportsOAuth, authenticate, removeAuth } from "./mcp-auth-flow.ts";
|
|
19
|
+
import { getAuthForUrl } from "./mcp-auth.ts";
|
|
20
|
+
import { notifyReconnectFailed } from "./notify.ts";
|
|
21
|
+
import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.ts";
|
|
22
|
+
import { openPath } from "./utils.ts";
|
|
23
|
+
|
|
24
|
+
export async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
|
|
25
|
+
if (!ctx.hasUI) return;
|
|
26
|
+
|
|
27
|
+
const lines: string[] = ["MCP Server Status:", ""];
|
|
28
|
+
|
|
29
|
+
for (const name of Object.keys(state.config.mcpServers)) {
|
|
30
|
+
const connection = state.manager.getConnection(name);
|
|
31
|
+
const metadata = state.toolMetadata.get(name);
|
|
32
|
+
const toolCount = metadata?.length ?? 0;
|
|
33
|
+
const failedAgo = getFailureAgeSeconds(state, name);
|
|
34
|
+
let status = "not connected";
|
|
35
|
+
let statusIcon = "○";
|
|
36
|
+
let failed = false;
|
|
37
|
+
|
|
38
|
+
if (connection?.status === "connected") {
|
|
39
|
+
status = "connected";
|
|
40
|
+
statusIcon = "✓";
|
|
41
|
+
} else if (connection?.status === "needs-auth") {
|
|
42
|
+
status = "needs auth";
|
|
43
|
+
statusIcon = "⚠";
|
|
44
|
+
} else if (failedAgo !== null) {
|
|
45
|
+
status = `failed ${failedAgo}s ago`;
|
|
46
|
+
statusIcon = "✗";
|
|
47
|
+
failed = true;
|
|
48
|
+
} else if (metadata !== undefined) {
|
|
49
|
+
status = "cached";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const toolSuffix = failed ? "" : ` (${toolCount} tools${status === "cached" ? ", cached" : ""})`;
|
|
53
|
+
lines.push(`${statusIcon} ${name}: ${status}${toolSuffix}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Object.keys(state.config.mcpServers).length === 0) {
|
|
57
|
+
lines.push("No MCP servers configured");
|
|
58
|
+
lines.push("Run /mcp setup to adopt imports or scaffold a starter .mcp.json");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function showTools(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
|
|
65
|
+
if (!ctx.hasUI) return;
|
|
66
|
+
|
|
67
|
+
const allTools = [...state.toolMetadata.values()].flat().map(m => m.name);
|
|
68
|
+
|
|
69
|
+
if (allTools.length === 0) {
|
|
70
|
+
ctx.ui.notify("No MCP tools available", "info");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const lines = [
|
|
75
|
+
"MCP Tools:",
|
|
76
|
+
"",
|
|
77
|
+
...allTools.map(t => ` ${t}`),
|
|
78
|
+
"",
|
|
79
|
+
`Total: ${allTools.length} tools`,
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function reconnectServers(
|
|
86
|
+
state: McpExtensionState,
|
|
87
|
+
ctx: ExtensionContext,
|
|
88
|
+
targetServer?: string
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
if (targetServer && !state.config.mcpServers[targetServer]) {
|
|
91
|
+
if (ctx.hasUI) {
|
|
92
|
+
ctx.ui.notify(`Server "${targetServer}" not found in config`, "error");
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const entries = targetServer
|
|
98
|
+
? [[targetServer, state.config.mcpServers[targetServer]] as [string, ServerEntry]]
|
|
99
|
+
: Object.entries(state.config.mcpServers);
|
|
100
|
+
|
|
101
|
+
for (const [name, definition] of entries) {
|
|
102
|
+
try {
|
|
103
|
+
await state.manager.close(name);
|
|
104
|
+
|
|
105
|
+
const connection = await state.manager.connect(name, definition);
|
|
106
|
+
if (connection.status === "needs-auth") {
|
|
107
|
+
if (ctx.hasUI) {
|
|
108
|
+
ctx.ui.notify(`MCP: ${name} requires OAuth. Run /mcp-auth ${name} first.`, "warning");
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
113
|
+
|
|
114
|
+
const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
|
|
115
|
+
state.toolMetadata.set(name, metadata);
|
|
116
|
+
updateMetadataCache(state, name);
|
|
117
|
+
state.failureTracker.delete(name);
|
|
118
|
+
|
|
119
|
+
if (ctx.hasUI) {
|
|
120
|
+
ctx.ui.notify(
|
|
121
|
+
`MCP: Reconnected to ${name} (${connection.tools.length} tools, ${connection.resources.length} resources)`,
|
|
122
|
+
"info"
|
|
123
|
+
);
|
|
124
|
+
if (failedTools.length > 0) {
|
|
125
|
+
ctx.ui.notify(`MCP: ${name} - ${failedTools.length} tools skipped`, "warning");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
+
state.failureTracker.set(name, Date.now());
|
|
131
|
+
if (ctx.hasUI) {
|
|
132
|
+
notifyReconnectFailed(ctx.ui, name, message);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
updateStatusBar(state);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function authenticateServer(
|
|
141
|
+
serverName: string,
|
|
142
|
+
config: McpConfig,
|
|
143
|
+
ctx: ExtensionContext
|
|
144
|
+
): Promise<McpAuthResult> {
|
|
145
|
+
if (!ctx.hasUI) return { ok: false, message: "OAuth authentication requires an interactive session." };
|
|
146
|
+
|
|
147
|
+
const definition = config.mcpServers[serverName];
|
|
148
|
+
if (!definition) {
|
|
149
|
+
const message = `Server "${serverName}" not found in config`;
|
|
150
|
+
ctx.ui.notify(message, "error");
|
|
151
|
+
return { ok: false, message };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!supportsOAuth(definition)) {
|
|
155
|
+
const message = `Server "${serverName}" does not use OAuth authentication. Set "auth": "oauth" or omit auth for auto-detection.`;
|
|
156
|
+
ctx.ui.notify(
|
|
157
|
+
`Server "${serverName}" does not use OAuth authentication.\n` +
|
|
158
|
+
`Set "auth": "oauth" or omit auth for auto-detection.`,
|
|
159
|
+
"error"
|
|
160
|
+
);
|
|
161
|
+
return { ok: false, message };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!definition.url) {
|
|
165
|
+
const message = `Server "${serverName}" has no URL configured (OAuth requires HTTP transport)`;
|
|
166
|
+
ctx.ui.notify(message, "error");
|
|
167
|
+
return { ok: false, message };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
ctx.ui.setStatus("mcp-auth", `Authenticating ${serverName}...`);
|
|
172
|
+
const status = await authenticate(serverName, definition.url, definition);
|
|
173
|
+
|
|
174
|
+
if (status === "authenticated") {
|
|
175
|
+
const message = `OAuth authentication successful for "${serverName}"! Run /mcp reconnect ${serverName} to connect with the new token.`;
|
|
176
|
+
ctx.ui.notify(
|
|
177
|
+
`OAuth authentication successful for "${serverName}"!\n` +
|
|
178
|
+
`Run /mcp reconnect ${serverName} to connect with the new token.`,
|
|
179
|
+
"info"
|
|
180
|
+
);
|
|
181
|
+
return { ok: true, message };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const message = `OAuth authentication failed for "${serverName}".`;
|
|
185
|
+
ctx.ui.notify(message, "error");
|
|
186
|
+
return { ok: false, message };
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
189
|
+
ctx.ui.notify(`Failed to authenticate "${serverName}": ${message}`, "error");
|
|
190
|
+
return { ok: false, message };
|
|
191
|
+
} finally {
|
|
192
|
+
ctx.ui.setStatus("mcp-auth", undefined);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function logoutServer(
|
|
197
|
+
serverName: string,
|
|
198
|
+
state: McpExtensionState,
|
|
199
|
+
ctx: ExtensionContext
|
|
200
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
201
|
+
const definition = state.config.mcpServers[serverName];
|
|
202
|
+
if (!definition) {
|
|
203
|
+
const message = `Server "${serverName}" not found in config`;
|
|
204
|
+
if (ctx.hasUI) ctx.ui.notify(message, "error");
|
|
205
|
+
return { ok: false, message };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await removeAuth(serverName);
|
|
209
|
+
await state.manager.close(serverName);
|
|
210
|
+
updateStatusBar(state);
|
|
211
|
+
|
|
212
|
+
const message = `OAuth credentials cleared for "${serverName}". Run /mcp-auth ${serverName} to authenticate again.`;
|
|
213
|
+
if (ctx.hasUI) ctx.ui.notify(message, "info");
|
|
214
|
+
return { ok: true, message };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface PanelFlowResult {
|
|
218
|
+
configChanged: boolean;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildSharedConfigNoticeLines(configOverridePath: string | undefined, cwd: string): { lines: string[]; fingerprint: string | null } {
|
|
222
|
+
const discovery = getMcpDiscoverySummary(configOverridePath, cwd);
|
|
223
|
+
const onboardingState = loadOnboardingState();
|
|
224
|
+
if (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {
|
|
225
|
+
return { lines: [], fingerprint: null };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const sharedSources = discovery.sources.filter((source) => source.kind === "shared" && source.serverCount > 0);
|
|
229
|
+
const sourceList = sharedSources.map((source) => source.path).join(", ");
|
|
230
|
+
return {
|
|
231
|
+
lines: [
|
|
232
|
+
`Using standard MCP config from ${sourceList}.`,
|
|
233
|
+
"Pi only writes compatibility imports and adapter-specific overrides into Pi-owned files when needed.",
|
|
234
|
+
],
|
|
235
|
+
fingerprint: discovery.fingerprint,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function openMcpSetup(
|
|
240
|
+
_state: McpExtensionState,
|
|
241
|
+
pi: ExtensionAPI,
|
|
242
|
+
ctx: ExtensionContext,
|
|
243
|
+
configOverridePath?: string,
|
|
244
|
+
mode: "empty" | "setup" = "setup",
|
|
245
|
+
): Promise<PanelFlowResult> {
|
|
246
|
+
if (!ctx.hasUI) return { configChanged: false };
|
|
247
|
+
|
|
248
|
+
const discovery = getMcpDiscoverySummary(configOverridePath, ctx.cwd);
|
|
249
|
+
const onboardingState = loadOnboardingState();
|
|
250
|
+
const { createMcpSetupPanel } = await import("./mcp-setup-panel.ts");
|
|
251
|
+
let configChanged = false;
|
|
252
|
+
|
|
253
|
+
const callbacks = {
|
|
254
|
+
previewImports: (imports: ImportKind[]) => previewCompatibilityImports(imports, configOverridePath),
|
|
255
|
+
previewStarterProject: () => previewStarterProjectConfig(ctx.cwd),
|
|
256
|
+
previewRepoPrompt: () => {
|
|
257
|
+
const repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd).repoPrompt;
|
|
258
|
+
if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) return null;
|
|
259
|
+
return previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
|
|
260
|
+
},
|
|
261
|
+
adoptImports: async (imports: ImportKind[]) => {
|
|
262
|
+
const result = ensureCompatibilityImports(imports, configOverridePath);
|
|
263
|
+
if (result.added.length > 0) configChanged = true;
|
|
264
|
+
return result;
|
|
265
|
+
},
|
|
266
|
+
scaffoldProjectConfig: async () => {
|
|
267
|
+
const path = writeStarterProjectConfig(ctx.cwd);
|
|
268
|
+
configChanged = true;
|
|
269
|
+
return { path };
|
|
270
|
+
},
|
|
271
|
+
addRepoPrompt: async () => {
|
|
272
|
+
const repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd).repoPrompt;
|
|
273
|
+
if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {
|
|
274
|
+
throw new Error("RepoPrompt is not available to add from this setup screen.");
|
|
275
|
+
}
|
|
276
|
+
const path = writeSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
|
|
277
|
+
configChanged = true;
|
|
278
|
+
return { path, serverName: repoPrompt.serverName };
|
|
279
|
+
},
|
|
280
|
+
openPath: async (targetPath: string) => {
|
|
281
|
+
await openPath(pi, targetPath);
|
|
282
|
+
},
|
|
283
|
+
markSetupCompleted: () => {
|
|
284
|
+
persistSetupCompleted(discovery.fingerprint);
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return new Promise<PanelFlowResult>((resolve) => {
|
|
289
|
+
ctx.ui.custom(
|
|
290
|
+
(tui, _theme, keybindings, done) => {
|
|
291
|
+
return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings }, tui, () => {
|
|
292
|
+
done(undefined);
|
|
293
|
+
resolve({ configChanged });
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
{ overlay: true, overlayOptions: { anchor: "center", width: 92 } },
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildMcpPanelCallbacks(
|
|
302
|
+
state: McpExtensionState,
|
|
303
|
+
config: McpConfig,
|
|
304
|
+
ctx: ExtensionContext,
|
|
305
|
+
): McpPanelCallbacks {
|
|
306
|
+
return {
|
|
307
|
+
reconnect: (serverName: string) => lazyConnect(state, serverName),
|
|
308
|
+
canAuthenticate: (serverName: string) => {
|
|
309
|
+
const definition = config.mcpServers[serverName];
|
|
310
|
+
return definition ? supportsOAuth(definition) : false;
|
|
311
|
+
},
|
|
312
|
+
authenticate: (serverName: string) => authenticateServer(serverName, config, ctx),
|
|
313
|
+
getConnectionStatus: (serverName: string) => {
|
|
314
|
+
const definition = config.mcpServers[serverName];
|
|
315
|
+
const connection = state.manager.getConnection(serverName);
|
|
316
|
+
if (connection?.status === "needs-auth") {
|
|
317
|
+
return "needs-auth";
|
|
318
|
+
}
|
|
319
|
+
if (
|
|
320
|
+
definition?.auth === "oauth"
|
|
321
|
+
&& definition.url
|
|
322
|
+
&& definition.oauth !== false
|
|
323
|
+
&& definition.oauth?.grantType !== "client_credentials"
|
|
324
|
+
&& !getAuthForUrl(serverName, definition.url)?.tokens
|
|
325
|
+
) {
|
|
326
|
+
return "needs-auth";
|
|
327
|
+
}
|
|
328
|
+
if (connection?.status === "connected") return "connected";
|
|
329
|
+
if (getFailureAgeSeconds(state, serverName) !== null) return "failed";
|
|
330
|
+
return "idle";
|
|
331
|
+
},
|
|
332
|
+
refreshCacheAfterReconnect: (serverName: string) => {
|
|
333
|
+
const freshCache = loadMetadataCache();
|
|
334
|
+
return freshCache?.servers?.[serverName] ?? null;
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function openMcpPanel(
|
|
340
|
+
state: McpExtensionState,
|
|
341
|
+
pi: ExtensionAPI,
|
|
342
|
+
ctx: ExtensionContext,
|
|
343
|
+
configOverridePath?: string,
|
|
344
|
+
): Promise<PanelFlowResult> {
|
|
345
|
+
if (Object.keys(state.config.mcpServers).length === 0) {
|
|
346
|
+
return openMcpSetup(state, pi, ctx, configOverridePath, "empty");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const config = state.config;
|
|
350
|
+
const cache = loadMetadataCache();
|
|
351
|
+
const configPath = pi.getFlag("mcp-config") as string | undefined ?? configOverridePath;
|
|
352
|
+
const provenanceMap = getServerProvenance(configPath, ctx.cwd);
|
|
353
|
+
const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);
|
|
354
|
+
|
|
355
|
+
const callbacks = buildMcpPanelCallbacks(state, config, ctx);
|
|
356
|
+
|
|
357
|
+
const { createMcpPanel } = await import("./mcp-panel.ts");
|
|
358
|
+
let configChanged = false;
|
|
359
|
+
|
|
360
|
+
await new Promise<void>((resolve) => {
|
|
361
|
+
ctx.ui.custom(
|
|
362
|
+
(tui, _theme, keybindings, done) => {
|
|
363
|
+
return createMcpPanel(config, cache, provenanceMap, callbacks, tui, (result: McpPanelResult) => {
|
|
364
|
+
if (!result.cancelled && result.changes.size > 0) {
|
|
365
|
+
writeDirectToolsConfig(result.changes, provenanceMap, config);
|
|
366
|
+
configChanged = true;
|
|
367
|
+
ctx.ui.notify("Direct tools updated. Pi will reload after this panel closes.", "info");
|
|
368
|
+
}
|
|
369
|
+
done(undefined);
|
|
370
|
+
resolve();
|
|
371
|
+
}, { noticeLines, keybindings });
|
|
372
|
+
},
|
|
373
|
+
{ overlay: true, overlayOptions: { anchor: "center", width: 82 } },
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (noticeLines.length > 0 && fingerprint) {
|
|
378
|
+
markSharedConfigHintShown(fingerprint);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { configChanged };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function openMcpAuthPanel(
|
|
385
|
+
state: McpExtensionState,
|
|
386
|
+
pi: ExtensionAPI,
|
|
387
|
+
ctx: ExtensionContext,
|
|
388
|
+
configOverridePath?: string,
|
|
389
|
+
): Promise<PanelFlowResult> {
|
|
390
|
+
if (!ctx.hasUI) return { configChanged: false };
|
|
391
|
+
|
|
392
|
+
const config = state.config;
|
|
393
|
+
const oauthServers = Object.entries(config.mcpServers).filter(([, definition]) => supportsOAuth(definition));
|
|
394
|
+
if (oauthServers.length === 0) {
|
|
395
|
+
ctx.ui.notify("No OAuth-capable MCP servers are configured.", "warning");
|
|
396
|
+
return { configChanged: false };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const cache = loadMetadataCache();
|
|
400
|
+
const configPath = pi.getFlag("mcp-config") as string | undefined ?? configOverridePath;
|
|
401
|
+
const provenanceMap = getServerProvenance(configPath, ctx.cwd);
|
|
402
|
+
const callbacks = buildMcpPanelCallbacks(state, config, ctx);
|
|
403
|
+
const { createMcpPanel } = await import("./mcp-panel.ts");
|
|
404
|
+
|
|
405
|
+
await new Promise<void>((resolve) => {
|
|
406
|
+
ctx.ui.custom(
|
|
407
|
+
(tui, _theme, keybindings, done) => {
|
|
408
|
+
return createMcpPanel(config, cache, provenanceMap, callbacks, tui, () => {
|
|
409
|
+
done(undefined);
|
|
410
|
+
resolve();
|
|
411
|
+
}, {
|
|
412
|
+
authOnly: true,
|
|
413
|
+
keybindings,
|
|
414
|
+
noticeLines: ["Select an OAuth MCP server and press Enter or ctrl+a to authenticate."],
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
{ overlay: true, overlayOptions: { anchor: "center", width: 82 } },
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
return { configChanged: false };
|
|
422
|
+
}
|