fix(mcp): isolate a single failing stdio server from the bridge - #50589
trevorgordon981 wants to merge 1 commit into
Conversation
|
Duplicate of #50482 — both PRs fix #50394 (a single failing stdio MCP server churning the whole bridge) by adding a per-server connection-failure cooldown/circuit-breaker in |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating the cross-discovery failure mode. The premise still exists on current main: _servers[name] is assigned only after _connect_server() returns (tools/mcp_tool.py:4850-4858), but start() raises an initial-connect error (tools/mcp_tool.py:2879-2896); the registration filter then retries any enabled name absent from _servers (tools/mcp_tool.py:4900-4904).
Problems
- The added reset at
tools/mcp_tool.py:4652-4658is skipped in the exact only-failing-server case.shutdown_mcp_servers()returns when its_serverssnapshot is empty before its nested_shutdown()coroutine runs (tools/mcp_tool.py:5425-5431on current main). The connection cooldown therefore survives the reload path the PR says should retry immediately.
Suggested changes
- Clear the retry maps before the empty-server fast path and add a regression test for shutdown after a failed-only discovery.
- Reconcile this with current main's parked initial-connect task flow (
tools/mcp_tool.py:2772-2799) during salvage.
Automated hermes-sweeper review.
| _server_connect_retry_after.clear() | ||
| _server_connect_failures.clear() | ||
|
|
||
| with _lock: |
There was a problem hiding this comment.
This reset is unreachable when the failed server is the only configured server: shutdown_mcp_servers() returns through its empty-_servers fast path before _shutdown() runs. Clear these maps before that fast path and cover failed-only shutdown/reload with a regression test.
Builds on trevorgordon981's #50589 (cherry-picked as the previous commit). The #50394 cooldown reset only ran inside the async _shutdown coroutine, which is skipped on the empty-_servers fast path — the most common state when a server failed to connect (failed servers are never recorded in _servers). It was also skipped when the MCP loop wasn't running. Clear _server_connect_retry_after/_server_connect_failures on the fast path and in a final unconditional sweep so a full shutdown/restart always re-attempts every configured server immediately. Adds regression tests for both paths.
Builds on trevorgordon981's #50589 (cherry-picked as the previous commit). The #50394 cooldown reset only ran inside the async _shutdown coroutine, which is skipped on the empty-_servers fast path — the most common state when a server failed to connect (failed servers are never recorded in _servers). It was also skipped when the MCP loop wasn't running. Clear _server_connect_retry_after/_server_connect_failures on the fast path and in a final unconditional sweep so a full shutdown/restart always re-attempts every configured server immediately. Adds regression tests for both paths.
Builds on trevorgordon981's #50589 (cherry-picked as the previous commit). The #50394 cooldown reset only ran inside the async _shutdown coroutine, which is skipped on the empty-_servers fast path — the most common state when a server failed to connect (failed servers are never recorded in _servers). It was also skipped when the MCP loop wasn't running. Clear _server_connect_retry_after/_server_connect_failures on the fast path and in a final unconditional sweep so a full shutdown/restart always re-attempts every configured server immediately. Adds regression tests for both paths.
|
Merged via #68660. Your stdio failure cooldown was cherry-picked with authorship preserved; we added a follow-up so cooldown state also resets on the empty-_servers shutdown fast path. |
Builds on trevorgordon981's NousResearch#50589 (cherry-picked as the previous commit). The NousResearch#50394 cooldown reset only ran inside the async _shutdown coroutine, which is skipped on the empty-_servers fast path — the most common state when a server failed to connect (failed servers are never recorded in _servers). It was also skipped when the MCP loop wasn't running. Clear _server_connect_retry_after/_server_connect_failures on the fast path and in a final unconditional sweep so a full shutdown/restart always re-attempts every configured server immediately. Adds regression tests for both paths.
Builds on trevorgordon981's NousResearch#50589 (cherry-picked as the previous commit). The NousResearch#50394 cooldown reset only ran inside the async _shutdown coroutine, which is skipped on the empty-_servers fast path — the most common state when a server failed to connect (failed servers are never recorded in _servers). It was also skipped when the MCP loop wasn't running. Clear _server_connect_retry_after/_server_connect_failures on the fast path and in a final unconditional sweep so a full shutdown/restart always re-attempts every configured server immediately. Adds regression tests for both paths.
Builds on trevorgordon981's NousResearch#50589 (cherry-picked as the previous commit). The NousResearch#50394 cooldown reset only ran inside the async _shutdown coroutine, which is skipped on the empty-_servers fast path — the most common state when a server failed to connect (failed servers are never recorded in _servers). It was also skipped when the MCP loop wasn't running. Clear _server_connect_retry_after/_server_connect_failures on the fast path and in a final unconditional sweep so a full shutdown/restart always re-attempts every configured server immediately. Adds regression tests for both paths.
Closes #50394.
A failing stdio MCP server (bad PATH / exec-not-found / crash-on-start) was never recorded in
_servers—_connect_server→MCPServerTask.start()raises before the_servers[name] = serverline runs. So the new-servers filter never skipped it, and everydiscover_mcp_tools()call (one per agent worker session, every few seconds) re-spawned it from scratch — a restart storm of unreaped subprocesses + event-loop churn that intermittently knocked healthy co-located servers' tools into "Unknown tool". The existing circuit breaker only guarded runtime tool-call dispatch, not re-discovery.Fix: a per-server connection-retry cooldown with exponential backoff (30s → 600s).
register_mcp_serversskips a server whose cooldown is active (isolating the failure; healthy servers untouched); the failure path arms the backoff, success clears it, andshutdown_mcp_serversclears the maps so/reload-mcpand restarts re-attempt immediately.Tests: +6 (
test_mcp_bridge_single_failure.py) incl. the regression (failing server not re-spawned on the 2nd discovery pass, but retried after cooldown expiry); 228 pass across the MCP suite.