@phi-code-admin/phi-code 0.85.0 → 0.87.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 (86) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +92 -123
  3. package/config/routing.example.json +129 -0
  4. package/config/routing.schema.json +58 -0
  5. package/dist/cli/args.d.ts.map +1 -1
  6. package/dist/cli/args.js +1 -1
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/sdk.d.ts.map +1 -1
  12. package/dist/core/sdk.js +1 -1
  13. package/dist/core/sdk.js.map +1 -1
  14. package/dist/core/system-prompt.d.ts.map +1 -1
  15. package/dist/core/system-prompt.js +6 -6
  16. package/dist/core/system-prompt.js.map +1 -1
  17. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  18. package/dist/modes/interactive/components/config-selector.js +1 -1
  19. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  20. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  21. package/dist/modes/interactive/interactive-mode.js +2 -2
  22. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  23. package/dist/package-manager-cli.d.ts.map +1 -1
  24. package/dist/package-manager-cli.js +9 -8
  25. package/dist/package-manager-cli.js.map +1 -1
  26. package/dist/utils/pi-user-agent.d.ts.map +1 -1
  27. package/dist/utils/pi-user-agent.js +4 -1
  28. package/dist/utils/pi-user-agent.js.map +1 -1
  29. package/docs/compaction.md +11 -11
  30. package/docs/custom-provider.md +4 -4
  31. package/docs/development.md +2 -2
  32. package/docs/extensions.md +47 -47
  33. package/docs/fork-policy.md +81 -0
  34. package/docs/index.md +7 -7
  35. package/docs/json.md +3 -3
  36. package/docs/keybindings.md +5 -5
  37. package/docs/models.md +6 -6
  38. package/docs/packages.md +37 -37
  39. package/docs/prompt-templates.md +4 -4
  40. package/docs/providers.md +9 -9
  41. package/docs/quickstart.md +25 -25
  42. package/docs/rpc.md +3 -3
  43. package/docs/sdk.md +34 -34
  44. package/docs/session-format.md +4 -4
  45. package/docs/sessions.md +11 -11
  46. package/docs/settings.md +9 -9
  47. package/docs/shell-aliases.md +2 -2
  48. package/docs/skills.md +9 -9
  49. package/docs/terminal-setup.md +7 -7
  50. package/docs/termux.md +6 -6
  51. package/docs/themes.md +9 -9
  52. package/docs/tmux.md +3 -3
  53. package/docs/tui.md +8 -8
  54. package/docs/usage.md +39 -39
  55. package/docs/windows.md +2 -2
  56. package/extensions/phi/agents.ts +12 -117
  57. package/extensions/phi/benchmark.ts +129 -49
  58. package/extensions/phi/browser.ts +10 -37
  59. package/extensions/phi/btw/btw.ts +1 -7
  60. package/extensions/phi/chrome/index.ts +1283 -741
  61. package/extensions/phi/commit.ts +9 -14
  62. package/extensions/phi/goal/index.ts +10 -33
  63. package/extensions/phi/init.ts +37 -47
  64. package/extensions/phi/keys.ts +2 -7
  65. package/extensions/phi/mcp/callback-server.ts +162 -165
  66. package/extensions/phi/mcp/config.ts +122 -136
  67. package/extensions/phi/mcp/errors.ts +18 -23
  68. package/extensions/phi/mcp/index.ts +322 -355
  69. package/extensions/phi/mcp/oauth-provider.ts +289 -289
  70. package/extensions/phi/mcp/server-manager.ts +390 -413
  71. package/extensions/phi/mcp/tool-bridge.ts +381 -415
  72. package/extensions/phi/memory.ts +2 -2
  73. package/extensions/phi/models.ts +27 -26
  74. package/extensions/phi/orchestrator.ts +343 -266
  75. package/extensions/phi/productivity.ts +4 -2
  76. package/extensions/phi/providers/agent-def.ts +128 -0
  77. package/extensions/phi/providers/alibaba.ts +56 -7
  78. package/extensions/phi/providers/context-window.ts +4 -1
  79. package/extensions/phi/providers/live-models.ts +5 -20
  80. package/extensions/phi/providers/orchestrator-helpers.ts +19 -5
  81. package/extensions/phi/providers/phase-machine.ts +220 -0
  82. package/extensions/phi/setup.ts +196 -169
  83. package/extensions/phi/skill-loader.ts +18 -21
  84. package/extensions/phi/smart-router.ts +6 -6
  85. package/extensions/phi/web-search.ts +90 -50
  86. package/package.json +2 -1
