Repository navigation
fix(mcp): report non-timeout MCP errors with their own type instead of as a timeout - #9440
Sergey Blekher (blurman-ai) wants to merge 1 commit into
Conversation
…f as a timeout MCPToolWrapper._execute_tool turned any exception with a __cause__, or whose text contained "TaskGroup", into asyncio.TimeoutError. A 401/404 raised inside the client task group was therefore retried 3 times and shown to the model as "Connection timed out". Classify by exception type: flatten exception groups, treat only TimeoutError leaves as a timeout, and report any other leaf by type and message without retrying. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Sergey <blurman@mail.ru>
Strix Security Review✅ No open security findings. PR summary: Changes how the MCP tool wrapper classifies failures from a remote MCP server: instead of treating any exception group as a timeout, it flattens the group and sorts by type, retrying only genuine timeouts and otherwise reporting the real error type without retrying. Adds two focused tests for the timeout and non-timeout shapes. Review summaryI reviewed the rewritten MCP error classification in The change only narrows what used to be misreported: every error whose leaf is a timeout was already retried before, and a dedicated handler now catches non-timeout failures before the generic substring-based handlers, so no failure newly escapes, hangs, or gets retried as a timeout. Error text now reaches the agent on this path where it previously collapsed to a canned "Connection timed out" sentence, but that text is composed from the operator's own server configuration plus whatever the configured server already sends verbatim as tool output, and the pre-existing handler already returned exception text the same way, so no trust boundary is crossed. Hostile exception trees are not server-constructible: group nesting comes from the client's own task groups, and groups carrying cancellation-style leaves bypass these handlers exactly as they did before. No security issues found. Ruled out (4)
Review 1 on this PR · Reviewed by Strix |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 434831f. Configure here.
| if len(leaves) == 1 and leaves[0] is e: | ||
| raise | ||
| details = "; ".join(f"{type(leaf).__name__}: {leaf}" for leaf in leaves) | ||
| raise MCPExecutionError(details) from e |
There was a problem hiding this comment.
Mixed exception groups skip classifier
Medium Severity
The new group classifier runs only under except Exception, so a BaseExceptionGroup that mixes an HTTP refusal with CancelledError never reaches _leaf_exceptions. That is the shape the rest of the MCP stack already handles with (Exception, BaseExceptionGroup) and find_http_status. Those failures still hit the CancelledError branch and become a timeout, or they escape _run uncaught.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 434831f. Configure here.


Fixes #9438
What the user sees
When an MCP server rejects a request (for example HTTP 401 or 404), the error is raised inside the MCP client's task group.
MCPToolWrapper._execute_tooltreated any exception that had a__cause__, or whose text contained "TaskGroup" / "unhandled errors", asasyncio.TimeoutError. The wrapper then retried the call 3 times and told the model "Connection timed out after 60 seconds": the agent retried a request that can never succeed, and the real reason was lost.Fix
Classify by exception type, not by text.
_execute_toolflattens (nested) exception groups into their leaf exceptions:TimeoutError, it is a timeout and is retried as before;A plain exception that is not a group is re-raised unchanged and goes through the existing handling.
Test
lib/crewai/tests/mcp/test_tool_wrapper_errors.pystubs the MCP client and session socall_toolraises an exception group:call_toolis awaited once (before the fix: 3 attempts and "Connection timed out");TimeoutError: still reported as a timeout and retried 3 times.Both pass with the change; the first fails without it.
ruff checkis clean.Written with an AI coding assistant (Claude Code) and reviewed and run before submitting, hence the
llm-generatedlabel.Resubmission of #9368, which was automatically closed before the required bug report existed. #9438 now contains a reproduction against upstream main with a local HTTP endpoint returning 401. Reopening #9368 returns GitHub validation error 422. The fix is unchanged; its two focused MCP tests were rerun successfully on 2026-11-14.
Note
Low Risk
Narrows MCP error handling and retry behavior; adds tests with no auth or data-model changes.
Overview
Fixes misclassified MCP failures that were surfaced as connection timeouts (with 3 retries) when the MCP client raised nested
TaskGrouperrors for real HTTP/auth failures.MCPToolWrapper._execute_toolnow flattens exception groups via_leaf_exceptionsand branches on leaf types:TimeoutErrorleaves still become a timeout and follow the existing retry path; other grouped errors raise a newMCPExecutionErrorwith leaf type/message details._execute_single_attemptcatches that type, returns "MCP execution error: …", and does not retry. Plain non-group exceptions are re-raised unchanged.Adds
test_tool_wrapper_errors.pywith stubbed MCP client/session: HTTP 401 inside a group is reported once with the real error (not "timed out"); timeout leaves still retry 3 times.Reviewed by Cursor Bugbot for commit 434831f. Bugbot is set up for automated code reviews on this repo. Configure here.