@fitchmultz/pi-mcp-adapter 6.0.0 → 7.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +12 -8
  3. package/dist/app-bridge.bundle.js +14 -14
  4. package/dist/config.js +41 -16
  5. package/dist/gateway-arguments.d.ts +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/lifecycle.d.ts +2 -2
  8. package/dist/lifecycle.js +2 -2
  9. package/dist/mcp-auth-flow.js +2 -2
  10. package/dist/mcp-auth.d.ts +1 -1
  11. package/dist/mcp-auth.js +3 -3
  12. package/dist/mcp-code.d.ts +6 -6
  13. package/dist/mcp-code.js +11 -10
  14. package/dist/mcp-oauth-provider.d.ts +5 -5
  15. package/dist/mcp-oauth-provider.js +5 -5
  16. package/dist/mcp-output-guard.d.ts +1 -1
  17. package/dist/mcp-panel.d.ts +2 -2
  18. package/dist/mcp-panel.js +2 -2
  19. package/dist/mcp-setup-panel.d.ts +5 -4
  20. package/dist/mcp-setup-panel.js +15 -7
  21. package/dist/mcp-trace.d.ts +1 -1
  22. package/dist/mcp-trace.js +1 -1
  23. package/dist/npx-resolver.js +8 -4
  24. package/dist/proxy-modes.d.ts +2 -2
  25. package/dist/proxy-modes.js +9 -9
  26. package/dist/sampling-handler.js +1 -0
  27. package/dist/search-ranking.js +6 -2
  28. package/dist/server-manager.d.ts +2 -2
  29. package/dist/server-manager.js +44 -27
  30. package/dist/session-recovery.d.ts +7 -4
  31. package/dist/session-recovery.js +76 -10
  32. package/dist/tool-metadata.d.ts +43 -43
  33. package/dist/tool-result-renderer.d.ts +3 -3
  34. package/dist/tool-result-renderer.js +3 -3
  35. package/dist/ui-resource-handler.d.ts +2 -2
  36. package/dist/ui-resource-handler.js +2 -2
  37. package/dist/ui-server.js +4 -3
  38. package/dist/ui-stream-types.d.ts +8 -8
  39. package/dist/unix-socket-transport.d.ts +1 -1
  40. package/dist/unix-socket-transport.js +1 -1
  41. package/dist/utils.d.ts +1 -0
  42. package/dist/utils.js +14 -0
  43. package/package.json +34 -98
  44. package/abort.ts +0 -34
  45. package/agent-dir.ts +0 -24
  46. package/app-bridge.bundle.js +0 -109
  47. package/banner.png +0 -0
  48. package/checkpoint.ts +0 -55
  49. package/commands.ts +0 -622
  50. package/config.ts +0 -1204
  51. package/consent-manager.ts +0 -25
  52. package/direct-tools.ts +0 -75
  53. package/elicitation-handler.ts +0 -349
  54. package/error-signal.ts +0 -21
  55. package/gateway-arguments.ts +0 -73
  56. package/glimpse-ui.ts +0 -80
  57. package/host-html-template.ts +0 -456
  58. package/index.ts +0 -767
  59. package/init.ts +0 -613
  60. package/legacy-migration.ts +0 -47
  61. package/lifecycle.ts +0 -165
  62. package/logger.ts +0 -127
  63. package/mcp-auth-flow.ts +0 -975
  64. package/mcp-auth.ts +0 -793
  65. package/mcp-callback-server.ts +0 -450
  66. package/mcp-code.ts +0 -412
  67. package/mcp-keyring-helper.cjs +0 -89
  68. package/mcp-oauth-provider.ts +0 -774
  69. package/mcp-output-guard.ts +0 -530
  70. package/mcp-panel.ts +0 -907
  71. package/mcp-script-worker.mjs +0 -131
  72. package/mcp-setup-panel.ts +0 -666
  73. package/mcp-status.ts +0 -110
  74. package/mcp-trace.ts +0 -295
  75. package/metadata-cache.ts +0 -271
  76. package/npx-resolver.ts +0 -488
  77. package/onboarding-state.ts +0 -58
  78. package/panel-keys.ts +0 -37
  79. package/prompts.ts +0 -337
  80. package/proxy-modes.ts +0 -1109
  81. package/resource-tools.ts +0 -52
  82. package/runtime-owner.ts +0 -123
  83. package/sampling-handler.ts +0 -276
  84. package/scripts/build-app-bridge.mjs +0 -15
  85. package/scripts/build.mjs +0 -132
  86. package/scripts/prepare.mjs +0 -103
  87. package/search-ranking.ts +0 -132
  88. package/server-manager.ts +0 -1320
  89. package/session-recovery.ts +0 -316
  90. package/state.ts +0 -64
  91. package/tool-approval.ts +0 -78
  92. package/tool-loader.ts +0 -153
  93. package/tool-metadata.ts +0 -240
  94. package/tool-registrar.ts +0 -74
  95. package/tool-result-renderer.ts +0 -294
  96. package/tsconfig.build.json +0 -13
  97. package/tsconfig.json +0 -16
  98. package/types.ts +0 -785
  99. package/ui-resource-handler.ts +0 -249
  100. package/ui-server.ts +0 -808
  101. package/ui-session.ts +0 -568
  102. package/ui-stream-types.ts +0 -89
  103. package/ui-tool-visibility.ts +0 -25
  104. package/unix-socket-transport.ts +0 -85
  105. package/utils.ts +0 -307
