fix(mcp): use per-provider closures and allow_reuse_address for OAuth - #44872
fix(mcp): use per-provider closures and allow_reuse_address for OAuth#44872Code-suphub wants to merge 2 commits into
Conversation
…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
1a57834 to
b1a12d8
Compare
|
Dead code after The closure refactor moves the browser-opening logic into the inner 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, |
|
Good catch! The dead code has been removed in the latest push. The Thanks for the thorough review! |
tonydwb
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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:496setsallow_reuse_addressafterHTTPServer(...)has already bound. Python'sTCPServer.__init__callsserver_bind()during construction, andserver_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_callbackattools/mcp_oauth.py:791andtools/mcp_oauth_manager.py:447. That handler reads module-global_oauth_port; current main still assigns that global intools/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
HTTPServersubclass withallow_reuse_address = Trueset 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.
| # (fixes #44590). | ||
| try: | ||
| server = HTTPServer(("127.0.0.1", _oauth_port), handler_cls) | ||
| server.allow_reuse_address = True |
There was a problem hiding this comment.
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.
| client_metadata=client_metadata, | ||
| storage=storage, | ||
| redirect_handler=_redirect_handler, | ||
| redirect_handler=redirect_handler, |
There was a problem hiding this comment.
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.
_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.
|
Merged on main via PR #65622 with your two commits cherry-picked and your authorship preserved (13e19a9, f4c7caa) — thanks @Code-suphub! One correctness note: |
_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.
_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.
Summary
Fixes #44588
Fixes #44590
Two related OAuth reliability fixes:
1. Cross-server state pollution (#44588)
_redirect_handlerwas a module-level function reading the global_oauth_port. When multiple MCP servers run OAuth concurrently, the later call overwrites the port, causing the wrongauthorization_urlto be returned.Fix: Replace
_redirect_handlerwith_make_redirect_handler(port)— a closure factory that closes over the per-provider resolved port. Updated bothmcp_oauth.pyandmcp_oauth_manager.pycall sites.2.
OSError: Address already in useon repeated OAuth flows (#44590)_wait_for_callback()creates a newHTTPServerper flow withoutallow_reuse_address. After the first flow, the socket stays inTIME_WAITand the next flow'sbind()fails.Fix: Set
server.allow_reuse_address = Trueon the ephemeral callback HTTPServer.Changes
tools/mcp_oauth.py_redirect_handlerwith_make_redirect_handler(port)closure; addallow_reuse_address = Truetools/mcp_oauth_manager.py_make_redirect_handlerinstead of_redirect_handlertests/tools/test_mcp_oauth.pyTesting
python -m pytest tests/tools/test_mcp_oauth.py -v -k "redirect"Type of Change
Checklist
Code
fix(scope): description)Documentation
cli-config.yaml.exampleif I added/changed config keys or marked as N/A