Merge pull request #2160: fix(mcp): accept manual OAuth redirect input

This commit is contained in:
can1357
2026-06-10 08:26:58 +02:00
5 changed files with 88 additions and 3 deletions
@@ -46,9 +46,14 @@ import { theme } from "../theme/theme";
import type { InteractiveModeContext } from "../types";
import { groupBySource, parseRemoveArgs, readScopeFlag, showCommandMessage } from "./command-controller-shared";
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
const MCP_MANUAL_INPUT_PROVIDER_ID = "mcp";
const MCP_MANUAL_LOGIN_TIP = "Headless? Paste the redirect URL or code with /login <value>.";
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string, onTimeout?: () => void): Promise<T> {
const { promise: timeoutPromise, reject } = Promise.withResolvers<T>();
const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
const timer = setTimeout(() => {
onTimeout?.();
reject(new Error(message));
}, timeoutMs);
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
}
@@ -591,6 +596,13 @@ export class MCPCommandController {
const resolvedClientId = clientId.trim() || parsedAuthUrl.searchParams.get("client_id") || undefined;
const resolvedClientSecret = clientSecret.trim() || undefined;
const manualInput = this.ctx.oauthManualInput;
if (manualInput.hasPending() && manualInput.pendingProviderId !== MCP_MANUAL_INPUT_PROVIDER_ID) {
throw new Error(
`OAuth login already in progress for ${manualInput.pendingProviderId}. Complete or cancel it before starting MCP OAuth.`,
);
}
const oauthTimeout = new AbortController();
try {
// Create OAuth flow
const flow = new MCPOAuthFlow(
@@ -620,6 +632,7 @@ export class MCPCommandController {
0,
),
);
block.addChild(new Text(theme.fg("muted", MCP_MANUAL_LOGIN_TIP), 1, 0));
block.addChild(new Spacer(1));
block.addChild(new Text(theme.fg("accent", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), 1, 0));
// Try to open browser automatically
@@ -644,11 +657,26 @@ export class MCPCommandController {
onProgress: (message: string) => {
this.ctx.present([new Spacer(1), new Text(theme.fg("muted", message), 1, 0)]);
},
onManualCodeInput: () => {
const pendingInput = manualInput.tryWaitForInput(MCP_MANUAL_INPUT_PROVIDER_ID);
if (!pendingInput) {
throw new Error(
`OAuth login already in progress for ${manualInput.pendingProviderId}. Complete or cancel it before starting MCP OAuth.`,
);
}
return pendingInput;
},
signal: oauthTimeout.signal,
},
);
// Execute OAuth flow with 5 minute timeout
const credentials = await withTimeout(flow.login(), 5 * 60 * 1000, "OAuth flow timed out after 5 minutes");
const credentials = await withTimeout(
flow.login(),
5 * 60 * 1000,
"OAuth flow timed out after 5 minutes",
() => oauthTimeout.abort("MCP OAuth flow timed out"),
);
this.ctx.present([
new Spacer(1),
@@ -687,6 +715,8 @@ export class MCPCommandController {
} else {
throw new Error(`OAuth authentication failed: ${errorMsg}`);
}
} finally {
manualInput.clear("Manual MCP OAuth input cleared");
}
}
@@ -17,6 +17,11 @@ export class OAuthManualInputManager {
return promise;
}
tryWaitForInput(providerId: string): Promise<string> | undefined {
if (this.#pending) return undefined;
return this.waitForInput(providerId);
}
submit(input: string): boolean {
if (!this.#pending) return false;
const { resolve } = this.#pending;