Conversation
…ability The circuit breaker (NousResearch#10776, for NousResearch#10447) counts application-level tool errors (result.isError payloads, e.g. a "not found" lookup) toward the per-server consecutive-failure count, alongside transport exceptions. Three bad-argument calls in a row open the breaker: every tool on that server fast-fails as "MCP server is unreachable" for the 60s cooldown, even though the server answered each call in under a second. Observed in production: an agent probed with three stale IDs, then all its subsequent, well-formed calls to other tools on the same server were short-circuited with a message telling it the server was down. Count only failures that actually evidence unreachability: a completed RPC round-trip resets the breaker regardless of payload, and only the transport-exception paths bump the count. An isError result is not uninformative like a transport failure — the model sees the actual error text and can correct its arguments, so the burn-loop the breaker targets doesn't apply, while the false "unreachable" verdict actively misdirects the model and the user toward checking server health. The auth-recovery and session-expired retry helpers had the same payload-shape check — a retry that completed but carried an app-level error fell through to needs_reauth / the generic error path — and now return the completed result as-is. Two regression tests: consecutive isError results never trip the breaker, and an isError result closes a partially-tripped one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xa5D4FJwo8A3R8skLiK2aq
|
Added some more tests to verify all branches get the correct behavior Also added a backward compatible section on the description :) |
|
Thanks for the focused MCP classification fix. The premise remains present on current The proposed reset after every non-raising This is an automated hermes-sweeper review. |
…-level-errors # Conflicts: # tests/tools/test_mcp_tool_session_expired.py
|
solved conflicts :) |
|
Closing with a maintainer ruling rather than a quality verdict. Tool |
What
The MCP circuit breaker counts application-level tool errors toward the per-server "unreachable" verdict.
_make_tool_handlerclassifies each completed call by sniffing the result JSON:An MCP
result.isErrorpayload (e.g. a "not found" lookup with a bad ID) produces exactly that shape, so three bad-argument calls in a row open the breaker — and every tool on that server then fast-fails withMCP server '<name>' is unreachable after 3 consecutive failuresfor the full 60s cooldown.Observed in production: an agent probed a server with three stale IDs, got three sub-second "not found" responses (the server was healthy and answering), and the harness then told the model — and the user — that the server was down, short-circuiting its subsequent well-formed calls to other tools on the same server.
This PR counts only failures that actually evidence unreachability: a completed RPC round-trip resets the breaker regardless of payload; only the transport-exception paths (and the not-connected path) bump the count. The auth-recovery and session-expired retry helpers had the same payload-shape check — a retry that completed but carried an app-level error fell through to
needs_reauth/ the generic error path — and now return the completed result as-is.Why (and why this isn't the breaker's intended design doing its job)
I want to be upfront: the original commit (
3ff18ffe1, #10776) deliberately listed "MCP-level errors" among the counted failures, so this changes intended behavior rather than fixing an accidental one. The case for the change:isErrorresult carries the server's actual error text every time, so the model has what it needs to correct course; and unlike a crashed server, each such call is cheap and answered. Transport failures — the Gateway hangs when Google Workspace MCP skill returns error — infinite retry loop #10447 scenario (crashed, disconnected, hung server) — still trip the breaker exactly as before, including auth failures via theneeds_reauthpath.If a per-tool guard against a model hammering the same failing call is still wanted, that's a different mechanism (keyed on tool + arguments, with an honest message) — happy to discuss, but it shouldn't ride on the server-unreachability breaker.
How to test
Reproduce the false positive on current
main: register any HTTP MCP server, make 3 consecutive calls to a tool with arguments the server rejects (any "not found" style error), then call a different, valid tool on the same server — it returns the "unreachable" message without hitting the server. On this branch the fourth call goes through.Regression tests added:
test_app_level_tool_errors_do_not_trip_breaker— threshold+1 consecutiveisErrorresults: every call reaches the session, breaker stays closed.test_app_level_tool_error_closes_partially_tripped_breaker— anisErrorresult resets a below-threshold transport-failure count, like any successful response.test_call_tool_handler_returns_app_error_after_auth_recovery— an application error returned after OAuth recovery is preserved instead of being replaced withneeds_reauth.test_call_tool_handler_returns_app_error_after_session_reconnect— an application error returned by a fresh transport session is preserved instead of being replaced with the original session-expired failure.Ran per-file with the project venv:
test_mcp_circuit_breaker.py(9 — the four existing breaker tests, including half-open/cooldown/reconnect behavior, pass unchanged),test_mcp_tool.py(234),test_mcp_tool_401_handling.py(8),test_mcp_tool_session_expired.py(33) — all passing.Platforms tested
macOS (Apple Silicon, Python 3.13). The change is pure in-process counter bookkeeping in
tools/mcp_tool.py— no file I/O, process management, or terminal handling touched.Related
#10447 (the retry burn loop the breaker was built for), #10776 (the breaker), #13383 (session-expired retry path, one of the three sites touched).
Replaces #74042 (same change; reopened to follow the
fix/branch-naming convention and with the design rationale spelled out).Backward compatibility
Backward compatible. This changes only in-process circuit-breaker accounting. MCP wire payloads, tool-result JSON shapes, configuration, and persisted state are unchanged, so old and new MCP servers remain compatible with this client behavior.