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