package/banner.png DELETED
Binary file
package/checkpoint.ts DELETED
@@ -1,55 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import type { McpExtensionState } from "./state.ts";
3
- import { flushMetadataCache } from "./init.ts";
4
- import { getOAuthCheckpointBlocker, holdOAuthCheckpoint } from "./mcp-auth-flow.ts";
5
- import { isServerDisabled } from "./types.ts";
6
-
7
- /** Optional host event. Published Pi hosts without this event simply never emit it.
8
- * Keep its structural typing local; do not require a private host package/version. */
9
- export interface McpCheckpointEvent {
10
- readonly boundary: "turn" | "settled";
11
- readonly signal: AbortSignal;
12
- invalidate(): void;
13
- }
14
- export type McpCheckpointResult = { sleepReady: true } | { sleepReady: false; reason: string };
15
- export function onSessionCheckpoint(pi: ExtensionAPI, handler: (event: McpCheckpointEvent) => Promise<McpCheckpointResult>): void {
16
- (pi.on as unknown as (event: "session_checkpoint", callback: (event: McpCheckpointEvent) => Promise<McpCheckpointResult>) => unknown)("session_checkpoint", handler);
17
- }
18
-
19
- /** No shutdown, credential copying, connection replay or permanent owner cancellation. */
20
- export async function prepareMcpCheckpoint(state: McpExtensionState, event: McpCheckpointEvent): Promise<McpCheckpointResult> {
21
- event.signal.throwIfAborted();
22
- const releases: Array<() => void> = [];
23
- const release = () => { for (const undo of releases.splice(0).reverse()) undo(); };
24
- // Fence all owned sources synchronously, before the first await (including trace flush).
25
- event.signal.addEventListener("abort", release, { once: true });
26
- const undo = () => { release(); event.signal.removeEventListener("abort", release); };
27
- const blocked = (reason: string): McpCheckpointResult => { undo(); return { sleepReady: false, reason }; };
28
- try {
29
- releases.push(state.owner.holdCheckpoint(event));
30
- releases.push(state.manager.holdCheckpoint(event));
31
- releases.push(holdOAuthCheckpoint(state.oauthRuntime, event));
32
- releases.push(state.lifecycle.pauseForCheckpoint());
33
- if (state.lifecycle.hasActiveHealthCheck()) return blocked("MCP health check is active");
34
- for (const [name, definition] of Object.entries(state.config.mcpServers)) {
35
- if (!isServerDisabled(definition) && !definition.url) return blocked(`MCP ${name}: stdio/Unix server state is not reconstructible`);
36
- }
37
- const callback = state.owner.getCheckpointBlocker();
38
- if (callback) return blocked(callback);
39
- if (state.activeScripts) return blocked("MCP script is active");
40
- if (state.uiServer || state.completedUiSessions.length) return blocked("MCP UI session/messages require the running runtime");
41
- if (state.approvedToolCalls.size || state.consentManager.hasDecisions()) return blocked("MCP session approvals are not persisted");
42
- const oauth = getOAuthCheckpointBlocker(state.oauthRuntime);
43
- if (oauth) return blocked(oauth);
44
- const manager = state.manager.getCheckpointBlocker();
45
- if (manager) return blocked(manager);
46
- flushMetadataCache(state);
47
- await state.manager.flushForCheckpoint();
48
- event.signal.throwIfAborted();
49
- // Release/cancellation is host-owned. Existing clients/timers remain usable afterwards.
50
- return { sleepReady: true };
51
- } catch (error) {
52
- undo();
53
- throw error;
54
- }
55
- }
package/commands.ts DELETED
@@ -1,622 +0,0 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { McpExtensionState } from "./state.ts";
3
- import { isServerDisabled, isNonInteractiveOAuth, type McpAuthResult, type McpConfig, type McpPanelCallbacks, type McpPanelResult, type ImportKind } from "./types.ts";
4
- import {
5
- ensureCompatibilityImports,
6
- getMcpDiscoverySummary,
7
- getProjectConfigPath,
8
- type KnownServerPreset,
9
- getServerProvenance,
10
- previewCompatibilityImports,
11
- previewSharedServerEntry,
12
- previewStarterProjectConfig,
13
- writeDirectToolsConfig,
14
- writeSharedServerEntry,
15
- writeStarterProjectConfig,
16
- } from "./config.ts";
17
- import { markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateMetadataCache, updateStatusBar, getFailureAgeSeconds, getFailureMessage, clearFailure, recordFailure } from "./init.ts";
18
- import { loadMetadataCache, reconstructPromptMetadata } from "./metadata-cache.ts";
19
- import { buildToolMetadata } from "./tool-metadata.ts";
20
- import { supportsOAuth, authenticate, removeAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
21
- import { getAuthStorageOptions, inspectAuthForUrl } from "./mcp-auth.ts";
22
- import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.ts";
23
- import { openPath, resolveServerUrl, sanitizeTerminalText } from "./utils.ts";
24
- import { isAbortError } from "./runtime-owner.ts";
25
- import { createMcpPanel } from "./mcp-panel.ts";
26
- import { createMcpSetupPanel } from "./mcp-setup-panel.ts";
27
-
28
- export async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
29
- if (!ctx.hasUI) return;
30
-
31
- const lines: string[] = ["MCP Server Status:", ""];
32
-
33
- for (const name of Object.keys(state.config.mcpServers)) {
34
- const definition = state.config.mcpServers[name];
35
- if (isServerDisabled(definition)) {
36
- lines.push(`⊘ ${name}: disabled (run /mcp enable ${name}, then /reload)`);
37
- continue;
38
- }
39
- const connection = state.manager.getConnection(name);
40
- const metadata = state.toolMetadata.get(name);
41
- const toolCount = metadata?.filter(tool => tool.resourceUri === undefined).length ?? 0;
42
- const resourceCount = metadata?.filter(tool => tool.resourceUri !== undefined).length ?? 0;
43
- const failedAgo = getFailureAgeSeconds(state, name);
44
- let status = "not connected";
45
- let statusIcon = "○";
46
- let failed = false;
47
-
48
- if (connection?.status === "connected") {
49
- status = "connected";
50
- statusIcon = "✓";
51
- } else if (connection?.status === "needs-auth") {
52
- status = "needs auth";
53
- statusIcon = "⚠";
54
- } else if (failedAgo !== null) {
55
- const reason = sanitizeTerminalText(getFailureMessage(state, name) ?? "");
56
- status = reason ? `failed ${failedAgo}s ago — ${reason}` : `failed ${failedAgo}s ago`;
57
- statusIcon = "✗";
58
- failed = true;
59
- } else if (metadata !== undefined) {
60
- status = "cached";
61
- }
62
-
63
- const toolSuffix = failed ? "" : metadata === undefined
64
- ? " (undiscovered)"
65
- : ` (${toolCount} tool${toolCount === 1 ? "" : "s"}, ${resourceCount} resource${resourceCount === 1 ? "" : "s"}${status === "cached" ? ", cached" : ""})`;
66
- lines.push(`${statusIcon} ${name}: ${status}${toolSuffix}`);
67
- }
68
-
69
- if (Object.keys(state.config.mcpServers).length === 0) {
70
- lines.push("No MCP servers configured");
71
- lines.push("Run /mcp setup to adopt imports or scaffold a starter .mcp.json");
72
- }
73
-
74
- ctx.ui.notify(lines.join("\n"), "info");
75
- }
76
-
77
- export async function showPrompts(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
78
- if (!ctx.hasUI) return;
79
- const allPrompts = [...(state.promptMetadata?.values() ?? [])].flat();
80
- const failedPromptServers = [...(state.manager.getAllConnections?.() ?? [])]
81
- .filter(([, connection]) => connection.status === "connected" && connection.promptDiscoveryFailed)
82
- .map(([serverName]) => serverName)
83
- .sort();
84
- if (allPrompts.length === 0) {
85
- const failureNote = failedPromptServers.length > 0
86
- ? ` Prompt discovery failed for: ${failedPromptServers.join(", ")}.`
87
- : "";
88
- ctx.ui.notify(`No MCP prompts available. Prompts are discovered when servers with the \`prompts\` capability connect.${failureNote}`, "info");
89
- return;
90
- }
91
- const lines = ["MCP Prompts:", ""];
92
- const grouped = new Map<string, typeof allPrompts>();
93
- for (const prompt of allPrompts) {
94
- const list = grouped.get(prompt.serverName) ?? [];
95
- list.push(prompt);
96
- grouped.set(prompt.serverName, list);
97
- }
98
- for (const [serverName, prompts] of [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b))) {
99
- lines.push(`${serverName}:`);
100
- for (const prompt of prompts.sort((a, b) => a.commandName.localeCompare(b.commandName))) {
101
- const args = prompt.arguments.map(argument => argument.required ? `<${argument.name}>` : `[${argument.name}]`).join(" ");
102
- lines.push(` /${prompt.commandName}${args ? ` ${args}` : ""}`);
103
- if (prompt.description) lines.push(` ${prompt.description}`);
104
- }
105
- lines.push("");
106
- }
107
- lines.push(`Total: ${allPrompts.length} prompt${allPrompts.length === 1 ? "" : "s"}`);
108
- if (failedPromptServers.length > 0) {
109
- lines.push(`Prompt discovery failed for: ${failedPromptServers.join(", ")}. Cached prompt metadata may be stale.`);
110
- }
111
- ctx.ui.notify(lines.join("\n"), "info");
112
- }
113
-
114
- export async function showTools(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
115
- if (!ctx.hasUI) return;
116
-
117
- const allTools = [...state.toolMetadata.entries()]
118
- .filter(([serverName]) => !isServerDisabled(state.config.mcpServers[serverName]))
119
- .flatMap(([, metadata]) => metadata.filter(tool => tool.resourceUri === undefined).map(tool => tool.name));
120
-
121
- if (allTools.length === 0) {
122
- ctx.ui.notify("No MCP tools discovered. Use mcp_search to discover tools.", "info");
123
- return;
124
- }
125
-
126
- const lines = [
127
- "Discovered MCP Tools:",
128
- "",
129
- ...allTools.map(t => ` ${t}`),
130
- "",
131
- `Total: ${allTools.length} tool${allTools.length === 1 ? "" : "s"}`,
132
- ];
133
-
134
- ctx.ui.notify(lines.join("\n"), "info");
135
- }
136
-
137
- export async function reconnectServer(
138
- state: McpExtensionState,
139
- ctx: ExtensionContext,
140
- name: string,
141
- afterAuth = false,
142
- ): Promise<boolean> {
143
- const definition = state.config.mcpServers[name];
144
- const ui = ctx.hasUI ? ctx.ui : undefined;
145
- const signal = state.owner?.signal;
146
- if (!definition) {
147
- if (ui) {
148
- ui.notify(`Server "${name}" not found in config`, "error");
149
- }
150
- return false;
151
- }
152
- if (isServerDisabled(definition)) {
153
- if (ui) ui.notify(`MCP: ${name} is disabled. Run /mcp enable ${name}, then /reload.`, "warning");
154
- return false;
155
- }
156
-
157
- try {
158
- const previous = afterAuth ? state.manager.getConnection(name) : undefined;
159
- if (!afterAuth) await state.manager.close(name);
160
- state.owner?.throwIfInactive();
161
- const connection = previous
162
- ? await state.manager.reconnect(name, definition, previous, signal)
163
- : signal
164
- ? await state.manager.connect(name, definition, signal)
165
- : await state.manager.connect(name, definition);
166
- state.owner?.throwIfInactive();
167
- if (connection.status === "needs-auth") {
168
- if (ui) {
169
- ui.notify(`MCP: ${name} requires OAuth. Run /mcp-auth ${name} first.`, "warning");
170
- }
171
- updateStatusBar(state);
172
- return false;
173
- }
174
-
175
- const prefix = state.config.settings?.toolPrefix ?? "server";
176
- const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
177
- state.toolMetadata.set(name, metadata);
178
- if (!connection.promptDiscoveryFailed) {
179
- state.promptMetadata?.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix, definition));
180
- state.promptMetadataLive?.add(name);
181
- }
182
- if (connection.instructions) {
183
- state.serverInstructions.set(name, connection.instructions);
184
- } else {
185
- state.serverInstructions.delete(name);
186
- }
187
- updateMetadataCache(state, name);
188
- notifyToolMetadataUpdated(state, name, "command-reconnect");
189
- markKeepAliveAfterConnect(state, name);
190
- clearFailure(state, name);
191
-
192
- if (ui) {
193
- ui.notify(
194
- `MCP: Reconnected to ${name} (${connection.tools.length} tools, ${connection.resources.length} resources)`,
195
- "info"
196
- );
197
- if (failedTools.length > 0) {
198
- ui.notify(`MCP: ${name} - ${failedTools.length} tools skipped`, "warning");
199
- }
200
- }
201
- updateStatusBar(state);
202
- return true;
203
- } catch (error) {
204
- const message = error instanceof Error ? error.message : String(error);
205
- if (isAbortError(error, signal)) throw error;
206
- recordFailure(state, name, message);
207
- if (ui) {
208
- ui.notify(`MCP: Failed to reconnect to ${name}: ${sanitizeTerminalText(message)}`, "error");
209
- }
210
- updateStatusBar(state);
211
- return false;
212
- }
213
- }
214
-
215
- export async function reconnectServers(
216
- state: McpExtensionState,
217
- ctx: ExtensionContext,
218
- targetServer?: string
219
- ): Promise<void> {
220
- if (targetServer && !state.config.mcpServers[targetServer]) {
221
- if (ctx.hasUI) {
222
- ctx.ui.notify(`Server "${targetServer}" not found in config`, "error");
223
- }
224
- return;
225
- }
226
-
227
- const names = targetServer ? [targetServer] : Object.keys(state.config.mcpServers);
228
- for (const name of names) {
229
- await reconnectServer(state, ctx, name);
230
- }
231
-
232
- updateStatusBar(state);
233
- }
234
-
235
- export async function authenticateServer(
236
- serverName: string,
237
- config: McpConfig,
238
- ctx: ExtensionContext,
239
- signal?: AbortSignal,
240
- runtime?: McpOAuthRuntime,
241
- ): Promise<McpAuthResult> {
242
- const ui = ctx.hasUI ? ctx.ui : undefined;
243
- const cwd = ctx.cwd;
244
- signal ??= ctx.signal;
245
- if (!ui) return { ok: false, message: "OAuth authentication requires an interactive session." };
246
-
247
- const definition = config.mcpServers[serverName];
248
- if (!definition) {
249
- const message = `Server "${serverName}" not found in config`;
250
- ui.notify(message, "error");
251
- return { ok: false, message };
252
- }
253
- if (isServerDisabled(definition)) {
254
- const message = `Server "${serverName}" is disabled. Run /mcp enable ${serverName}, then /reload.`;
255
- ui.notify(message, "warning");
256
- return { ok: false, message };
257
- }
258
-
259
- if (!supportsOAuth(definition)) {
260
- const message = `Server "${serverName}" does not use OAuth authentication. Set "auth": "oauth" or omit auth for auto-detection.`;
261
- ui.notify(
262
- `Server "${serverName}" does not use OAuth authentication.\n` +
263
- `Set "auth": "oauth" or omit auth for auto-detection.`,
264
- "error"
265
- );
266
- return { ok: false, message };
267
- }
268
-
269
- try {
270
- const serverUrl = resolveServerUrl(definition);
271
- if (!serverUrl) {
272
- const message = `Server "${serverName}" has no URL configured (OAuth requires HTTP transport)`;
273
- ui.notify(message, "error");
274
- return { ok: false, message };
275
- }
276
-
277
- ui.setStatus("mcp-auth", `Authenticating ${serverName}...`);
278
- const authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd);
279
- const status = await authenticate(serverName, serverUrl, definition, {
280
- ...(authStorageOptions.baseDir ? { authStorageOptions } : {}),
281
- onAuthorizationUrl: (authorizationUrl) => {
282
- ui.notify(
283
- `Open this URL to authenticate ${serverName}:\n\n${authorizationUrl}\n\n` +
284
- "After approving, return to Pi; the local callback will complete automatically.",
285
- "info"
286
- );
287
- },
288
- ...(signal ? { signal } : {}),
289
- ...(runtime ? { runtime } : {}),
290
- });
291
- if (signal?.aborted) signal.throwIfAborted();
292
-
293
- if (status === "authenticated") {
294
- const message = `OAuth authentication successful for "${serverName}".`;
295
- ui.notify(message, "info");
296
- return { ok: true, message };
297
- }
298
-
299
- const message = `OAuth authentication failed for "${serverName}".`;
300
- ui.notify(message, "error");
301
- return { ok: false, message };
302
- } catch (error) {
303
- if (signal?.aborted) throw error;
304
- const message = error instanceof Error ? error.message : String(error);
305
- ui.notify(`Failed to authenticate "${serverName}": ${message}`, "error");
306
- return { ok: false, message };
307
- } finally {
308
- if (!signal?.aborted) ui.setStatus("mcp-auth", undefined);
309
- }
310
- }
311
-
312
- export async function logoutServer(
313
- serverName: string,
314
- state: McpExtensionState,
315
- ctx: ExtensionContext
316
- ): Promise<{ ok: boolean; message: string }> {
317
- const definition = state.config.mcpServers[serverName];
318
- const ui = ctx.hasUI ? ctx.ui : undefined;
319
- if (!definition) {
320
- const message = `Server "${serverName}" not found in config`;
321
- if (ui) ui.notify(message, "error");
322
- return { ok: false, message };
323
- }
324
-
325
- const signal = state.owner?.signal;
326
- try {
327
- await removeAuth(serverName, { authStorageOptions: state.authStorageOptions, signal, runtime: state.oauthRuntime });
328
- } catch (error) {
329
- if (isAbortError(error, signal)) throw error;
330
- const message = error instanceof Error ? error.message : String(error);
331
- if (ui) {
332
- ui.notify(`Failed to clear OAuth credentials for "${serverName}": ${sanitizeTerminalText(message)}`, "error");
333
- }
334
- return { ok: false, message };
335
- }
336
-
337
- state.owner?.throwIfInactive();
338
- try {
339
- await state.manager.close(serverName);
340
- } catch (error) {
341
- if (isAbortError(error, signal)) throw error;
342
- const message = error instanceof Error ? error.message : String(error);
343
- if (ui) {
344
- ui.notify(
345
- `OAuth credentials were cleared for "${serverName}", but its connection could not be closed: ${sanitizeTerminalText(message)}`,
346
- "error",
347
- );
348
- }
349
- return { ok: false, message };
350
- }
351
-
352
- state.owner?.throwIfInactive();
353
- updateStatusBar(state);
354
-
355
- const message = `OAuth credentials cleared for "${serverName}". Run /mcp-auth ${serverName} to authenticate again.`;
356
- if (ui) ui.notify(message, "info");
357
- return { ok: true, message };
358
- }
359
-
360
- export interface PanelFlowResult {
361
- configChanged: boolean;
362
- }
363
-
364
- function buildSharedConfigNoticeLines(configOverridePath: string | undefined, cwd: string): string[] {
365
- const discovery = getMcpDiscoverySummary(configOverridePath, cwd);
366
- const onboardingState = loadOnboardingState();
367
- if (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) return [];
368
-
369
- const sharedSources = discovery.sources.filter((source) => source.kind === "shared" && source.serverCount > 0);
370
- const sourceList = sharedSources.map((source) => source.path).join(", ");
371
- return [
372
- `Using standard MCP config from ${sourceList}.`,
373
- "Pi only writes compatibility imports and adapter-specific overrides into Pi-owned files when needed.",
374
- ];
375
- }
376
-
377
- export async function openMcpSetup(
378
- state: McpExtensionState,
379
- pi: ExtensionAPI,
380
- ctx: ExtensionContext,
381
- configOverridePath?: string,
382
- mode: "empty" | "setup" = "setup",
383
- ): Promise<PanelFlowResult> {
384
- if (!ctx.hasUI) return { configChanged: false };
385
- if (!ctx.isProjectTrusted()) {
386
- ctx.ui.notify("Project MCP setup is unavailable until this project is trusted.", "warning");
387
- return { configChanged: false };
388
- }
389
- if (ctx.mode !== "tui") {
390
- ctx.ui.notify("MCP setup is available in TUI mode only.", "info");
391
- return { configChanged: false };
392
- }
393
- if (state.programmaticConfig) {
394
- ctx.ui.notify("MCP setup is unavailable when config is supplied by createMcpAdapter().", "info");
395
- return { configChanged: false };
396
- }
397
-
398
- const discovery = getMcpDiscoverySummary(configOverridePath, ctx.cwd);
399
- const onboardingState = loadOnboardingState();
400
- let configChanged = false;
401
-
402
- const callbacks = {
403
- previewImports: (imports: ImportKind[]) => previewCompatibilityImports(imports, configOverridePath),
404
- previewStarterProject: () => previewStarterProjectConfig(ctx.cwd),
405
- previewRepoPrompt: () => {
406
- const repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd).repoPrompt;
407
- if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) return null;
408
- return previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
409
- },
410
- previewKnownServer: (preset: KnownServerPreset) => previewSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry),
411
- adoptImports: async (imports: ImportKind[]) => {
412
- const result = ensureCompatibilityImports(imports, configOverridePath);
413
- if (result.added.length > 0) configChanged = true;
414
- return result;
415
- },
416
- scaffoldProjectConfig: async () => {
417
- const path = writeStarterProjectConfig(ctx.cwd);
418
- configChanged = true;
419
- return { path };
420
- },
421
- addRepoPrompt: async () => {
422
- const repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd).repoPrompt;
423
- if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {
424
- throw new Error("RepoPrompt is not available to add from this setup screen.");
425
- }
426
- const path = writeSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
427
- configChanged = true;
428
- return { path, serverName: repoPrompt.serverName };
429
- },
430
- addKnownServer: async (preset: KnownServerPreset) => {
431
- const path = writeSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry);
432
- configChanged = true;
433
- return { path, serverName: preset.name };
434
- },
435
- openPath: async (targetPath: string) => {
436
- await openPath(pi, targetPath);
437
- },
438
- markSetupCompleted: persistSetupCompleted,
439
- };
440
-
441
- return new Promise<PanelFlowResult>((resolve) => {
442
- ctx.ui.custom(
443
- (tui, _theme, keybindings, done) => {
444
- return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings }, tui, () => {
445
- done(undefined);
446
- resolve({ configChanged });
447
- });
448
- },
449
- { overlay: true, overlayOptions: { anchor: "center", width: 92 } },
450
- );
451
- });
452
- }
453
-
454
- function buildMcpPanelCallbacks(
455
- state: McpExtensionState,
456
- config: McpConfig,
457
- ctx: ExtensionContext,
458
- ): McpPanelCallbacks {
459
- // Panel-only diagnostics keep status inspection from mutating connection
460
- // failure state while allowing the existing panel failure UI to show why the
461
- // credential store could not be inspected.
462
- const authStatusFailures = new Map<string, string>();
463
-
464
- return {
465
- reconnect: (serverName, afterAuth) => reconnectServer(state, ctx, serverName, afterAuth),
466
- canAuthenticate: (serverName: string) => {
467
- const definition = config.mcpServers[serverName];
468
- return definition ? !isServerDisabled(definition) && supportsOAuth(definition) : false;
469
- },
470
- authenticate: (serverName: string) => authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime),
471
- getConnectionStatus: (serverName: string) => {
472
- authStatusFailures.delete(serverName);
473
- const definition = config.mcpServers[serverName];
474
- if (isServerDisabled(definition)) return "disabled";
475
- const connection = state.manager.getConnection(serverName);
476
- let serverUrl: string | undefined;
477
- try {
478
- serverUrl = definition ? resolveServerUrl(definition) : undefined;
479
- } catch {
480
- return "failed";
481
- }
482
- if (
483
- definition?.auth === "oauth"
484
- && serverUrl
485
- && definition.oauth !== false
486
- && !isNonInteractiveOAuth(definition.oauth)
487
- ) {
488
- const authStatus = inspectAuthForUrl(serverName, serverUrl, state.authStorageOptions);
489
- if (authStatus.status === "unavailable") {
490
- authStatusFailures.set(serverName, authStatus.message);
491
- return "failed";
492
- }
493
- if (authStatus.status === "absent" || !authStatus.entry.tokens) {
494
- return "needs-auth";
495
- }
496
- }
497
- if (connection?.status === "needs-auth") return "needs-auth";
498
- if (connection?.status === "connected") return "connected";
499
- if (getFailureAgeSeconds(state, serverName) !== null) return "failed";
500
- return "idle";
501
- },
502
- getFailureMessage: (serverName: string) => authStatusFailures.get(serverName) ?? getFailureMessage(state, serverName),
503
- refreshCacheAfterReconnect: (serverName: string) => {
504
- const freshCache = loadMetadataCache(state.metadataCacheEnabled);
505
- return freshCache?.servers?.[serverName] ?? null;
506
- },
507
- };
508
- }
509
-
510
- export async function openMcpPanel(
511
- state: McpExtensionState,
512
- pi: ExtensionAPI,
513
- ctx: ExtensionContext,
514
- configOverridePath?: string,
515
- onDirectToolsConfigChanged?: (changes: Map<string, true | string[] | false>) => void | Promise<void>,
516
- ): Promise<PanelFlowResult> {
517
- if (!ctx.isProjectTrusted() || ctx.mode !== "tui") {
518
- if (ctx.hasUI) await showStatus(state, ctx);
519
- return { configChanged: false };
520
- }
521
- if (state.programmaticConfig) {
522
- if (ctx.hasUI) {
523
- ctx.ui.notify("MCP status is shown from the in-memory SDK config; configuration discovery is unavailable.", "info");
524
- await showStatus(state, ctx);
525
- }
526
- return { configChanged: false };
527
- }
528
- if (Object.keys(state.config.mcpServers).length === 0) {
529
- return openMcpSetup(state, pi, ctx, configOverridePath, "empty");
530
- }
531
-
532
- const config = state.config;
533
- const cache = loadMetadataCache(state.metadataCacheEnabled);
534
- const configPath = configOverridePath;
535
- const provenanceMap = getServerProvenance(configPath, ctx.cwd);
536
- const noticeLines = buildSharedConfigNoticeLines(configPath, ctx.cwd);
537
-
538
- const callbacks = buildMcpPanelCallbacks(state, config, ctx);
539
-
540
- let configChanged = false;
541
-
542
- await new Promise<void>((resolve) => {
543
- ctx.ui.custom(
544
- (tui, _theme, keybindings, done) => {
545
- return createMcpPanel(config, cache, provenanceMap, callbacks, tui, (result: McpPanelResult) => {
546
- void (async () => {
547
- if (!result.cancelled && result.changes.size > 0) {
548
- writeDirectToolsConfig(result.changes, provenanceMap);
549
- await onDirectToolsConfigChanged?.(result.changes);
550
- ctx.ui.notify("Direct tools updated for this session.", "info");
551
- }
552
- done(undefined);
553
- resolve();
554
- })().catch((error) => {
555
- const message = error instanceof Error ? error.message : String(error);
556
- ctx.ui.notify(`Direct tools updated, but live refresh failed: ${message}`, "error");
557
- configChanged = true;
558
- done(undefined);
559
- resolve();
560
- });
561
- }, { noticeLines, keybindings });
562
- },
563
- { overlay: true, overlayOptions: { anchor: "center", width: 82 } },
564
- );
565
- });
566
-
567
- if (noticeLines.length > 0) markSharedConfigHintShown();
568
-
569
- return { configChanged };
570
- }
571
-
572
- export async function openMcpAuthPanel(
573
- state: McpExtensionState,
574
- ctx: ExtensionContext,
575
- configOverridePath?: string,
576
- ): Promise<PanelFlowResult> {
577
- if (!ctx.hasUI) return { configChanged: false };
578
- if (!ctx.isProjectTrusted()) {
579
- ctx.ui.notify("Use /mcp-auth <server> while this project is untrusted.", "info");
580
- return { configChanged: false };
581
- }
582
- if (ctx.mode !== "tui") {
583
- ctx.ui.notify("Use /mcp-auth <server> to authenticate outside TUI mode.", "info");
584
- return { configChanged: false };
585
- }
586
- if (state.programmaticConfig) {
587
- ctx.ui.notify("Use /mcp-auth <server> to authenticate a server from the in-memory SDK config.", "info");
588
- return { configChanged: false };
589
- }
590
-
591
- const config = state.config;
592
- const oauthServers = Object.entries(config.mcpServers).filter(
593
- ([, definition]) => !isServerDisabled(definition) && supportsOAuth(definition),
594
- );
595
- if (oauthServers.length === 0) {
596
- ctx.ui.notify("No OAuth-capable MCP servers are configured.", "warning");
597
- return { configChanged: false };
598
- }
599
-
600
- const cache = loadMetadataCache(state.metadataCacheEnabled);
601
- const configPath = configOverridePath;
602
- const provenanceMap = getServerProvenance(configPath, ctx.cwd);
603
- const callbacks = buildMcpPanelCallbacks(state, config, ctx);
604
-
605
- await new Promise<void>((resolve) => {
606
- ctx.ui.custom(
607
- (tui, _theme, keybindings, done) => {
608
- return createMcpPanel(config, cache, provenanceMap, callbacks, tui, () => {
609
- done(undefined);
610
- resolve();
611
- }, {
612
- authOnly: true,
613
- keybindings,
614
- noticeLines: ["Select an OAuth MCP server and press Enter or ctrl+a to authenticate."],
615
- });
616
- },
617
- { overlay: true, overlayOptions: { anchor: "center", width: 82 } },
618
- );
619
- });
620
-
621
- return { configChanged: false };
622
- }