pi-mcp-extension 1.7.3 → 1.8.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/callback-server.ts +28 -47
- package/src/callback-url.ts +136 -0
- package/src/index.ts +128 -28
package/package.json
CHANGED
package/src/callback-server.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "http";
|
|
9
9
|
import type { Socket } from "net";
|
|
10
10
|
import { URL } from "url";
|
|
11
|
+
import { CallbackUrlValidationError, isLocalCallbackHost, parseCallbackUrl } from "./callback-url.js";
|
|
11
12
|
|
|
12
13
|
interface PendingAuth {
|
|
13
14
|
resolve: (code: string) => void;
|
|
@@ -73,72 +74,52 @@ function escapeHtml(text: string): string {
|
|
|
73
74
|
* Handle incoming HTTP requests to the callback server.
|
|
74
75
|
*/
|
|
75
76
|
function handleRequest(req: IncomingMessage, res: ServerResponse): void {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
77
|
+
let parsed: ReturnType<typeof parseCallbackUrl>;
|
|
78
|
+
try {
|
|
79
|
+
const requestUrl = new URL(req.url || "/", "http://localhost").toString();
|
|
80
|
+
parsed = parseCallbackUrl(requestUrl, {
|
|
81
|
+
expectedState: (state) => pendingAuths.has(state),
|
|
82
|
+
});
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err instanceof CallbackUrlValidationError) {
|
|
85
|
+
if (err.reason === "wrong_path") {
|
|
86
|
+
res.writeHead(404, { "Content-Type": "text/plain", "Connection": "close" });
|
|
87
|
+
res.end("Not found");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
res.writeHead(400, { "Content-Type": "text/html", "Connection": "close" });
|
|
91
|
+
res.end(HTML_ERROR(err.message));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
93
94
|
res.writeHead(400, { "Content-Type": "text/html", "Connection": "close" });
|
|
94
|
-
res.end(HTML_ERROR(
|
|
95
|
+
res.end(HTML_ERROR("Invalid callback request"));
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
// Handle OAuth errors
|
|
99
|
-
if (error) {
|
|
100
|
-
const errorMsg = errorDescription || error;
|
|
100
|
+
if ("error" in parsed) {
|
|
101
101
|
res.writeHead(200, { "Content-Type": "text/html", "Connection": "close" });
|
|
102
|
-
res.end(HTML_ERROR(
|
|
103
|
-
if (pendingAuths.has(state)) {
|
|
104
|
-
const pending = pendingAuths.get(state)!;
|
|
102
|
+
res.end(HTML_ERROR(parsed.error));
|
|
103
|
+
if (pendingAuths.has(parsed.state)) {
|
|
104
|
+
const pending = pendingAuths.get(parsed.state)!;
|
|
105
105
|
clearTimeout(pending.timeout);
|
|
106
|
-
pendingAuths.delete(state);
|
|
107
|
-
setTimeout(() => pending.reject(new Error(
|
|
106
|
+
pendingAuths.delete(parsed.state);
|
|
107
|
+
setTimeout(() => pending.reject(new Error(parsed.error)), 0);
|
|
108
108
|
}
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
if (!code) {
|
|
114
|
-
res.writeHead(400, { "Content-Type": "text/html", "Connection": "close" });
|
|
115
|
-
res.end(HTML_ERROR("No authorization code provided"));
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Validate state parameter
|
|
120
|
-
if (!pendingAuths.has(state)) {
|
|
121
|
-
const errorMsg = "Invalid or expired state parameter - potential CSRF attack";
|
|
122
|
-
res.writeHead(400, { "Content-Type": "text/html", "Connection": "close" });
|
|
123
|
-
res.end(HTML_ERROR(errorMsg));
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const pending = pendingAuths.get(state)!;
|
|
112
|
+
const pending = pendingAuths.get(parsed.state)!;
|
|
128
113
|
|
|
129
114
|
// Clear timeout and resolve the pending promise
|
|
130
115
|
clearTimeout(pending.timeout);
|
|
131
|
-
pendingAuths.delete(state);
|
|
132
|
-
pending.resolve(code);
|
|
116
|
+
pendingAuths.delete(parsed.state);
|
|
117
|
+
pending.resolve(parsed.code);
|
|
133
118
|
|
|
134
119
|
res.writeHead(200, { "Content-Type": "text/html", "Connection": "close" });
|
|
135
120
|
res.end(HTML_SUCCESS);
|
|
136
121
|
}
|
|
137
122
|
|
|
138
|
-
function isLocalCallbackHost(hostname: string): boolean {
|
|
139
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
123
|
async function closeCurrentServer(): Promise<void> {
|
|
143
124
|
if (!server) return;
|
|
144
125
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure URL validation and parsing helpers for MCP OAuth authorization and callbacks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type CallbackUrlValidationReason =
|
|
6
|
+
| "invalid_url"
|
|
7
|
+
| "wrong_origin"
|
|
8
|
+
| "wrong_path"
|
|
9
|
+
| "missing_state"
|
|
10
|
+
| "invalid_state"
|
|
11
|
+
| "missing_code";
|
|
12
|
+
|
|
13
|
+
export class CallbackUrlValidationError extends Error {
|
|
14
|
+
readonly reason: CallbackUrlValidationReason;
|
|
15
|
+
|
|
16
|
+
constructor(reason: CallbackUrlValidationReason, message: string) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CallbackUrlValidationError";
|
|
19
|
+
this.reason = reason;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ParseCallbackUrlOptions {
|
|
24
|
+
redirectUrl?: string | URL | undefined;
|
|
25
|
+
expectedState?: string | ((state: string) => boolean) | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isLocalCallbackHost(hostname: string): boolean {
|
|
29
|
+
return (
|
|
30
|
+
hostname === "localhost" ||
|
|
31
|
+
hostname === "127.0.0.1" ||
|
|
32
|
+
hostname === "::1" ||
|
|
33
|
+
hostname === "[::1]"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate that an OAuth authorization URL is safe to pass to a system browser opener.
|
|
39
|
+
* Only allows `https:` URLs or loopback `http:` URLs (`localhost`, `127.0.0.1`, `::1`, `[::1]`).
|
|
40
|
+
*/
|
|
41
|
+
export function isSafeAuthorizationUrl(input: string | URL): boolean {
|
|
42
|
+
let url: URL;
|
|
43
|
+
try {
|
|
44
|
+
url = input instanceof URL ? input : new URL(input);
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (url.protocol === "https:") {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (url.protocol === "http:" && isLocalCallbackHost(url.hostname)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse and validate an OAuth callback URL (either from an incoming HTTP callback request
|
|
62
|
+
* or from a user-pasted redirect URL).
|
|
63
|
+
*/
|
|
64
|
+
export function parseCallbackUrl(
|
|
65
|
+
input: string,
|
|
66
|
+
options: ParseCallbackUrlOptions = {},
|
|
67
|
+
): { state: string; code: string } | { state: string; error: string } {
|
|
68
|
+
const trimmed = input.trim();
|
|
69
|
+
let url: URL;
|
|
70
|
+
try {
|
|
71
|
+
url = new URL(trimmed);
|
|
72
|
+
} catch {
|
|
73
|
+
throw new CallbackUrlValidationError("invalid_url", "Invalid redirect URL");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (options.redirectUrl !== undefined) {
|
|
77
|
+
const expected =
|
|
78
|
+
options.redirectUrl instanceof URL
|
|
79
|
+
? options.redirectUrl
|
|
80
|
+
: new URL(String(options.redirectUrl));
|
|
81
|
+
if (
|
|
82
|
+
url.protocol !== expected.protocol ||
|
|
83
|
+
url.hostname !== expected.hostname ||
|
|
84
|
+
url.port !== expected.port
|
|
85
|
+
) {
|
|
86
|
+
throw new CallbackUrlValidationError(
|
|
87
|
+
"wrong_origin",
|
|
88
|
+
`Redirect URL origin must match ${expected.origin}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (url.pathname !== expected.pathname) {
|
|
92
|
+
throw new CallbackUrlValidationError(
|
|
93
|
+
"wrong_path",
|
|
94
|
+
`Redirect URL path must be ${expected.pathname}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
} else if (url.pathname !== "/callback") {
|
|
98
|
+
throw new CallbackUrlValidationError("wrong_path", "Redirect URL path must be /callback");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const code = url.searchParams.get("code");
|
|
102
|
+
const state = url.searchParams.get("state");
|
|
103
|
+
const error = url.searchParams.get("error");
|
|
104
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
105
|
+
|
|
106
|
+
if (!state) {
|
|
107
|
+
throw new CallbackUrlValidationError(
|
|
108
|
+
"missing_state",
|
|
109
|
+
"Missing required state parameter - potential CSRF attack",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof options.expectedState === "string" && state !== options.expectedState) {
|
|
114
|
+
throw new CallbackUrlValidationError(
|
|
115
|
+
"invalid_state",
|
|
116
|
+
"Invalid or expired state parameter - potential CSRF attack",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (error) {
|
|
121
|
+
return { state, error: errorDescription || error };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!code) {
|
|
125
|
+
throw new CallbackUrlValidationError("missing_code", "No authorization code provided");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof options.expectedState === "function" && !options.expectedState(state)) {
|
|
129
|
+
throw new CallbackUrlValidationError(
|
|
130
|
+
"invalid_state",
|
|
131
|
+
"Invalid or expired state parameter - potential CSRF attack",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { state, code };
|
|
136
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
stopCallbackServer,
|
|
25
25
|
callbackServerConfigFromRedirectUrl,
|
|
26
26
|
} from "./callback-server.js";
|
|
27
|
+
import { CallbackUrlValidationError, isSafeAuthorizationUrl, parseCallbackUrl } from "./callback-url.js";
|
|
27
28
|
import { setCallbackPort, McpOAuthProvider, acquireAuthLock, type AuthLockHandle } from "./oauth-provider.js";
|
|
28
29
|
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
29
30
|
import { discoverManualAuthChallenge } from "./auth-challenge.js";
|
|
@@ -48,19 +49,25 @@ export function browserOpenCommand(url: string): BrowserOpenCommand {
|
|
|
48
49
|
* Open a URL in the user's default browser.
|
|
49
50
|
* Works on macOS, Linux, and Windows.
|
|
50
51
|
*/
|
|
51
|
-
function openBrowser(url: string): void {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
function openBrowser(url: string): Promise<void> {
|
|
53
|
+
return new Promise<void>((resolve, reject) => {
|
|
54
|
+
const { command, args } = browserOpenCommand(url);
|
|
55
|
+
const child = spawn(command, args, {
|
|
56
|
+
detached: true,
|
|
57
|
+
stdio: "ignore",
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
});
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
child.once("error", (err) => {
|
|
62
|
+
console.error(`[pi-mcp] Failed to open browser: ${err.message}`);
|
|
63
|
+
reject(err);
|
|
64
|
+
});
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
child.once("spawn", () => {
|
|
67
|
+
child.unref();
|
|
68
|
+
resolve();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
export class AuthCancelledError extends Error {
|
|
@@ -71,6 +78,7 @@ export class AuthCancelledError extends Error {
|
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
export const authRetryOption = "Retry: open browser again";
|
|
81
|
+
export const authPasteOption = "Paste redirect URL";
|
|
74
82
|
export const authCancelOption = "Cancel authentication";
|
|
75
83
|
|
|
76
84
|
interface OAuthCallbackResult {
|
|
@@ -92,19 +100,35 @@ interface OAuthDialogAbortedResult {
|
|
|
92
100
|
type: "dialogAborted";
|
|
93
101
|
}
|
|
94
102
|
|
|
103
|
+
interface OAuthUserInputResult {
|
|
104
|
+
type: "input";
|
|
105
|
+
value: string | undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
type OAuthWaitResult = OAuthCallbackResult | OAuthCallbackErrorResult | OAuthUserChoiceResult | OAuthDialogAbortedResult;
|
|
109
|
+
type OAuthInputWaitResult = OAuthCallbackResult | OAuthCallbackErrorResult | OAuthUserInputResult | OAuthDialogAbortedResult;
|
|
110
|
+
|
|
111
|
+
export interface OAuthPasteOptions {
|
|
112
|
+
redirectUrl?: string | URL | undefined;
|
|
113
|
+
expectedState?: string | undefined;
|
|
114
|
+
onPasteResolved?: (() => void) | undefined;
|
|
115
|
+
}
|
|
96
116
|
|
|
97
117
|
export async function waitForOAuthCallback(
|
|
98
118
|
serverName: string,
|
|
99
119
|
callbackPromise: Promise<string>,
|
|
100
|
-
reopenBrowser: () => void
|
|
101
|
-
context: {
|
|
120
|
+
reopenBrowser: () => void | Promise<void>,
|
|
121
|
+
context: {
|
|
122
|
+
hasUI: boolean;
|
|
123
|
+
ui: Pick<ExtensionUIContext, "select"> & Partial<Pick<ExtensionUIContext, "input" | "notify">>;
|
|
124
|
+
},
|
|
125
|
+
pasteOptions?: OAuthPasteOptions,
|
|
102
126
|
): Promise<string> {
|
|
103
127
|
if (!context.hasUI) {
|
|
104
128
|
throw new Error(`Authorization for ${serverName} requires an interactive Pi session`);
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
const callbackResultPromise: Promise<
|
|
131
|
+
const callbackResultPromise: Promise<OAuthCallbackResult | OAuthCallbackErrorResult> = callbackPromise
|
|
108
132
|
.then((code): OAuthCallbackResult => ({ type: "callback", code }))
|
|
109
133
|
.catch((error): OAuthCallbackErrorResult => ({ type: "callbackError", error }));
|
|
110
134
|
|
|
@@ -113,7 +137,7 @@ export async function waitForOAuthCallback(
|
|
|
113
137
|
const choicePromise: Promise<OAuthWaitResult> = context.ui
|
|
114
138
|
.select(
|
|
115
139
|
`OAuth pending for ${serverName}`,
|
|
116
|
-
[authRetryOption, authCancelOption],
|
|
140
|
+
[authRetryOption, authPasteOption, authCancelOption],
|
|
117
141
|
{ signal: dialogAbortController.signal },
|
|
118
142
|
)
|
|
119
143
|
.then((choice): OAuthUserChoiceResult => ({ type: "choice", choice }))
|
|
@@ -141,10 +165,66 @@ export async function waitForOAuthCallback(
|
|
|
141
165
|
}
|
|
142
166
|
|
|
143
167
|
if (result.choice === authRetryOption) {
|
|
144
|
-
reopenBrowser();
|
|
168
|
+
await reopenBrowser();
|
|
145
169
|
continue;
|
|
146
170
|
}
|
|
147
171
|
|
|
172
|
+
if (result.choice === authPasteOption) {
|
|
173
|
+
if (!context.ui.input) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const inputAbortController = new AbortController();
|
|
177
|
+
const inputPromise: Promise<OAuthInputWaitResult> = context.ui
|
|
178
|
+
.input(
|
|
179
|
+
`Paste redirect URL for ${serverName}`,
|
|
180
|
+
"http://127.0.0.1:19876/callback?code=...&state=...",
|
|
181
|
+
{ signal: inputAbortController.signal },
|
|
182
|
+
)
|
|
183
|
+
.then((value): OAuthUserInputResult => ({ type: "input", value }))
|
|
184
|
+
.catch((error): OAuthDialogAbortedResult => {
|
|
185
|
+
if (inputAbortController.signal.aborted) {
|
|
186
|
+
return { type: "dialogAborted" };
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const inputResult = await Promise.race([callbackResultPromise, inputPromise]);
|
|
192
|
+
|
|
193
|
+
if (inputResult.type === "callback") {
|
|
194
|
+
inputAbortController.abort();
|
|
195
|
+
return inputResult.code;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (inputResult.type === "callbackError") {
|
|
199
|
+
inputAbortController.abort();
|
|
200
|
+
throw inputResult.error;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (inputResult.type === "dialogAborted" || !inputResult.value?.trim()) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let parsed: ReturnType<typeof parseCallbackUrl>;
|
|
208
|
+
try {
|
|
209
|
+
parsed = parseCallbackUrl(inputResult.value, {
|
|
210
|
+
redirectUrl: pasteOptions?.redirectUrl,
|
|
211
|
+
expectedState: pasteOptions?.expectedState,
|
|
212
|
+
});
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (err instanceof CallbackUrlValidationError) {
|
|
215
|
+
context.ui.notify?.(`pi-mcp: Invalid redirect URL — ${err.message}`, "error");
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
pasteOptions?.onPasteResolved?.();
|
|
222
|
+
if ("error" in parsed) {
|
|
223
|
+
throw new Error(parsed.error);
|
|
224
|
+
}
|
|
225
|
+
return parsed.code;
|
|
226
|
+
}
|
|
227
|
+
|
|
148
228
|
dialogAbortController.abort();
|
|
149
229
|
throw new AuthCancelledError(serverName);
|
|
150
230
|
}
|
|
@@ -194,7 +274,7 @@ export function serverNameCompletions(
|
|
|
194
274
|
return completions.length > 0 ? completions : null;
|
|
195
275
|
}
|
|
196
276
|
|
|
197
|
-
export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string; globalConfigPath?: string; authStorageDir?: string; browserOpen?: (url: string) => void } = {}): Promise<void> {
|
|
277
|
+
export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string; globalConfigPath?: string; authStorageDir?: string; browserOpen?: (url: string) => void | Promise<void> } = {}): Promise<void> {
|
|
198
278
|
// ── 1. Load and validate config ──────────────────────────────────────────
|
|
199
279
|
// cwd is available on the ExtensionContext passed to event handlers.
|
|
200
280
|
// We load config lazily on session_start to get the correct per-session cwd.
|
|
@@ -415,13 +495,34 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
415
495
|
.join("");
|
|
416
496
|
|
|
417
497
|
// 3. Create the auth provider.
|
|
498
|
+
const launchAuthorizationBrowser = async (url: URL): Promise<void> => {
|
|
499
|
+
if (!isSafeAuthorizationUrl(url)) {
|
|
500
|
+
throw new McpError(
|
|
501
|
+
`Refusing to open unsafe authorization URL for "${serverName}": only HTTPS (or loopback HTTP) URLs are allowed`,
|
|
502
|
+
serverName,
|
|
503
|
+
"protocol",
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
latestAuthorizationUrl = new URL(url.toString());
|
|
507
|
+
if (signal.aborted || lockLost) return;
|
|
508
|
+
try {
|
|
509
|
+
await (paths.browserOpen ?? openBrowser)(url.toString());
|
|
510
|
+
ctx.ui.notify(
|
|
511
|
+
`pi-mcp: Authorize ${serverName}: ${url.toString()} (On SSH, forward the callback with ssh -L ${port}:127.0.0.1:${port}, or choose "${authPasteOption}".)`,
|
|
512
|
+
"info",
|
|
513
|
+
);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
ctx.ui.notify(
|
|
516
|
+
`pi-mcp: Failed to open browser for ${serverName} (${err instanceof Error ? err.message : String(err)}). Open ${url.toString()} manually or choose "${authPasteOption}".`,
|
|
517
|
+
"error",
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
418
522
|
const authProvider = new McpOAuthProvider(
|
|
419
523
|
serverName,
|
|
420
524
|
config.auth || { type: "oauth" },
|
|
421
|
-
|
|
422
|
-
latestAuthorizationUrl = new URL(url.toString());
|
|
423
|
-
if (!signal.aborted && !lockLost) (paths.browserOpen ?? openBrowser)(url.toString());
|
|
424
|
-
},
|
|
525
|
+
launchAuthorizationBrowser,
|
|
425
526
|
paths.authStorageDir,
|
|
426
527
|
false,
|
|
427
528
|
() => signal.aborted,
|
|
@@ -474,12 +575,6 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
474
575
|
// Auth succeeded without needing browser interaction (e.g., had valid tokens).
|
|
475
576
|
ctx.ui.notify(`pi-mcp: ${serverName} authenticated successfully. Starting MCP server...`, "info");
|
|
476
577
|
} else if (authResult === "REDIRECT") {
|
|
477
|
-
// Browser was opened, wait for the callback from the user
|
|
478
|
-
ctx.ui.notify(
|
|
479
|
-
`pi-mcp: Browser opened for ${serverName}. Complete authorization to continue. Choose Retry to reopen the browser or Cancel to stop authentication.`,
|
|
480
|
-
"info",
|
|
481
|
-
);
|
|
482
|
-
|
|
483
578
|
// 7. Wait for the callback while giving the user an escape hatch.
|
|
484
579
|
const code = await waitForOAuthCallback(
|
|
485
580
|
serverName,
|
|
@@ -492,9 +587,14 @@ export default async function (pi: ExtensionAPI, paths: { bootstrapCwd?: string;
|
|
|
492
587
|
"protocol",
|
|
493
588
|
);
|
|
494
589
|
}
|
|
495
|
-
|
|
590
|
+
return launchAuthorizationBrowser(latestAuthorizationUrl);
|
|
496
591
|
},
|
|
497
592
|
ctx,
|
|
593
|
+
{
|
|
594
|
+
redirectUrl: authProvider.redirectUrl,
|
|
595
|
+
expectedState: oauthState,
|
|
596
|
+
onPasteResolved: cancel,
|
|
597
|
+
},
|
|
498
598
|
);
|
|
499
599
|
if (signal.aborted) throw new AuthCancelledError(serverName);
|
|
500
600
|
if (lockLost) throw new AuthLockError(serverName, `Authorization lock lost for ${serverName}; retry`);
|