fix(mcp): cancel manual OAuth waits on timeout

This commit is contained in:
danzaio
2026-06-09 13:55:21 -03:00
committed by can1357
parent 3b47ee93e9
commit 8e6f6c13e5
2 changed files with 21 additions and 7 deletions
+4
View File
@@ -133,6 +133,10 @@
- Fixed Windows stdio MCP servers launched through PATH shims such as `codegraph.cmd` so bare commands like `codegraph` resolve via `PATHEXT` before spawn ([#2174](https://github.com/can1357/oh-my-pi/issues/2174)).
- Fixed compiled-binary extensions failing to load `@oh-my-pi/pi-*` packages when `bun --compile` quietly dropped one of the extra entrypoints (observed on macOS arm64 release builds): the legacy-pi compat shim's package-root override branch returned the bunfs path without checking the target was present, so the rewrite emitted a `file://` URL to a missing module and the #1216 fallback (scoped to the throwing `getResolvedSpecifier` path) never ran. Override targets are now validated against the on-disk filesystem at module init, missing entries are dropped, and resolution falls through to canonical lookup so Bun resolves the import from the extension's own `node_modules` ([#2168](https://github.com/can1357/oh-my-pi/issues/2168)).
### Fixed
- Fixed MCP OAuth flows accepting pasted redirect URLs or authorization codes through `/login` in headless environments ([#2122](https://github.com/can1357/oh-my-pi/issues/2122)).
## [15.10.8] - 2026-06-09
### Added
@@ -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));
}
@@ -592,6 +597,7 @@ export class MCPCommandController {
const resolvedClientSecret = clientSecret.trim() || undefined;
const manualInput = this.ctx.oauthManualInput;
const oauthTimeout = new AbortController();
try {
// Create OAuth flow
const flow = new MCPOAuthFlow(
@@ -621,9 +627,7 @@ export class MCPCommandController {
0,
),
);
block.addChild(
new Text(theme.fg("muted", "Headless? Paste the redirect URL or code with /login <value>."), 1, 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
@@ -648,12 +652,18 @@ export class MCPCommandController {
onProgress: (message: string) => {
this.ctx.present([new Spacer(1), new Text(theme.fg("muted", message), 1, 0)]);
},
onManualCodeInput: () => manualInput.waitForInput("mcp"),
onManualCodeInput: () => manualInput.waitForInput(MCP_MANUAL_INPUT_PROVIDER_ID),
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),