Skip to content

fix(mcp-oauth): graceful fallback when OAuth callback port is already bound - #55741

Closed
oppih wants to merge 1 commit into
NousResearch:mainfrom
oppih:fix/oauth-callback-double-bind
Closed

fix(mcp-oauth): graceful fallback when OAuth callback port is already bound#55741
oppih wants to merge 1 commit into
NousResearch:mainfrom
oppih:fix/oauth-callback-double-bind

Conversation

@oppih

@oppih oppih commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a crash in hermes mcp login where _wait_for_callback() tries to bind a second HTTPServer to the same port already occupied by the MCP SDK's build_oauth_auth() callback server, causing OSError: Address already in use that aborts the entire OAuth flow.

Instead of raising OAuthNonInteractiveError, the fix gracefully falls through to polling the SDK's existing server.

Related Issue

N/A (no existing issue found for this specific double-bind bug)

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • tools/mcp_oauth.py: On OSError (port already in use by SDK server), pass instead of raising OAuthNonInteractiveError, allowing the polling loop to detect the callback result from the SDK's server. Guard server.server_close() with if server is not None to handle the unbound variable in the fallback path.
  • hermes_cli/mcp_config.py: Bump _probe_single_server() connect_timeout from default 30s to 600s, giving users enough time to complete interactive browser-based OAuth authorization.

How to Test

  1. Configure an MCP server requiring OAuth (e.g. Google Drive MCP)
  2. Run hermes mcp login <server-name>
  3. Without the fix: crashes with OSError: Address already in useOAuthNonInteractiveError
  4. With the fix: browser opens, user authorizes, callback captured by SDK server, polling loop detects result, login succeeds

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes
  • I've tested on my platform: Ubuntu 24.04

Documentation & Housekeeping

  • I've updated relevant documentation — N/A
  • I've updated cli-config.yaml.example — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md — N/A
  • I've considered cross-platform impact — N/A
  • I've updated tool descriptions/schemas — N/A

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists tool/mcp MCP client and OAuth comp/cli CLI entry point, hermes_cli/, setup wizard area/auth Authentication, OAuth, credential pools labels Jun 30, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Same MCP-OAuth callback-port cluster as #34260 (module-level _oauth_port global), #44590 (ephemeral HTTPServer TIME_WAIT), and #54325 (bind-first OAuthCallbackServer) — but a distinct mechanism: this PR gracefully falls through to polling the SDK's existing callback server on a double-bind OSError rather than eliminating the global or scoping ports per provider. Related, not a duplicate; a maintainer should reconcile these competing approaches to the mcp login port collision.

… bound

Fixes a crash in `hermes mcp login` where `_wait_for_callback()` tries to
bind a second HTTPServer to the same port already occupied by the MCP SDK's
callback server, causing OSError: Address already in use that aborts the
entire OAuth flow.

Instead of raising OAuthNonInteractiveError, the fix gracefully falls
through to polling the SDK's existing server. Also bumps connect_timeout
from 30s to 600s for interactive OAuth flows.

Fixes: NousResearch#55741
@oppih
oppih force-pushed the fix/oauth-callback-double-bind branch from 278d41f to 0d645a2 Compare July 13, 2026 16:48
@oppih

oppih commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Relationship to #58154 (merged)

These two PRs are complementary, not duplicate — they address the same port-collision symptom at different layers:

#55741 (this PR) #58154 (merged)
Strategy Tolerant fallback (port in use → poll SDK's server) Fail-fast (non-interactive → reject before bind)
Trigger point After bind() fails with OSError Before any bind() attempt
Applies to Interactive hermes mcp login (user actively completing OAuth) Non-interactive/background discovery (systemd, cron, startup)
Port collision Resolved by falling through to polling Prevented entirely by never binding in non-interactive path

#58154 handles the background/non-interactive case — when a cached-but-unusable token would cause the SDK to enter the auth-code flow during gateway startup. It fails fast before printing a URL or binding a listener.

This PR handles the interactive case — when a user runs hermes mcp login manually and the SDK has already bound its own callback server on the same port. Instead of crashing with OSError, we fall through to polling the SDK's existing server.

Both guards can coexist: #58154 prevents non-interactive hangs; this PR prevents interactive crashes when the SDK pre-binds.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for isolating the interactive callback-port path. The premise is still present on current main, but the proposed fallback needs a shared callback-state mechanism before it can work.

Problems

  • tools/mcp_oauth.py:664 suppresses the bind failure and polls the new local result from line 651. _make_callback_handler() writes only the result dictionary closed over by its own handler (tools/mcp_oauth.py:497-516); a callback handled by another listener cannot update this dict. The fallback will therefore wait until the existing timeout path (tools/mcp_oauth.py:701-704).
  • hermes_cli/mcp_config.py:515 changes mcp add, not mcp login. Current main already gives the actual login flow a 315-second floor at hermes_cli/mcp_config.py:794-801 (f26ae4f).
  • No bound-port regression test accompanies the new behavior.

Suggested changes

  • Establish a single callback owner or an explicit result hand-off that this coroutine can observe; do not poll an unconnected result dict.
  • Remove the unrelated add-time 600-second override and add a collision regression test.

Automated hermes-sweeper review.

Comment thread tools/mcp_oauth.py
server_thread.start()
# Port already in use — the server from the MCP SDK is running.
# Fall back to polling: the SDK's server will handle the callback.
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result was freshly created at line 651 and is only written by the handler returned from _make_callback_handler(). Since this fallback does not start that handler, a callback received by a different listener cannot update result; polling will run to the 300-second timeout. Please use a shared callback-result hand-off or retain a prompt bind failure.

Comment thread hermes_cli/mcp_config.py

try:
tools = _probe_single_server(name, server_config)
tools = _probe_single_server(name, server_config, connect_timeout=600)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cmd_mcp_add, not the hermes mcp login path described by the PR. Current main already grants interactive login at least 315 seconds in _reauth_oauth_server (hermes_cli/mcp_config.py:794-801); extending every add-time probe to 600 seconds changes unrelated discovery failures.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Closing after the OAuth callback rework landed (#65622, #65664). The premise here doesn't hold against how the flow actually works: build_oauth_auth never starts a callback server, so on OSError there is no SDK server to fall back to polling — the pass would leave the flow waiting on a result dict nothing fills, converting a clear failure into a silent 300s hang before a misleading timeout. The stale in-code comment claiming otherwise was the trap (it fooled the original code too, and was fixed by @doxe0x in #50786#65664). On current main the double-bind itself is largely gone: ephemeral ports keep their reserved socket until the callback server adopts it, and a genuine conflict now raises a clear 'port already in use' error immediately. Appreciate the report and the repro work — the symptom was real, the mechanism just lived elsewhere.

@teknium1 teknium1 closed this Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data 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.

3 participants