Skip to content

fix(mcp): suppress interactive OAuth stdin prompts during background discovery (#35927) - #53294

Merged
kshitijk4poor merged 2 commits into
mainfrom
salvage/35927-mcp-oauth-stdin
Jun 26, 2026
Merged

fix(mcp): suppress interactive OAuth stdin prompts during background discovery (#35927)#53294
kshitijk4poor merged 2 commits into
mainfrom
salvage/35927-mcp-oauth-stdin

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

The interactive hermes TUI no longer freezes on startup when a configured MCP server requires OAuth.

Root cause (#35927)

On startup, background MCP discovery connects to configured servers. When a server requires OAuth, the flow (tools/mcp_oauth._wait_for_callback) — on an interactive TTY — spawns a daemon thread doing a blocking sys.stdin.readline() (the "paste the redirect URL" fallback that races the HTTP callback listener). That thread competes with the TUI's own stdin reader for the same terminal, so the user's keystrokes get swallowed and the TUI appears frozen (up to the 300s OAuth timeout). Reported symptom: "MCP OAuth: authorization required / Open this URL … the tui is freezing, not respond to typing."

Fix

Add a thread-local suppress_interactive_oauth() context manager (tools/mcp_oauth.py). While active, _is_interactive() returns False, so the stdin paste-thread and its prompt are never created. Background discovery (hermes_cli/mcp_startup.py, tui_gateway/entry.py) now runs inside that context — an OAuth-requiring server soft-skips (raises OAuthNonInteractiveError, already handled as "skip this server") instead of stealing the TUI's stdin. A genuine hermes mcp login on the main thread is unaffected (thread-local, not global).

Validation

  • E2E (real code path): with suppression, _wait_for_callback does not print the paste prompt and spawns no stdin thread — it raises OAuthNonInteractiveError (soft skip), leaving the TUI's stdin untouched. Without suppression the prompt IS shown (the freeze). Demonstrated directly.
  • Mutation-verified: removing the suppress check in _is_interactive() fails the regression test.
  • 76 tests pass (test_mcp_startup.py + test_mcp_oauth.py), incl. new tests for the context manager and the suppressed-discovery path. ruff clean.

Salvage notes

Salvaged from #35945 by @zapabob (authorship preserved via cherry-pick; already in AUTHOR_MAP). Resolved a conflict against main's newer mcp_discovery_timeout / wait_for_mcp_discovery refactor, keeping both that and the OAuth-suppression helper.

Closes #35927.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: salvage/35927-mcp-oauth-stdin vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11498 on HEAD, 11499 on base (✅ -1)

🆕 New issues (2):

Rule Count
invalid-assignment 2
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`
hermes_cli/mcp_startup.py:79: [invalid-assignment] invalid-assignment: Object of type `<class 'nullcontext'>` is not assignable to `() -> _GeneratorContextManager[Unknown, None, None]`

✅ Fixed issues (2):

Rule Count
unresolved-attribute 2
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
run_agent.py:3002: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`

Unchanged: 6043 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

…discovery (#35927)

When an MCP server requires OAuth, the interactive `hermes` TUI froze on
startup: background MCP discovery hit the OAuth flow, which on an interactive
TTY spawns a daemon thread doing a blocking `sys.stdin.readline()` (the
"paste the redirect URL" fallback in mcp_oauth._wait_for_callback). That
thread competes with the TUI's own stdin reader for the same terminal, so
keystrokes get swallowed and the TUI appears frozen (up to the 300s OAuth
timeout). Reported symptom: "MCP OAuth: authorization required / Open this URL
... the tui is freezing, not respond to typing."

Add a thread-local `suppress_interactive_oauth()` context manager in
tools/mcp_oauth.py; `_is_interactive()` returns False while it's active, so the
stdin paste-thread and prompt are never created. Background discovery
(hermes_cli/mcp_startup.py, tui_gateway/entry.py) now runs discovery inside
that context, so OAuth-requiring servers soft-skip (raise
OAuthNonInteractiveError, already handled) instead of stealing the TUI's stdin.
A real `hermes mcp login` on the main thread is unaffected (thread-local).

Salvaged from #35945 by @zapabob (authorship preserved via cherry-pick;
resolved a conflict against main's new mcp_discovery_timeout / wait_for_mcp_
discovery refactor, keeping both). Verified E2E: with suppression the paste
prompt is NOT printed and no stdin thread spawns (raises OAuthNonInteractive
soft-skip); without it the prompt shows (the freeze). Mutation-verified
(removing the suppress check in _is_interactive fails the regression test).
76 tests pass, ruff clean.

Closes #35927.

SELF-REVIEW FIX: the original #35945 used threading.local(), which does NOT
propagate to the dedicated mcp-event-loop thread where OAuth actually runs
(discover_mcp_tools dispatches the connect via run_coroutine_threadsafe), so
the suppression was a NO-OP in production (the tests passed only by stubbing
out the cross-thread dispatch). Converted to a contextvars.ContextVar, which
asyncio copies onto the scheduled coroutine — empirically verified suppression
now holds on the mcp-event-loop thread through the real _run_on_mcp_loop path.
Added a cross-thread regression test (fails on threading.local, passes on the
ContextVar) so the no-op can't regress.
@kshitijk4poor
kshitijk4poor force-pushed the salvage/35927-mcp-oauth-stdin branch from 95441d8 to 280134b Compare June 26, 2026 22:16
@kshitijk4poor
kshitijk4poor enabled auto-merge (rebase) June 26, 2026 22:18
@alt-glitch alt-glitch added type/bug Something isn't working tool/mcp MCP client and OAuth comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists labels Jun 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Salvage of #35945 (@zapabob), cherry-picked onto current main with authorship preserved (Closes #35927). Related to the prior non-interactive-OAuth attempts #4521 / #4757. This is the active version of the fix, not a duplicate.

The backgrounding-contract test (test_prepare_agent_startup_backgrounds_
blocking_mcp_for_chat) failed intermittently on loaded CI shards: it stubs
tools.mcp_tool.discover_mcp_tools but NOT tools.mcp_oauth, so the background
discovery thread paid the real, cold ~0.75s 'import tools.mcp_oauth' (added by
this PR's _discover_mcp_tools_without_interactive_oauth) before calling the
stubbed discovery. On a slow/loaded runner that import plus thread scheduling
exceeded the 1.0s polling deadline, leaving calls['mcp'] == 0.

Fix: stub tools.mcp_oauth with a nullcontext suppress_interactive_oauth (the
same no-op production falls back to when mcp_oauth is unavailable), so the
test exercises the backgrounding contract without paying an unrelated cold
import in its timing window. Bumped the poll deadline 1.0s -> 3.0s as
belt-and-suspenders. Production behaviour is unchanged; the import cost was
always off the main thread.

Verified: 5/5 pass repeatedly via scripts/run_tests.sh (per-file isolation,
matching CI), ruff clean.
@kshitijk4poor
kshitijk4poor merged commit 7475d12 into main Jun 26, 2026
27 checks passed
@kshitijk4poor
kshitijk4poor deleted the salvage/35927-mcp-oauth-stdin branch June 26, 2026 23:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists tool/mcp MCP client and OAuth type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: failed mcp cause hermes tui start freeze

3 participants