@@ -12,371 +12,338 @@
12
12
  * and <cwd>/.phi/mcp.json (phi configDir), and it ships bundled (no install).
13
13
  */
14
14
 
15
- import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "phi-code";
15
+ import { exec } from "node:child_process";
16
+ import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
17
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
18
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "phi-code";
19
+ // OAuth imports
20
+ import { cancelCallback, ensureCallbackServer, stopCallbackServer, waitForCallback } from "./callback-server.js";
16
21
  import { loadConfig } from "./config.js";
17
- import { ServerManager } from "./server-manager.js";
22
+ import { McpError } from "./errors.js";
23
+ import { McpOAuthProvider, setCallbackPort } from "./oauth-provider.js";
18
24
  import type { TransportAuthCallbacks } from "./server-manager.js";
25
+ import { ServerManager } from "./server-manager.js";
19
26
  import { ToolBridge } from "./tool-bridge.js";
20
- import { McpError } from "./errors.js";
21
- import { exec } from "node:child_process";
22
-
23
- // OAuth imports
24
- import {
25
- ensureCallbackServer,
26
- waitForCallback,
27
- cancelCallback,
28
- stopCallbackServer,
29
- } from "./callback-server.js";
30
- import { setCallbackPort, McpOAuthProvider } from "./oauth-provider.js";
31
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
32
- import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
33
27
 
34
28
  /**
35
29
  * Open a URL in the user's default browser.
36
30
  * Works on macOS, Linux, and Windows.
37
31
  */
38
32
  function openBrowser(url: string): void {
39
- const cmd = process.platform === "darwin"
40
- ? `open "${url}"`
41
- : process.platform === "win32"
42
- ? `start "" "${url}"`
43
- : `xdg-open "${url}"`;
44
-
45
- exec(cmd, (err, stdout, stderr) => {
46
- if (err) {
47
- const errorMsg = `[pi-mcp] Failed to open browser: ${err.message}`;
48
- console.error(errorMsg);
49
- if (stderr) {
50
- console.error(`[pi-mcp] Browser error output: ${stderr}`);
51
- }
52
- }
53
- });
33
+ const cmd =
34
+ process.platform === "darwin"
35
+ ? `open "${url}"`
36
+ : process.platform === "win32"
37
+ ? `start "" "${url}"`
38
+ : `xdg-open "${url}"`;
39
+
40
+ exec(cmd, (err, _stdout, stderr) => {
41
+ if (err) {
42
+ const errorMsg = `[pi-mcp] Failed to open browser: ${err.message}`;
43
+ console.error(errorMsg);
44
+ if (stderr) {
45
+ console.error(`[pi-mcp] Browser error output: ${stderr}`);
46
+ }
47
+ }
48
+ });
54
49
  }
55
50
 
