fix(mcp): don't trip the circuit breaker on application-level tool errors - #68669
fix(mcp): don't trip the circuit breaker on application-level tool errors#68669lizhenyi wants to merge 2 commits into
Conversation
…rors The per-server circuit breaker in _make_tool_handler bumped the consecutive-failure count whenever a tool result's JSON contained an "error" key. But reaching that point means _call_once() returned rather than raising — the transport delivered a response, so the server IS reachable. The only "error" payloads that get there come from the MCP isError path (application-level tool errors, e.g. "function not found"). Counting those as reachability failures let 3 bad symbol lookups trip a false "server unreachable" 60s cooldown while the server was perfectly healthy (observed with a codebase-memory MCP: 3 trace_path calls with mistyped qualified names -> 3x "function not found" -> breaker opened -> agent reported the service as down). A delivered response — success or application error — now resets the breaker. Genuine transport failures still bump it via the exception handler and the not-connected/dead-session paths above, so real outages are unaffected.
Locks in that application-level tool errors (isError responses, e.g. "function not found") do not open the per-server circuit breaker, while genuine transport failures still do. Mocks _run_on_mcp_loop to exercise the bump-vs-reset decision in _make_tool_handler without a live session.
Related: #32728 and closed #47955. All prevent healthy MCP application errors from opening the circuit breaker, but this patch resets on every delivered response while the others classify specific error payloads; maintainers should choose the intended contract. |
|
Thanks for the triage and the pointers to #32728 / #47955. The difference really comes down to what signal the breaker keys on. This is a The payload-classification approach in #32728 works too, but its fail-closed The honest tradeoff, and why it's a contract decision: with "reset on any Happy to go whichever way you prefer: keep this as the simpler reachability-only |
|
Thanks for the focused regression and direct-path fix. Current Problems
Suggested changes
Automated hermes-sweeper review. |
Current-head correction: the latest review concern remains open. The direct isError path is fixed, but delivered application-error envelopes can still bump the breaker after auth recovery and session-expiry retries. This remains a contract choice alongside #32728, #61555, and #74045; add focused recovery-path coverage before merge. |
SummaryTwenty PRs address or reference four related MCP reliability and hardening issues. The central #11113 diffs range from heuristic or direct-path-only error classification to the complete direct-plus-auth/session-recovery rule in #74045, while the remaining diffs cover idle keepalive, dead-transport recovery, breaker messaging, locking, reconnect signaling, and configuration. Related pull requests
Duplicates#67340 is a closed duplicate of #61555; #40951, #61555, #68669, and #74606 overlap on the direct completed-RPC reset, while #11128 and #32728 are weaker classifier-based alternatives. #74042 was replaced by #74045; #74718 was replaced by #74795; #74795 and #75511 duplicate #74045's direct-plus-auth/session-recovery production behavior. Suggested consolidationClose #68669 as duplicate of #74045 despite #68669's MAINTAINER-BOT keep_open verdict: the visible diffs show #74045 includes the same direct reset plus the auth/session-recovery fixes and handler-level coverage that #68669 still lacks. Keep #74045 open as the complete consolidation branch; keep Verify-designated #61555 open only with the concrete salvage path of adding both recovery-helper fixes and tests, close #74606 and #74795 as duplicates of #74045 in line with their reviews, and close #75511 as the larger duplicate despite its addressed keep_open objections. Complex graphflowchart TD
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I11113(["issue #11113 (open)"])
subgraph Dup40951 ["PRs duplicating each other"]
P40951["PR #40951 (closed)"]
P61555["PR #61555 (open)"]
P67340["PR #67340 (closed)"]
P68669["PR #68669 (open)"]
P74042["PR #74042 (closed)"]
P74045["PR #74045 (open)"]
P74606["PR #74606 (open)"]
P74718["PR #74718 (closed)"]
P74795["PR #74795 (open)"]
P75511["PR #75511 (open)"]
end
P68669 -.->|partial| I11113
class I11113 open
class P40951 closed
class P61555 open
class P67340 closed
class P68669 open
class P74042 closed
class P74045 open
class P74606 open
class P74718 closed
class P74795 open
class P75511 open
class P61555 best
class P74045 best
class P68669 target
click I11113 "https://github.com/NousResearch/hermes-agent/issues/11113"
click P40951 "https://github.com/NousResearch/hermes-agent/pull/40951"
click P61555 "https://github.com/NousResearch/hermes-agent/pull/61555"
click P67340 "https://github.com/NousResearch/hermes-agent/pull/67340"
click P68669 "https://github.com/NousResearch/hermes-agent/pull/68669"
click P74042 "https://github.com/NousResearch/hermes-agent/pull/74042"
click P74045 "https://github.com/NousResearch/hermes-agent/pull/74045"
click P74606 "https://github.com/NousResearch/hermes-agent/pull/74606"
click P74718 "https://github.com/NousResearch/hermes-agent/pull/74718"
click P74795 "https://github.com/NousResearch/hermes-agent/pull/74795"
click P75511 "https://github.com/NousResearch/hermes-agent/pull/75511"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 20 pull requests and 4 issues in this complex. Each diff was read against this issue; Assessment working set: 156 kB of PR diffs, 61 kB of issue/PR text, 48 kB of discussion (62 comments), 33 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
…rors
The per-server circuit breaker in _make_tool_handler bumped the consecutive-failure count whenever a tool result's JSON contained an "error" key. But reaching that point means _call_once() returned rather than raising — the transport delivered a response, so the server IS reachable. The only "error" payloads that get there come from the MCP isError path (application-level tool errors, e.g. "function not found").
Counting those as reachability failures let 3 bad symbol lookups trip a false "server unreachable" 60s cooldown while the server was perfectly healthy (observed with a codebase-memory MCP: 3 trace_path calls with mistyped qualified names -> 3x "function not found" -> breaker opened -> agent reported the service as down).
A delivered response — success or application error — now resets the breaker. Genuine transport failures still bump it via the exception handler and the not-connected/dead-session paths above, so real outages are unaffected.
What does this PR do?
Stops the per-server MCP circuit breaker from counting application-level tool errors (e.g. "function not found") as reachability failures, which could falsely trip a "server unreachable" cooldown on a healthy server.
Related Issue
Fixes #
Type of Change
Changes Made
tools/mcp_tool.py: the MCP circuit breaker no longer counts application-level tool errors (isError responses) as reachability failures — only genuine transport failures (exceptions / not-connected paths) trip it.How to Test
Checklist
Code
Documentation & Housekeeping
[ x] I've updated relevant documentation (README,
docs/, docstrings) — or N/A[ x] I've updated
cli-config.yaml.exampleif I added/changed config keys — or N/A[ x] I've updated
CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A[x ] I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
[ x] I've updated tool descriptions/schemas if I changed tool behavior — or N/A
This skill is broadly useful to most users (if bundled) — see Contributing Guide
SKILL.md follows the standard format (frontmatter, trigger conditions, steps, pitfalls)
No external dependencies that aren't already available (prefer stdlib, curl, existing Hermes tools)
I've tested the skill end-to-end:
hermes --toolsets skills -q "Use the X skill to do Y"Screenshots / Logs
Verified the breaker decision directly against the module helpers (threshold = 3):
[1] 3x application-level "function not found" -> error count = 0, breaker stays closed ✅
[2] 3x transport failure (exception) -> error count = 3, breaker opens ✅