fix(mcp): recovered application errors keep the breaker strike

The pick returns the real result after a transport recovery instead of
dropping it, but it also skipped the breaker bookkeeping. Application
errors counting as strikes is the point of the breaker (3ff18ffe14,
#10447: a server answering errors made the model hammer it 8x in 10s).
Route the recovered result through _record_call_outcome so the caller
sees the tool's answer and the counter still moves the right way.
This commit is contained in:
teknium1
2026-09-12 07:20:50 -07:00
committed by Teknium
parent 5dca47a651
commit 02b398bda5
2 changed files with 7 additions and 5 deletions
@@ -204,6 +204,9 @@ def test_call_tool_handler_rebuilds_configured_server_transport(
assert call_count["n"] == 1
else:
assert parsed == {"error" if application_error else "result": "reconnected"}
# The recovered result is the tool's real answer either way; an application error is
# still one breaker strike (#10447), a success resets the counter.
assert mcp_tool._server_error_counts.get("resumed", 0) == (1 if application_error else 0)
assert call_count["n"] == 2
assert routes == [expected_route, expected_route]
assert configs == [transport_config, transport_config]
+4 -5
View File
@@ -132,16 +132,15 @@ def _lookup_reconnectable_server(server_name: str, require_loop: bool = False):
def _retry_once(server_name: str, retry_call, op_description: str, what: str):
"""Re-run ``retry_call`` after a recovery step. Returns the result (closing the breaker)
when the RPC completed; None when the retry raised (caller falls through)."""
"""Re-run ``retry_call`` after a recovery step. Returns the result when the RPC completed
(an application error is still the tool's real answer, and still a breaker strike per #10447);
None when the retry raised (caller falls through)."""
try:
result = retry_call()
except Exception as retry_exc:
logger.warning("MCP %s/%s retry after %s failed: %s", server_name, op_description, what, retry_exc)
return None
# An application error still proves the recovered transport completed a round-trip.
_core._reset_server_error(server_name)
return result
return _record_call_outcome(server_name, result)
def _handle_auth_error_and_retry(server_name: str, exc: BaseException, retry_call, op_description: str):