pi-mcp-extension 1.8.0 → 1.9.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/package.json +1 -1
- package/src/index.ts +149 -12
- package/src/oauth-provider.ts +1 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -80,6 +80,13 @@ export class AuthCancelledError extends Error {
|
|
|
80
80
|
export const authRetryOption = "Retry: open browser again";
|
|
81
81
|
export const authPasteOption = "Paste redirect URL";
|
|
82
82
|
export const authCancelOption = "Cancel authentication";
|
|
83
|
+
export const startupAuthNowOption = "Authorize now";
|
|
84
|
+
export const startupAuthLaterOption = "Not now";
|
|
85
|
+
|
|
86
|
+
function formatErrorMessage(err: unknown): string {
|
|
87
|
+
const msg = err instanceof McpError ? err.userMessage : err instanceof Error ? err.message : String(err);
|
|
88
|
+
return msg.replace(/^AuthRequiredError:\s*/, "");
|
|
89
|
+
}
|
|
83
90
|
|
|
84
91
|
interface OAuthCallbackResult {
|
|
85
92
|
type: "callback";
|
|
@@ -299,8 +306,91 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
299
306
|
const manager = new ServerManager(config, undefined, paths.authStorageDir);
|
|
300
307
|
const bridge = new ToolBridge(config.settings, pi);
|
|
301
308
|
const connectAttempts = new Map<string, { promise: Promise<void>; controller: AbortController; reset: boolean; waiters: number }>();
|
|
309
|
+
const declinedStartupAuth = new Set<string>();
|
|
310
|
+
const startupPromptControllers = new Map<string, AbortController>();
|
|
302
311
|
let authQueue: Promise<void> = Promise.resolve();
|
|
303
312
|
|
|
313
|
+
function enqueueStartupAuthPrompt(name: string, ctx: ExtensionContext): void {
|
|
314
|
+
let release!: () => void;
|
|
315
|
+
const previous = authQueue;
|
|
316
|
+
authQueue = new Promise<void>((resolve) => {
|
|
317
|
+
release = resolve;
|
|
318
|
+
});
|
|
319
|
+
void (async () => {
|
|
320
|
+
try {
|
|
321
|
+
await previous;
|
|
322
|
+
if (
|
|
323
|
+
declinedStartupAuth.has(name) ||
|
|
324
|
+
manager.getServer(name)?.state === "ready" ||
|
|
325
|
+
connectAttempts.has(name)
|
|
326
|
+
) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const promptController = new AbortController();
|
|
330
|
+
startupPromptControllers.set(name, promptController);
|
|
331
|
+
let choice: string | undefined;
|
|
332
|
+
try {
|
|
333
|
+
choice = await new Promise<string | undefined>((resolve, reject) => {
|
|
334
|
+
if (promptController.signal.aborted) {
|
|
335
|
+
resolve(undefined);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const onAbort = () => resolve(undefined);
|
|
339
|
+
promptController.signal.addEventListener("abort", onAbort, { once: true });
|
|
340
|
+
ctx.ui
|
|
341
|
+
.select(
|
|
342
|
+
`${name} needs authorization`,
|
|
343
|
+
[startupAuthNowOption, startupAuthLaterOption],
|
|
344
|
+
{ signal: promptController.signal },
|
|
345
|
+
)
|
|
346
|
+
.then(
|
|
347
|
+
(value) => {
|
|
348
|
+
promptController.signal.removeEventListener("abort", onAbort);
|
|
349
|
+
resolve(value);
|
|
350
|
+
},
|
|
351
|
+
(err) => {
|
|
352
|
+
promptController.signal.removeEventListener("abort", onAbort);
|
|
353
|
+
if (promptController.signal.aborted) resolve(undefined);
|
|
354
|
+
else reject(err);
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
} finally {
|
|
359
|
+
if (startupPromptControllers.get(name) === promptController) {
|
|
360
|
+
startupPromptControllers.delete(name);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (
|
|
364
|
+
promptController.signal.aborted ||
|
|
365
|
+
manager.getServer(name)?.state === "ready" ||
|
|
366
|
+
connectAttempts.has(name)
|
|
367
|
+
) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (choice === startupAuthNowOption) {
|
|
371
|
+
try {
|
|
372
|
+
await connectRequested(name, ctx, undefined, false, true);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (!(err instanceof AuthCancelledError)) {
|
|
375
|
+
const msg = formatErrorMessage(err);
|
|
376
|
+
ctx.ui.notify(`pi-mcp: Failed to start ${name} — ${msg}`, "error");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
declinedStartupAuth.add(name);
|
|
381
|
+
ctx.ui.setStatus?.("pi-mcp", `run /mcp:auth ${name}`);
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (!(err instanceof AuthCancelledError)) {
|
|
385
|
+
const msg = formatErrorMessage(err);
|
|
386
|
+
ctx.ui.notify(`pi-mcp: Failed to start ${name} — ${msg}`, "error");
|
|
387
|
+
}
|
|
388
|
+
} finally {
|
|
389
|
+
release();
|
|
390
|
+
}
|
|
391
|
+
})();
|
|
392
|
+
}
|
|
393
|
+
|
|
304
394
|
manager.setToolRefreshCallback(async (serverName, client) => {
|
|
305
395
|
await bridge.refreshTools(serverName, client, () => {
|
|
306
396
|
const server = manager.getServer(serverName);
|
|
@@ -316,7 +406,7 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
316
406
|
try {
|
|
317
407
|
sessionConfig = await loadConfig(ctx.cwd, paths.globalConfigPath);
|
|
318
408
|
} catch (err) {
|
|
319
|
-
const msg =
|
|
409
|
+
const msg = formatErrorMessage(err);
|
|
320
410
|
ctx.ui.notify(`pi-mcp: Config error — ${msg}`, "error");
|
|
321
411
|
return;
|
|
322
412
|
}
|
|
@@ -324,6 +414,11 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
324
414
|
// If config changed (different cwd with project-level overrides),
|
|
325
415
|
// shut down old servers and rebuild the manager's server list
|
|
326
416
|
if (JSON.stringify(sessionConfig) !== JSON.stringify(config)) {
|
|
417
|
+
declinedStartupAuth.clear();
|
|
418
|
+
for (const controller of startupPromptControllers.values()) {
|
|
419
|
+
controller.abort();
|
|
420
|
+
}
|
|
421
|
+
startupPromptControllers.clear();
|
|
327
422
|
// Deactivate and remove all tools from old config
|
|
328
423
|
for (const server of manager.getAllServers()) {
|
|
329
424
|
bridge.removeServer(server.name);
|
|
@@ -339,20 +434,41 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
339
434
|
([, cfg]) => cfg.lifecycle === "eager",
|
|
340
435
|
);
|
|
341
436
|
|
|
437
|
+
const authRequiredServers = new Set<string>();
|
|
438
|
+
|
|
342
439
|
// Start all eager servers concurrently
|
|
343
440
|
await Promise.allSettled(
|
|
344
|
-
eagerServers.map(async ([name]) => {
|
|
441
|
+
eagerServers.map(async ([name, cfg]) => {
|
|
345
442
|
try {
|
|
346
443
|
await manager.startServer(name, ctx.cwd);
|
|
347
444
|
} catch (err) {
|
|
348
|
-
|
|
445
|
+
if (err instanceof AuthRequiredError && cfg.auth && ctx.hasUI) {
|
|
446
|
+
authRequiredServers.add(name);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const msg = formatErrorMessage(err);
|
|
349
450
|
ctx.ui.notify(`pi-mcp: Failed to start ${name} — ${msg}`, "error");
|
|
350
451
|
}
|
|
351
452
|
}),
|
|
352
453
|
);
|
|
454
|
+
|
|
455
|
+
for (const [name] of eagerServers) {
|
|
456
|
+
if (!authRequiredServers.has(name)) continue;
|
|
457
|
+
if (declinedStartupAuth.has(name)) {
|
|
458
|
+
ctx.ui.setStatus?.("pi-mcp", `run /mcp:auth ${name}`);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
enqueueStartupAuthPrompt(name, ctx);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
for (let i = 0; i < 4 * authRequiredServers.size; i++) {
|
|
465
|
+
await Promise.resolve();
|
|
466
|
+
}
|
|
353
467
|
});
|
|
354
468
|
|
|
355
469
|
pi.on("session_shutdown", async (_event, _ctx: ExtensionContext) => {
|
|
470
|
+
for (const controller of startupPromptControllers.values()) controller.abort();
|
|
471
|
+
startupPromptControllers.clear();
|
|
356
472
|
for (const attempt of connectAttempts.values()) attempt.controller.abort();
|
|
357
473
|
await stopCallbackServer().catch(() => {});
|
|
358
474
|
|
|
@@ -410,6 +526,7 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
410
526
|
ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
|
|
411
527
|
return;
|
|
412
528
|
}
|
|
529
|
+
startupPromptControllers.get(serverName)?.abort();
|
|
413
530
|
connectAttempts.get(serverName)?.controller.abort();
|
|
414
531
|
bridge.deactivateServer(serverName);
|
|
415
532
|
await manager.stopServer(serverName);
|
|
@@ -435,7 +552,7 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
435
552
|
await connectRequested(serverName, ctx);
|
|
436
553
|
ctx.ui.notify(`pi-mcp: Started ${serverName}`, "info");
|
|
437
554
|
} catch (err) {
|
|
438
|
-
const msg =
|
|
555
|
+
const msg = formatErrorMessage(err);
|
|
439
556
|
ctx.ui.notify(`pi-mcp: Failed to start ${serverName} — ${msg}`, "error");
|
|
440
557
|
}
|
|
441
558
|
},
|
|
@@ -653,12 +770,21 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
653
770
|
}
|
|
654
771
|
}
|
|
655
772
|
|
|
656
|
-
async function connectRequested(
|
|
773
|
+
async function connectRequested(
|
|
774
|
+
serverName: string,
|
|
775
|
+
ctx: ExtensionContext,
|
|
776
|
+
signal?: AbortSignal,
|
|
777
|
+
reset = false,
|
|
778
|
+
alreadyQueued = false,
|
|
779
|
+
): Promise<void> {
|
|
657
780
|
const server = manager.getServer(serverName);
|
|
658
781
|
if (!server) throw new McpError(`Unknown server "${serverName}"`, serverName, "config");
|
|
659
782
|
if (signal?.aborted) throw new AuthCancelledError(serverName);
|
|
660
783
|
if (reset && !server.config.auth) throw new McpError(`Server "${serverName}" has no OAuth configuration`, serverName, "config");
|
|
661
784
|
if (reset && !ctx.hasUI) throw new AuthRequiredError(serverName);
|
|
785
|
+
if (!alreadyQueued) {
|
|
786
|
+
startupPromptControllers.get(serverName)?.abort();
|
|
787
|
+
}
|
|
662
788
|
|
|
663
789
|
const waitForAttempt = async (entry: { promise: Promise<void>; controller: AbortController; reset: boolean; waiters: number }): Promise<void> => {
|
|
664
790
|
entry.waiters++;
|
|
@@ -725,24 +851,35 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
725
851
|
if (!reset) await manager.startServer(serverName, ctx.cwd);
|
|
726
852
|
else throw new AuthRequiredError(serverName);
|
|
727
853
|
if (controller.signal.aborted) { await manager.stopServer(serverName); throw new AuthCancelledError(serverName); }
|
|
854
|
+
declinedStartupAuth.delete(serverName);
|
|
855
|
+
if (declinedStartupAuth.size === 0) ctx.ui.setStatus?.("pi-mcp", undefined);
|
|
728
856
|
return;
|
|
729
857
|
} catch (err) {
|
|
730
858
|
if (!(err instanceof AuthRequiredError) || !server.config.auth) throw err;
|
|
731
859
|
}
|
|
732
860
|
if (!ctx.hasUI) throw new AuthRequiredError(serverName);
|
|
733
|
-
|
|
734
|
-
const previous = authQueue;
|
|
735
|
-
authQueue = new Promise<void>((resolve) => { release = resolve; });
|
|
736
|
-
try {
|
|
737
|
-
await previous;
|
|
861
|
+
const runInteractiveAuth = async (): Promise<void> => {
|
|
738
862
|
if (controller.signal.aborted) throw new AuthCancelledError(serverName);
|
|
739
863
|
await authorize(serverName, ctx, controller.signal, reset);
|
|
740
864
|
if (controller.signal.aborted) throw new AuthCancelledError(serverName);
|
|
741
865
|
await manager.startServer(serverName, ctx.cwd);
|
|
742
866
|
if (controller.signal.aborted) { await manager.stopServer(serverName); throw new AuthCancelledError(serverName); }
|
|
743
|
-
}
|
|
744
|
-
|
|
867
|
+
};
|
|
868
|
+
if (alreadyQueued) {
|
|
869
|
+
await runInteractiveAuth();
|
|
870
|
+
} else {
|
|
871
|
+
let release!: () => void;
|
|
872
|
+
const previous = authQueue;
|
|
873
|
+
authQueue = new Promise<void>((resolve) => { release = resolve; });
|
|
874
|
+
try {
|
|
875
|
+
await previous;
|
|
876
|
+
await runInteractiveAuth();
|
|
877
|
+
} finally {
|
|
878
|
+
release();
|
|
879
|
+
}
|
|
745
880
|
}
|
|
881
|
+
declinedStartupAuth.delete(serverName);
|
|
882
|
+
if (declinedStartupAuth.size === 0) ctx.ui.setStatus?.("pi-mcp", undefined);
|
|
746
883
|
})().catch((err: unknown) => {
|
|
747
884
|
if (controller.signal.aborted) throw new AuthCancelledError(serverName);
|
|
748
885
|
throw err;
|
package/src/oauth-provider.ts
CHANGED
|
@@ -263,6 +263,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
263
263
|
...(state.clientInfo.client_secret && { client_secret: state.clientInfo.client_secret }),
|
|
264
264
|
};
|
|
265
265
|
}
|
|
266
|
+
if (this.silent) throw new AuthRequiredError(this.serverName);
|
|
266
267
|
return undefined;
|
|
267
268
|
}
|
|
268
269
|
|