@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.
- package/CHANGELOG.md +83 -0
- package/README.md +92 -123
- package/config/routing.example.json +129 -0
- package/config/routing.schema.json +58 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -6
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +1 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +9 -8
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/pi-user-agent.d.ts.map +1 -1
- package/dist/utils/pi-user-agent.js +4 -1
- package/dist/utils/pi-user-agent.js.map +1 -1
- package/docs/compaction.md +11 -11
- package/docs/custom-provider.md +4 -4
- package/docs/development.md +2 -2
- package/docs/extensions.md +47 -47
- package/docs/fork-policy.md +81 -0
- package/docs/index.md +7 -7
- package/docs/json.md +3 -3
- package/docs/keybindings.md +5 -5
- package/docs/models.md +6 -6
- package/docs/packages.md +37 -37
- package/docs/prompt-templates.md +4 -4
- package/docs/providers.md +9 -9
- package/docs/quickstart.md +25 -25
- package/docs/rpc.md +3 -3
- package/docs/sdk.md +34 -34
- package/docs/session-format.md +4 -4
- package/docs/sessions.md +11 -11
- package/docs/settings.md +9 -9
- package/docs/shell-aliases.md +2 -2
- package/docs/skills.md +9 -9
- package/docs/terminal-setup.md +7 -7
- package/docs/termux.md +6 -6
- package/docs/themes.md +9 -9
- package/docs/tmux.md +3 -3
- package/docs/tui.md +8 -8
- package/docs/usage.md +39 -39
- package/docs/windows.md +2 -2
- package/extensions/phi/agents.ts +12 -117
- package/extensions/phi/benchmark.ts +129 -49
- package/extensions/phi/browser.ts +10 -37
- package/extensions/phi/btw/btw.ts +1 -7
- package/extensions/phi/chrome/index.ts +1283 -741
- package/extensions/phi/commit.ts +9 -14
- package/extensions/phi/goal/index.ts +10 -33
- package/extensions/phi/init.ts +37 -47
- package/extensions/phi/keys.ts +2 -7
- package/extensions/phi/mcp/callback-server.ts +162 -165
- package/extensions/phi/mcp/config.ts +122 -136
- package/extensions/phi/mcp/errors.ts +18 -23
- package/extensions/phi/mcp/index.ts +322 -355
- package/extensions/phi/mcp/oauth-provider.ts +289 -289
- package/extensions/phi/mcp/server-manager.ts +390 -413
- package/extensions/phi/mcp/tool-bridge.ts +381 -415
- package/extensions/phi/memory.ts +2 -2
- package/extensions/phi/models.ts +27 -26
- package/extensions/phi/orchestrator.ts +343 -266
- package/extensions/phi/productivity.ts +4 -2
- package/extensions/phi/providers/agent-def.ts +128 -0
- package/extensions/phi/providers/alibaba.ts +56 -7
- package/extensions/phi/providers/context-window.ts +4 -1
- package/extensions/phi/providers/live-models.ts +5 -20
- package/extensions/phi/providers/orchestrator-helpers.ts +19 -5
- package/extensions/phi/providers/phase-machine.ts +220 -0
- package/extensions/phi/setup.ts +196 -169
- package/extensions/phi/skill-loader.ts +18 -21
- package/extensions/phi/smart-router.ts +6 -6
- package/extensions/phi/web-search.ts +90 -50
- 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
|
|
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 {
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
}
|