pi-mcp-extension 1.7.1 → 1.7.2
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/README.md +1 -1
- package/package.json +1 -1
- package/src/callback-server.ts +5 -1
- package/src/index.ts +12 -5
- package/src/oauth-provider.ts +6 -1
- package/src/server-manager.ts +18 -3
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Config files are loaded from two locations. **Project config overrides global co
|
|
|
63
63
|
|
|
64
64
|
| Location | Scope |
|
|
65
65
|
|---|---|
|
|
66
|
-
| `~/.pi/agent/mcp.json` | Global — applies to all projects |
|
|
66
|
+
| `~/.pi/agent/mcp.json` | Global — applies to all projects (honors `$PI_CODING_AGENT_DIR` when set: `<agent dir>/mcp.json`) |
|
|
67
67
|
| `.pi/mcp.json` | Project — overrides global per-server |
|
|
68
68
|
|
|
69
69
|
### Full Example
|
package/package.json
CHANGED
package/src/callback-server.ts
CHANGED
|
@@ -193,7 +193,11 @@ export function callbackServerConfigFromRedirectUrl(redirectUrl?: string): Callb
|
|
|
193
193
|
throw new Error(`Local OAuth redirect URL must use path ${CALLBACK_PATH}`);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
if (!url.port) {
|
|
197
|
+
throw new Error("Manual OAuth redirect URL must include an explicit port (e.g. http://127.0.0.1:19876/callback)");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const preferredPort = Number(url.port);
|
|
197
201
|
if (preferredPort === 0) {
|
|
198
202
|
throw new Error("Manual OAuth redirect URL must use a fixed non-zero port");
|
|
199
203
|
}
|
package/src/index.ts
CHANGED
|
@@ -418,9 +418,12 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
418
418
|
() => signal.aborted,
|
|
419
419
|
);
|
|
420
420
|
|
|
421
|
-
//
|
|
421
|
+
// Why set state before calling auth(): the SDK reads provider.state() while
|
|
422
|
+
// building the authorization URL inside auth(), and omits the state parameter
|
|
423
|
+
// if empty, which the callback server then rejects as a CSRF error.
|
|
422
424
|
authProvider.setState(oauthState);
|
|
423
425
|
|
|
426
|
+
// 4. Discover protected-resource challenge metadata from the server.
|
|
424
427
|
const authChallenge: ManualAuthChallenge = await discoverManualAuthChallenge(config.url, {
|
|
425
428
|
headers: config.headers,
|
|
426
429
|
}).catch((): ManualAuthChallenge => {
|
|
@@ -430,7 +433,7 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
430
433
|
return {};
|
|
431
434
|
});
|
|
432
435
|
|
|
433
|
-
// Register the callback immediately before auth() can open the browser.
|
|
436
|
+
// 5. Register the callback immediately before auth() can open the browser.
|
|
434
437
|
// Attach a catch handler so cancellation after immediate authorization
|
|
435
438
|
// does not produce an unhandled rejection.
|
|
436
439
|
const callbackPromise = waitForCallback(oauthState);
|
|
@@ -448,7 +451,11 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
448
451
|
authOptions.scope = authChallenge.scope;
|
|
449
452
|
}
|
|
450
453
|
|
|
451
|
-
// Start the auth flow.
|
|
454
|
+
// 6. Start the auth flow.
|
|
455
|
+
// Why branch on the return value: the SDK auth() call returns "AUTHORIZED" when
|
|
456
|
+
// stored or refreshed tokens already succeeded without opening a browser, or
|
|
457
|
+
// "REDIRECT" after invoking redirectToAuthorization(), in which case we must
|
|
458
|
+
// wait for the callback code and call auth() a second time with authorizationCode.
|
|
452
459
|
const authResult = await auth(authProvider, authOptions);
|
|
453
460
|
if (signal.aborted) throw new AuthCancelledError(serverName);
|
|
454
461
|
|
|
@@ -462,7 +469,7 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
462
469
|
"info",
|
|
463
470
|
);
|
|
464
471
|
|
|
465
|
-
//
|
|
472
|
+
// 7. Wait for the callback while giving the user an escape hatch.
|
|
466
473
|
const code = await waitForOAuthCallback(
|
|
467
474
|
serverName,
|
|
468
475
|
callbackPromise,
|
|
@@ -483,7 +490,7 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
483
490
|
`pi-mcp: Authorization callback received for ${serverName}. Exchanging token...`,
|
|
484
491
|
"info",
|
|
485
492
|
);
|
|
486
|
-
//
|
|
493
|
+
// 8. Complete the OAuth flow with the authorization code
|
|
487
494
|
const finishAuthOptions: {
|
|
488
495
|
serverUrl: string;
|
|
489
496
|
authorizationCode: string;
|
package/src/oauth-provider.ts
CHANGED
|
@@ -405,7 +405,12 @@ export async function getAuthStatus(serverName: string, storageDir?: string): Pr
|
|
|
405
405
|
* Used to force re-authorization on next connection.
|
|
406
406
|
*/
|
|
407
407
|
export async function resetAuth(serverName: string, storageDir?: string): Promise<void> {
|
|
408
|
-
|
|
408
|
+
try {
|
|
409
|
+
await unlink(statePath(serverName, storageDir));
|
|
410
|
+
} catch (err) {
|
|
411
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
409
414
|
}
|
|
410
415
|
|
|
411
416
|
export class AuthRequiredError extends Error {
|
package/src/server-manager.ts
CHANGED
|
@@ -25,7 +25,16 @@ import {
|
|
|
25
25
|
import { McpError } from "./errors.js";
|
|
26
26
|
import type { McpConfig, ServerConfig, Settings } from "./config.js";
|
|
27
27
|
import { McpOAuthProvider, AuthRequiredError, getAuthStatus, resetAuth } from "./oauth-provider.js";
|
|
28
|
-
import type
|
|
28
|
+
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
29
|
+
|
|
30
|
+
export function isOAuthUnauthorizedError(error: unknown): boolean {
|
|
31
|
+
return (
|
|
32
|
+
error instanceof AuthRequiredError ||
|
|
33
|
+
error instanceof UnauthorizedError ||
|
|
34
|
+
(typeof (error as { code?: unknown })?.code === "number" && (error as { code: number }).code === 401) ||
|
|
35
|
+
/\b(401|Unauthorized)\b/i.test(error instanceof Error ? error.message : String(error))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
29
38
|
|
|
30
39
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
31
40
|
|
|
@@ -356,7 +365,7 @@ export class ServerManager {
|
|
|
356
365
|
try {
|
|
357
366
|
await client.connect(transport);
|
|
358
367
|
if (signal.aborted) throw new Error("Connection cancelled");
|
|
359
|
-
|
|
368
|
+
server.childPid = transport instanceof StdioClientTransport ? transport.pid : null;
|
|
360
369
|
// Readiness includes tool discovery, not just the MCP handshake.
|
|
361
370
|
if (this.onToolRefresh) await this.onToolRefresh(server.name, client);
|
|
362
371
|
if (signal.aborted) throw new Error("Connection cancelled");
|
|
@@ -388,10 +397,16 @@ export class ServerManager {
|
|
|
388
397
|
if (signal.aborted) throw new Error("Connection cancelled");
|
|
389
398
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
390
399
|
server.lastError = error;
|
|
391
|
-
if (error instanceof AuthRequiredError || error instanceof McpError && error.code === "protocol") {
|
|
400
|
+
if (error instanceof AuthRequiredError || (error instanceof McpError && error.code === "protocol")) {
|
|
392
401
|
server.state = "stopped";
|
|
393
402
|
throw error;
|
|
394
403
|
}
|
|
404
|
+
if (Boolean(server.config.auth) && server.config.transport !== "stdio" && isOAuthUnauthorizedError(error)) {
|
|
405
|
+
const authError = new AuthRequiredError(server.name);
|
|
406
|
+
server.lastError = authError;
|
|
407
|
+
server.state = "stopped";
|
|
408
|
+
throw authError;
|
|
409
|
+
}
|
|
395
410
|
if (server.retryCount >= this.settings.maxRetries) {
|
|
396
411
|
server.state = "stopped";
|
|
397
412
|
throw new McpError(`Connection failed after ${server.retryCount} retries: ${error.message}`, server.name, "connection", err);
|