Skip to content

fix(mcp): use per-provider closures and allow_reuse_address for OAuth - #44872

Closed
Code-suphub wants to merge 2 commits into
NousResearch:mainfrom
Code-suphub:fix/44588-44590-oauth-state-pollution
Closed

fix(mcp): use per-provider closures and allow_reuse_address for OAuth#44872
Code-suphub wants to merge 2 commits into
NousResearch:mainfrom
Code-suphub:fix/44588-44590-oauth-state-pollution

Conversation

@Code-suphub

@Code-suphub Code-suphub commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #44588
Fixes #44590

Two related OAuth reliability fixes:

1. Cross-server state pollution (#44588)

_redirect_handler was a module-level function reading the global _oauth_port. When multiple MCP servers run OAuth concurrently, the later call overwrites the port, causing the wrong authorization_url to be returned.

Fix: Replace _redirect_handler with _make_redirect_handler(port) — a closure factory that closes over the per-provider resolved port. Updated both mcp_oauth.py and mcp_oauth_manager.py call sites.

2. OSError: Address already in use on repeated OAuth flows (#44590)

_wait_for_callback() creates a new HTTPServer per flow without allow_reuse_address. After the first flow, the socket stays in TIME_WAIT and the next flow's bind() fails.

Fix: Set server.allow_reuse_address = True on the ephemeral callback HTTPServer.

Changes

File Change
tools/mcp_oauth.py Replace _redirect_handler with _make_redirect_handler(port) closure; add allow_reuse_address = True
tools/mcp_oauth_manager.py Import and use _make_redirect_handler instead of _redirect_handler
tests/tools/test_mcp_oauth.py Update tests to use the new factory API

Testing

python -m pytest tests/tools/test_mcp_oauth.py -v -k "redirect"

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✅ Tests (adding or improving test coverage)

Checklist

Code

Documentation

  • I have updated relevant documentation or marked as N/A
  • I have updated cli-config.yaml.example if I added/changed config keys or marked as N/A

…NousResearch#44588, NousResearch#44590)

Two related OAuth fixes:

1. Replace module-level _redirect_handler with _make_redirect_handler()
   closure factory that closes over the resolved port. This prevents
   cross-server state pollution when multiple MCP servers run OAuth
   concurrently (NousResearch#44588).

2. Set server.allow_reuse_address = True on the ephemeral callback
   HTTPServer so the socket doesn't stay in TIME_WAIT after the flow
   completes. This prevents 'Address already in use' errors on the
   next OAuth flow for the same port (NousResearch#44590).

Fixes NousResearch#44588
Fixes NousResearch#44590
@Code-suphub
Code-suphub force-pushed the fix/44588-44590-oauth-state-pollution branch from 1a57834 to b1a12d8 Compare June 12, 2026 11:42
@liuhao1024

Copy link
Copy Markdown
Contributor

Dead code after return in _make_redirect_handler

The closure refactor moves the browser-opening logic into the inner _redirect_handler, but the old standalone browser block was left in place after return _redirect_handler — making it unreachable dead code:

def _make_redirect_handler(port: int):
    async def _redirect_handler(authorization_url: str) -> None:
        # ... SSH hint + browser logic now lives here ...
        if _can_open_browser():
            try:
                opened = webbrowser.open(authorization_url)
                ...
            except Exception:
                ...
        else:
            print("(Headless environment detected ...")

    return _redirect_handler

    # DEAD CODE — unreachable after the return above
    if _can_open_browser():
        try:
            opened = webbrowser.open(authorization_url)
            ...

This does not affect correctness (the closure handles browser opening), but it is ~10 lines of unreachable code that should be removed.

Everything else looks clean — the closure-based port isolation, allow_reuse_address = True, and the mcp_oauth_manager.py integration are all correct.

@Code-suphub

Copy link
Copy Markdown
Contributor Author

Good catch! The dead code has been removed in the latest push. The return _redirect_handler is now the last statement in the factory function — no unreachable browser block remaining.

Thanks for the thorough review!

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists tool/mcp MCP client and OAuth duplicate This issue or pull request already exists labels Jun 12, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #5345 (earliest open) — same per-provider closure approach for the MCP OAuth _oauth_port cross-server pollution (#44588). #44607 / #44685 take the same approach. The allow_reuse_address half (#44590) is also covered by #44611.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Clean fix. Converts _redirect_handler to _make_redirect_handler closure to avoid module-level _oauth_port state that caused cross-server pollution when multiple MCP servers run OAuth concurrently. Fixes #44588. Test updated accordingly. No issues found.

@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 tackling two real MCP OAuth reliability failures. The current main branch still has shared callback-port state, but this patch needs adaptation before it can safely fix it.

Problems

  • tools/mcp_oauth.py:496 sets allow_reuse_address after HTTPServer(...) has already bound. Python's TCPServer.__init__ calls server_bind() during construction, and server_bind() checks the flag before binding, so this does not enable reuse for the attempted bind.
  • The new redirect closure does not isolate callbacks: the PR still passes _wait_for_callback at tools/mcp_oauth.py:791 and tools/mcp_oauth_manager.py:447. That handler reads module-global _oauth_port; current main still assigns that global in tools/mcp_oauth.py:828-832.
  • The test changes only exercise SSH redirect text. They do not cover manager-built providers with distinct ports or a real fixed-port rebind.

Suggested changes

  • Use an HTTPServer subclass with allow_reuse_address = True set before construction.
  • Pass per-provider callback and redirect closures through MCPOAuthManager._build_provider.
  • Add manager-path concurrency and real socket-rebind regression tests.

This is an automated hermes-sweeper review.

Comment thread tools/mcp_oauth.py
# (fixes #44590).
try:
server = HTTPServer(("127.0.0.1", _oauth_port), handler_cls)
server.allow_reuse_address = True

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.

HTTPServer.__init__ already calls server_bind() before this assignment, so SO_REUSEADDR is not enabled for this bind. Use a dedicated HTTPServer subclass with allow_reuse_address = True as a class attribute.

Comment thread tools/mcp_oauth.py
client_metadata=client_metadata,
storage=storage,
redirect_handler=_redirect_handler,
redirect_handler=redirect_handler,

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 closure isolates only the redirect display path; the provider still receives global-dependent _wait_for_callback below. A concurrent provider can overwrite _oauth_port before this flow binds its callback listener.

@alt-glitch alt-glitch added comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/auth Authentication, OAuth, credential pools labels Jul 14, 2026
@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 14, 2026
@alt-glitch alt-glitch removed 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 labels Jul 14, 2026
teknium1 added a commit that referenced this pull request Jul 16, 2026
_find_free_port() closed its probe socket before HTTPServer re-bound
the port minutes later, leaving a window where another process could
steal it (#22161 by @amathxbt). _reserve_callback_port() now keeps the
selected socket bound (bounded FIFO pool) until _wait_for_callback
adopts it via bind_and_activate=False. Also sets allow_reuse_address
BEFORE binding — the cherry-picked #44872 set it after the constructor
had already bound, where it is a no-op.

Also updates the three #57836 non-interactive-guard tests to the
closure-factory API from #44872.
@teknium1

Copy link
Copy Markdown
Contributor

Merged on main via PR #65622 with your two commits cherry-picked and your authorship preserved (13e19a9, f4c7caa) — thanks @Code-suphub! One correctness note: allow_reuse_address had to be set BEFORE binding (the constructor had already bound, so setting it after was a no-op) — fixed during salvage via bind_and_activate=False. The TOCTOU half from #22161 was folded in on top. Closing since the branch itself was too stale to merge directly.

Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
_find_free_port() closed its probe socket before HTTPServer re-bound
the port minutes later, leaving a window where another process could
steal it (NousResearch#22161 by @amathxbt). _reserve_callback_port() now keeps the
selected socket bound (bounded FIFO pool) until _wait_for_callback
adopts it via bind_and_activate=False. Also sets allow_reuse_address
BEFORE binding — the cherry-picked NousResearch#44872 set it after the constructor
had already bound, where it is a no-op.

Also updates the three NousResearch#57836 non-interactive-guard tests to the
closure-factory API from NousResearch#44872.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
_find_free_port() closed its probe socket before HTTPServer re-bound
the port minutes later, leaving a window where another process could
steal it (NousResearch#22161 by @amathxbt). _reserve_callback_port() now keeps the
selected socket bound (bounded FIFO pool) until _wait_for_callback
adopts it via bind_and_activate=False. Also sets allow_reuse_address
BEFORE binding — the cherry-picked NousResearch#44872 set it after the constructor
had already bound, where it is a no-op.

Also updates the three NousResearch#57836 non-interactive-guard tests to the
closure-factory API from NousResearch#44872.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 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/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform tool/mcp MCP client and OAuth type/bug Something isn't working

Projects

None yet

5 participants