From 8e6f6c13e500e8d6d397235e42522be6e917edd9 Mon Sep 17 00:00:00 2001 From: danzaio <213864024+danzaio@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:55:21 -0300 Subject: [PATCH] fix(mcp): cancel manual OAuth waits on timeout --- packages/coding-agent/CHANGELOG.md | 4 ++++ .../controllers/mcp-command-controller.ts | 24 +++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ee70e90fe..b0ab9c1b2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts index 53bcadc60..8f26e9740 100644 --- a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts @@ -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(promise: Promise, timeoutMs: number, message: string): Promise { +const MCP_MANUAL_INPUT_PROVIDER_ID = "mcp"; +const MCP_MANUAL_LOGIN_TIP = "Headless? Paste the redirect URL or code with /login ."; +function withTimeout(promise: Promise, timeoutMs: number, message: string, onTimeout?: () => void): Promise { const { promise: timeoutPromise, reject } = Promise.withResolvers(); - 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 ."), 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),