56
51
  export default async function (pi: ExtensionAPI): Promise<void> {
57
- // ── 1. Load and validate config ──────────────────────────────────────────
58
- // cwd is available on the ExtensionContext passed to event handlers.
59
- // We load config lazily on session_start to get the correct per-session cwd.
60
- // For the initial load we use process.cwd() as a bootstrap path to detect
61
- // whether any config exists at all.
62
- let config;
63
- try {
64
- config = await loadConfig(process.cwd());
65
- } catch (err) {
66
- // Can't notify yet (no ctx), so log to stderr. The session_start handler
67
- // will re-try with the real cwd and surface errors properly.
68
- console.error(`[pi-mcp] Config error: ${err instanceof McpError ? err.message : String(err)}`);
69
- return;
70
- }
71
-
72
- // Even with zero configured servers we proceed so the bundled `/mcp` command
73
- // is always available (discoverability); it then guides the user to create an
74
- // mcp.json. Servers are still only connected when one is actually configured.
75
-
76
- // ── 2. Initialize bridge components ──────────────────────────────────────
77
- // Auth callbacks — opens browser and notifies user when OAuth is needed
78
- const authCallbacks: TransportAuthCallbacks = {
79
- onAuthRequired: (serverName: string, authorizationUrl: URL): void => {
80
- console.error(
81
- `[pi-mcp] OAuth required for "${serverName}". Opening browser for authorization...`,
82
- );
83
- openBrowser(authorizationUrl.toString());
84
- },
85
- };
86
-
87
- const manager = new ServerManager(config, authCallbacks);
88
- const bridge = new ToolBridge(config.settings, pi);
89
-
90
- // Connect tool refresh callback: called on connect and on list_changed
91
- manager.setToolRefreshCallback(async (serverName, client) => {
92
- await bridge.refreshTools(serverName, client);
93
- });
94
-
95
- // ── 3. Session lifecycle ──────────────────────────────────────────────────
96
- pi.on("session_start", async (_event, ctx: ExtensionContext) => {
97
- // Reload config with the real session cwd (project config may differ)
98
- let sessionConfig = config;
99
- try {
100
- sessionConfig = await loadConfig(ctx.cwd);
101
- } catch (err) {
102
- const msg = err instanceof McpError ? err.userMessage : String(err);
103
- ctx.ui.notify(`pi-mcp: Config error — ${msg}`, "error");
104
- return;
105
- }
106
-
107
- // If config changed (different cwd with project-level overrides),
108
- // shut down old servers and rebuild the manager's server list
109
- if (JSON.stringify(sessionConfig) !== JSON.stringify(config)) {
110
- // Deactivate and remove all tools from old config
111
- for (const server of manager.getAllServers()) {
112
- bridge.removeServer(server.name);
113
- }
114
- // Shut down all running servers
115
- await manager.shutdownAll();
116
- // Rebuild server entries from new config
117
- manager.rebuildServers(sessionConfig);
118
- }
119
-
120
- const eagerServers = Object.entries(sessionConfig.mcpServers).filter(
121
- ([, cfg]) => cfg.lifecycle === "eager",
122
- );
123
-
124
- // Start all eager servers concurrently
125
- await Promise.allSettled(
126
- eagerServers.map(async ([name]) => {
127
- try {
128
- await manager.startServer(name, ctx.cwd);
129
- } catch (err) {
130
- const msg = err instanceof McpError ? err.userMessage : String(err);
131
- ctx.ui.notify(`pi-mcp: Failed to start ${name} — ${msg}`, "error");
132
- }
133
- }),
134
- );
135
- });
136
-
137
- pi.on("session_shutdown", async (_event, _ctx: ExtensionContext) => {
138
- // Stop the callback server
139
- await stopCallbackServer().catch(() => {});
140
-
141
- // Deactivate all tools before shutting down servers
142
- for (const server of manager.getAllServers()) {
143
- bridge.deactivateServer(server.name);
144
- }
145
- await manager.shutdownAll();
146
- });
147
-
148
- // ── 4. /mcp — show server status ─────────────────────────────────────────
149
- pi.registerCommand("mcp", {
150
- description:
151
- "Show MCP server status. Usage: /mcp [server-name] for detail.",
152
- handler: async (args: string, ctx: ExtensionCommandContext) => {
153
- const serverName = args.trim();
154
- if (serverName) {
155
- // Detailed view: status + recent stderr
156
- const server = manager.getServer(serverName);
157
- if (!server) {
158
- ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
159
- return;
160
- }
161
- const logs = manager.getServerLogs(serverName);
162
- const detail = [
163
- `Server: ${serverName}`,
164
- `State: ${server.state}`,
165
- `Retries: ${server.retryCount}`,
166
- server.lastError ? `Last error: ${server.lastError.message}` : null,
167
- "",
168
- "Recent output:",
169
- logs,
170
- ]
171
- .filter(Boolean)
172
- .join("\n");
173
- ctx.ui.notify(detail, "info");
174
- } else if (manager.getAllServers().length === 0) {
175
- // No servers configured yet: guide the user instead of showing nothing.
176
- ctx.ui.notify(
177
- 'No MCP servers configured. Create ~/.phi/agent/mcp.json (global) or .phi/mcp.json (project) with an "mcpServers" block, e.g.:\n' +
178
- '{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."], "lifecycle": "eager" } } }',
179
- "info",
180
- );
181
- } else {
182
- // Summary view: all servers
183
- ctx.ui.notify(manager.getStatusSummary(), "info");
184
- }
185
- },
186
- });
187
-
188
- // ── 5. /mcp:stop — stop a server ─────────────────────────────────────────
189
- pi.registerCommand("mcp:stop", {
190
- description: "Stop an MCP server. Usage: /mcp:stop <server-name>",
191
- handler: async (args: string, ctx: ExtensionCommandContext) => {
192
- const serverName = args.trim();
193
- if (!serverName) {
194
- ctx.ui.notify("Usage: /mcp:stop <server-name>", "error");
195
- return;
196
- }
197
- if (!manager.getServer(serverName)) {
198
- ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
199
- return;
200
- }
201
- bridge.deactivateServer(serverName);
202
- await manager.stopServer(serverName);
203
- ctx.ui.notify(`pi-mcp: Stopped ${serverName}`, "info");
204
- },
205
- });
206
-
207
- // ── 6. /mcp:start — manually start a lazy server ─────────────────────────
208
- pi.registerCommand("mcp:start", {
209
- description: "Start an MCP server. Usage: /mcp:start <server-name>",
210
- handler: async (args: string, ctx: ExtensionCommandContext) => {
211
- const serverName = args.trim();
212
- if (!serverName) {
213
- ctx.ui.notify("Usage: /mcp:start <server-name>", "error");
214
- return;
215
- }
216
- if (!manager.getServer(serverName)) {
217
- ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
218
- return;
219
- }
220
- try {
221
- await manager.startServer(serverName, ctx.cwd);
222
- ctx.ui.notify(`pi-mcp: Started ${serverName}`, "info");
223
- } catch (err) {
224
- const msg = err instanceof McpError ? err.userMessage : String(err);
225
- ctx.ui.notify(`pi-mcp: Failed to start ${serverName} ${msg}`, "error");
226
- }
227
- },
228
- });
229
-
230
- // ── 7. /mcp:auth — trigger OAuth authentication for a server ────────────────
231
- pi.registerCommand("mcp:auth", {
232
- description:
233
- "Trigger OAuth authentication for a server. Resets credentials and opens browser for re-authorization. Usage: /mcp:auth <server-name>",
234
- handler: async (args: string, ctx: ExtensionCommandContext) => {
235
- const serverName = args.trim();
236
- if (!serverName) {
237
- // List servers with auth config
238
- const authServers = manager.getAllServers().filter((s) => s.config.auth);
239
- if (authServers.length === 0) {
240
- ctx.ui.notify(
241
- "pi-mcp: No servers with OAuth configured. Add `auth: { type: \"oauth\" }` to a server in mcp.json.",
242
- "error",
243
- );
244
- return;
245
- }
246
- const lines = authServers.map(async (s) => {
247
- const status = await manager.getServerAuthStatus(s.name);
248
- const authIcon = status?.hasTokens ? "\u2705 authenticated" : "\u274C not authenticated";
249
- const savedInfo = status?.savedAt ? ` (since ${status.savedAt})` : "";
250
- return ` ${s.name}: ${authIcon}${savedInfo}`;
251
- });
252
- const statusLines = await Promise.all(lines);
253
- ctx.ui.notify(
254
- [
255
- "Usage: /mcp:auth <server-name>",
256
- "",
257
- "OAuth-enabled servers:",
258
- ...statusLines,
259
- ].join("\n"),
260
- "info",
261
- );
262
- return;
263
- }
264
- const server = manager.getServer(serverName);
265
- if (!server) {
266
- ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
267
- return;
268
- }
269
- if (!server.config.auth) {
270
- ctx.ui.notify(
271
- `pi-mcp: Server "${serverName}" does not have OAuth configured. Add \`auth: { type: "oauth" }\` to its config in mcp.json.`,
272
- "error",
273
- );
274
- return;
275
- }
276
-
277
- const config = server.config;
278
- let oauthState: string | undefined;
279
-
280
- try {
281
- // Stop the server if running
282
- if (server.state !== "stopped") {
283
- bridge.deactivateServer(serverName);
284
- await manager.stopServer(serverName);
285
- }
286
-
287
- // Validate that we have a server URL (required for OAuth)
288
- if (!config.url) {
289
- throw new McpError(
290
- `Server "${serverName}" has OAuth configured but no URL. OAuth requires a URL-based server transport.`,
291
- serverName,
292
- "config",
293
- );
294
- }
295
-
296
- // Reset all OAuth credentials (tokens, client info, PKCE, discovery)
297
- await manager.resetServerAuth(serverName);
298
-
299
- ctx.ui.notify(
300
- `pi-mcp: Starting OAuth flow for ${serverName}...`,
301
- "info",
302
- );
303
-
304
- // 1. Start the callback server
305
- const port = await ensureCallbackServer();
306
- setCallbackPort(port);
307
-
308
- // 2. Generate a cryptographically secure state parameter for CSRF protection
309
- oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
310
- .map((b: number) => b.toString(16).padStart(2, "0"))
311
- .join("");
312
-
313
- // 3. Register the callback promise BEFORE opening the browser
314
- const callbackPromise = waitForCallback(oauthState);
315
-
316
- // 4. Create auth provider and transport
317
- const authProvider = new McpOAuthProvider(
318
- serverName,
319
- config.auth || { type: "oauth" },
320
- (url: URL) => {
321
- console.error(`[pi-mcp] Opening browser for ${serverName}...`);
322
- openBrowser(url.toString());
323
- },
324
- );
325
-
326
- // CRITICAL FIX #1: Set the OAuth state on the provider before calling auth()
327
- // This ensures the state parameter is included in the authorization URL
328
- authProvider.setState(oauthState);
329
-
330
- const transport = new StreamableHTTPClientTransport(
331
- new URL(config.url),
332
- { authProvider },
333
- );
334
-
335
- // 5. Start the auth flow - this will trigger redirectToAuthorization which opens the browser
336
- // CRITICAL FIX #2: Check the return value of auth() instead of catching UnauthorizedError
337
- // The SDK returns 'REDIRECT' when it needs browser interaction, not an error
338
- const authResult = await auth(authProvider, { serverUrl: config.url });
339
-
340
- if (authResult === "AUTHORIZED") {
341
- // Auth succeeded without needing browser interaction (e.g., had valid tokens)
342
- ctx.ui.notify(`pi-mcp: ${serverName} authenticated successfully!`, "info");
343
- } else if (authResult === "REDIRECT") {
344
- // Browser was opened, wait for the callback from the user
345
- ctx.ui.notify(
346
- `pi-mcp: Browser opened for ${serverName}. Complete authorization to continue...`,
347
- "info",
348
- );
349
-
350
- // 6. Wait for the callback (this blocks until the user authorizes)
351
- const code = await callbackPromise;
352
-
353
- // 7. Complete the OAuth flow with the authorization code
354
- await transport.finishAuth(code);
355
-
356
- ctx.ui.notify(`pi-mcp: ${serverName} authenticated successfully!`, "info");
357
- } else {
358
- throw new McpError(
359
- `Unexpected auth result: ${authResult}`,
360
- serverName,
361
- "protocol",
362
- );
363
- }
364
-
365
- // 8. Close the transport (we'll create a new one when starting the server)
366
- await transport.close().catch(() => {});
367
-
368
- // 9. Start the server with fresh auth credentials
369
- await manager.startServer(serverName, ctx.cwd);
370
-
371
- } catch (err) {
372
- const msg = err instanceof McpError ? err.userMessage : String(err);
373
- ctx.ui.notify(`pi-mcp: Authentication failed for ${serverName} — ${msg}`, "error");
374
-
375
- // Clean up on error
376
- if (oauthState) {
377
- cancelCallback(oauthState);
378
- }
379
- }
380
- },
381
- });
52
+ // ── 1. Load and validate config ──────────────────────────────────────────
53
+ // cwd is available on the ExtensionContext passed to event handlers.
54
+ // We load config lazily on session_start to get the correct per-session cwd.
55
+ // For the initial load we use process.cwd() as a bootstrap path to detect
56
+ // whether any config exists at all.
57
+ let config: Awaited<ReturnType<typeof loadConfig>>;
58
+ try {
59
+ config = await loadConfig(process.cwd());
60
+ } catch (err) {
61
+ // Can't notify yet (no ctx), so log to stderr. The session_start handler
62
+ // will re-try with the real cwd and surface errors properly.
63
+ console.error(`[pi-mcp] Config error: ${err instanceof McpError ? err.message : String(err)}`);
64
+ return;
65
+ }
66
+
67
+ // Even with zero configured servers we proceed so the bundled `/mcp` command
68
+ // is always available (discoverability); it then guides the user to create an
69
+ // mcp.json. Servers are still only connected when one is actually configured.
70
+
71
+ // ── 2. Initialize bridge components ──────────────────────────────────────
72
+ // Auth callbacks — opens browser and notifies user when OAuth is needed
73
+ const authCallbacks: TransportAuthCallbacks = {
74
+ onAuthRequired: (serverName: string, authorizationUrl: URL): void => {
75
+ console.error(`[pi-mcp] OAuth required for "${serverName}". Opening browser for authorization...`);
76
+ openBrowser(authorizationUrl.toString());
77
+ },
78
+ };
79
+
80
+ const manager = new ServerManager(config, authCallbacks);
81
+ const bridge = new ToolBridge(config.settings, pi);
82
+
83
+ // Connect tool refresh callback: called on connect and on list_changed
84
+ manager.setToolRefreshCallback(async (serverName, client) => {
85
+ await bridge.refreshTools(serverName, client);
86
+ });
87
+
88
+ // ── 3. Session lifecycle ──────────────────────────────────────────────────
89
+ pi.on("session_start", async (_event, ctx: ExtensionContext) => {
90
+ // Reload config with the real session cwd (project config may differ)
91
+ let sessionConfig = config;
92
+ try {
93
+ sessionConfig = await loadConfig(ctx.cwd);
94
+ } catch (err) {
95
+ const msg = err instanceof McpError ? err.userMessage : String(err);
96
+ ctx.ui.notify(`pi-mcp: Config error — ${msg}`, "error");
97
+ return;
98
+ }
99
+
100
+ // If config changed (different cwd with project-level overrides),
101
+ // shut down old servers and rebuild the manager's server list
102
+ if (JSON.stringify(sessionConfig) !== JSON.stringify(config)) {
103
+ // Deactivate and remove all tools from old config
104
+ for (const server of manager.getAllServers()) {
105
+ bridge.removeServer(server.name);
106
+ }
107
+ // Shut down all running servers
108
+ await manager.shutdownAll();
109
+ // Rebuild server entries from new config
110
+ manager.rebuildServers(sessionConfig);
111
+ }
112
+
113
+ const eagerServers = Object.entries(sessionConfig.mcpServers).filter(([, cfg]) => cfg.lifecycle === "eager");
114
+
115
+ // Start all eager servers concurrently
116
+ await Promise.allSettled(
117
+ eagerServers.map(async ([name]) => {
118
+ try {
119
+ await manager.startServer(name, ctx.cwd);
120
+ } catch (err) {
121
+ const msg = err instanceof McpError ? err.userMessage : String(err);
122
+ ctx.ui.notify(`pi-mcp: Failed to start ${name} — ${msg}`, "error");
123
+ }
124
+ }),
125
+ );
126
+ });
127
+
128
+ pi.on("session_shutdown", async (_event, _ctx: ExtensionContext) => {
129
+ // Stop the callback server
130
+ await stopCallbackServer().catch(() => {});
131
+
132
+ // Deactivate all tools before shutting down servers
133
+ for (const server of manager.getAllServers()) {
134
+ bridge.deactivateServer(server.name);
135
+ }
136
+ await manager.shutdownAll();
137
+ });
138
+
139
+ // ── 4. /mcp — show server status ─────────────────────────────────────────
140
+ pi.registerCommand("mcp", {
141
+ description: "Show MCP server status. Usage: /mcp [server-name] for detail.",
142
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
143
+ const serverName = args.trim();
144
+ if (serverName) {
145
+ // Detailed view: status + recent stderr
146
+ const server = manager.getServer(serverName);
147
+ if (!server) {
148
+ ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
149
+ return;
150
+ }
151
+ const logs = manager.getServerLogs(serverName);
152
+ const detail = [
153
+ `Server: ${serverName}`,
154
+ `State: ${server.state}`,
155
+ `Retries: ${server.retryCount}`,
156
+ server.lastError ? `Last error: ${server.lastError.message}` : null,
157
+ "",
158
+ "Recent output:",
159
+ logs,
160
+ ]
161
+ .filter(Boolean)
162
+ .join("\n");
163
+ ctx.ui.notify(detail, "info");
164
+ } else if (manager.getAllServers().length === 0) {
165
+ // No servers configured yet: guide the user instead of showing nothing.
166
+ ctx.ui.notify(
167
+ 'No MCP servers configured. Create ~/.phi/agent/mcp.json (global) or .phi/mcp.json (project) with an "mcpServers" block, e.g.:\n' +
168
+ '{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."], "lifecycle": "eager" } } }',
169
+ "info",
170
+ );
171
+ } else {
172
+ // Summary view: all servers
173
+ ctx.ui.notify(manager.getStatusSummary(), "info");
174
+ }
175
+ },
176
+ });
177
+
178
+ // ── 5. /mcp:stop — stop a server ─────────────────────────────────────────
179
+ pi.registerCommand("mcp:stop", {
180
+ description: "Stop an MCP server. Usage: /mcp:stop <server-name>",
181
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
182
+ const serverName = args.trim();
183
+ if (!serverName) {
184
+ ctx.ui.notify("Usage: /mcp:stop <server-name>", "error");
185
+ return;
186
+ }
187
+ if (!manager.getServer(serverName)) {
188
+ ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
189
+ return;
190
+ }
191
+ bridge.deactivateServer(serverName);
192
+ await manager.stopServer(serverName);
193
+ ctx.ui.notify(`pi-mcp: Stopped ${serverName}`, "info");
194
+ },
195
+ });
196
+
197
+ // ── 6. /mcp:start — manually start a lazy server ─────────────────────────
198
+ pi.registerCommand("mcp:start", {
199
+ description: "Start an MCP server. Usage: /mcp:start <server-name>",
200
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
201
+ const serverName = args.trim();
202
+ if (!serverName) {
203
+ ctx.ui.notify("Usage: /mcp:start <server-name>", "error");
204
+ return;
205
+ }
206
+ if (!manager.getServer(serverName)) {
207
+ ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
208
+ return;
209
+ }
210
+ try {
211
+ await manager.startServer(serverName, ctx.cwd);
212
+ ctx.ui.notify(`pi-mcp: Started ${serverName}`, "info");
213
+ } catch (err) {
214
+ const msg = err instanceof McpError ? err.userMessage : String(err);
215
+ ctx.ui.notify(`pi-mcp: Failed to start ${serverName} — ${msg}`, "error");
216
+ }
217
+ },
218
+ });
219
+
220
+ // ── 7. /mcp:auth trigger OAuth authentication for a server ────────────────
221
+ pi.registerCommand("mcp:auth", {
222
+ description:
223
+ "Trigger OAuth authentication for a server. Resets credentials and opens browser for re-authorization. Usage: /mcp:auth <server-name>",
224
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
225
+ const serverName = args.trim();
226
+ if (!serverName) {
227
+ // List servers with auth config
228
+ const authServers = manager.getAllServers().filter((s) => s.config.auth);
229
+ if (authServers.length === 0) {
230
+ ctx.ui.notify(
231
+ 'pi-mcp: No servers with OAuth configured. Add `auth: { type: "oauth" }` to a server in mcp.json.',
232
+ "error",
233
+ );
234
+ return;
235
+ }
236
+ const lines = authServers.map(async (s) => {
237
+ const status = await manager.getServerAuthStatus(s.name);
238
+ const authIcon = status?.hasTokens ? "\u2705 authenticated" : "\u274C not authenticated";
239
+ const savedInfo = status?.savedAt ? ` (since ${status.savedAt})` : "";
240
+ return ` ${s.name}: ${authIcon}${savedInfo}`;
241
+ });
242
+ const statusLines = await Promise.all(lines);
243
+ ctx.ui.notify(
244
+ ["Usage: /mcp:auth <server-name>", "", "OAuth-enabled servers:", ...statusLines].join("\n"),
245
+ "info",
246
+ );
247
+ return;
248
+ }
249
+ const server = manager.getServer(serverName);
250
+ if (!server) {
251
+ ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
252
+ return;
253
+ }
254
+ if (!server.config.auth) {
255
+ ctx.ui.notify(
256
+ `pi-mcp: Server "${serverName}" does not have OAuth configured. Add \`auth: { type: "oauth" }\` to its config in mcp.json.`,
257
+ "error",
258
+ );
259
+ return;
260
+ }
261
+
262
+ const config = server.config;
263
+ let oauthState: string | undefined;
264
+
265
+ try {
266
+ // Stop the server if running
267
+ if (server.state !== "stopped") {
268
+ bridge.deactivateServer(serverName);
269
+ await manager.stopServer(serverName);
270
+ }
271
+
272
+ // Validate that we have a server URL (required for OAuth)
273
+ if (!config.url) {
274
+ throw new McpError(
275
+ `Server "${serverName}" has OAuth configured but no URL. OAuth requires a URL-based server transport.`,
276
+ serverName,
277
+ "config",
278
+ );
279
+ }
280
+
281
+ // Reset all OAuth credentials (tokens, client info, PKCE, discovery)
282
+ await manager.resetServerAuth(serverName);
283
+
284
+ ctx.ui.notify(`pi-mcp: Starting OAuth flow for ${serverName}...`, "info");
285
+
286
+ // 1. Start the callback server
287
+ const port = await ensureCallbackServer();
288
+ setCallbackPort(port);
289
+
290
+ // 2. Generate a cryptographically secure state parameter for CSRF protection
291
+ oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
292
+ .map((b: number) => b.toString(16).padStart(2, "0"))
293
+ .join("");
294
+
295
+ // 3. Register the callback promise BEFORE opening the browser
296
+ const callbackPromise = waitForCallback(oauthState);
297
+
298
+ // 4. Create auth provider and transport
299
+ const authProvider = new McpOAuthProvider(serverName, config.auth || { type: "oauth" }, (url: URL) => {
300
+ console.error(`[pi-mcp] Opening browser for ${serverName}...`);
301
+ openBrowser(url.toString());
302
+ });
303
+
304
+ // CRITICAL FIX #1: Set the OAuth state on the provider before calling auth()
305
+ // This ensures the state parameter is included in the authorization URL
306
+ authProvider.setState(oauthState);
307
+
308
+ const transport = new StreamableHTTPClientTransport(new URL(config.url), { authProvider });
309
+
310
+ // 5. Start the auth flow - this will trigger redirectToAuthorization which opens the browser
311
+ // CRITICAL FIX #2: Check the return value of auth() instead of catching UnauthorizedError
312
+ // The SDK returns 'REDIRECT' when it needs browser interaction, not an error
313
+ const authResult = await auth(authProvider, { serverUrl: config.url });
314
+
315
+ if (authResult === "AUTHORIZED") {
316
+ // Auth succeeded without needing browser interaction (e.g., had valid tokens)
317
+ ctx.ui.notify(`pi-mcp: ${serverName} authenticated successfully!`, "info");
318
+ } else if (authResult === "REDIRECT") {
319
+ // Browser was opened, wait for the callback from the user
320
+ ctx.ui.notify(`pi-mcp: Browser opened for ${serverName}. Complete authorization to continue...`, "info");
321
+
322
+ // 6. Wait for the callback (this blocks until the user authorizes)
323
+ const code = await callbackPromise;
324
+
325
+ // 7. Complete the OAuth flow with the authorization code
326
+ await transport.finishAuth(code);
327
+
328
+ ctx.ui.notify(`pi-mcp: ${serverName} authenticated successfully!`, "info");
329
+ } else {
330
+ throw new McpError(`Unexpected auth result: ${authResult}`, serverName, "protocol");
331
+ }
332
+
333
+ // 8. Close the transport (we'll create a new one when starting the server)
334
+ await transport.close().catch(() => {});
335
+
336
+ // 9. Start the server with fresh auth credentials
337
+ await manager.startServer(serverName, ctx.cwd);
338
+ } catch (err) {
339
+ const msg = err instanceof McpError ? err.userMessage : String(err);
340
+ ctx.ui.notify(`pi-mcp: Authentication failed for ${serverName} — ${msg}`, "error");
341
+
342
+ // Clean up on error
343
+ if (oauthState) {
344
+ cancelCallback(oauthState);
345
+ }
346
+ }
347
+ },
348
+ });
382
349
  }