fix(core): handle ExceptionGroup-wrapped ToolError in MCP clients (#9329)

Extends the ExceptionGroup unwrapping fix from mcp_tool_executor to the
base MCP client implementations (AsyncBaseMCPClient, AsyncFastMCPSSEClient,
AsyncFastMCPStreamableHTTPClient). When ToolError exceptions are wrapped
in ExceptionGroup by Python's async TaskGroup, the exception handler now
unwraps single-exception groups before checking class names.

This prevents wrapped ToolError exceptions from being logged to Datadog
as unexpected errors instead of being handled as expected validation failures.

Related to commit 1cbf1b231 which fixed the same issue in mcp_tool_executor.

🐾 Generated with [Letta Code](https://letta.com)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Kian Jones
2026-02-05 21:44:51 -08:00
committed by Caren Thomas
parent e7039470e9
commit 98fa16899b
2 changed files with 30 additions and 6 deletions

View File

@@ -85,8 +85,16 @@ class AsyncBaseMCPClient:
# McpError is raised for other MCP-related errors
# Both are expected user-facing issues from external MCP servers
# Log at debug level to avoid triggering production alerts for expected failures
if e.__class__.__name__ in ("McpError", "ToolError"):
logger.debug(f"MCP tool '{tool_name}' execution failed: {str(e)}")
# Handle ExceptionGroup wrapping (Python 3.11+ async TaskGroup can wrap exceptions)
exception_to_check = e
if hasattr(e, "exceptions") and e.exceptions:
# If it's an ExceptionGroup with a single wrapped exception, unwrap it
if len(e.exceptions) == 1:
exception_to_check = e.exceptions[0]
if exception_to_check.__class__.__name__ in ("McpError", "ToolError"):
logger.debug(f"MCP tool '{tool_name}' execution failed: {str(exception_to_check)}")
# Return error message with failure status instead of raising to avoid Datadog alerts
return str(e), False
# Re-raise unexpected errors

View File

@@ -143,8 +143,16 @@ class AsyncFastMCPSSEClient:
# McpError is raised for other MCP-related errors
# Both are expected user-facing issues from external MCP servers
# Log at debug level to avoid triggering production alerts for expected failures
if e.__class__.__name__ in ("McpError", "ToolError"):
logger.debug(f"MCP tool '{tool_name}' execution failed: {str(e)}")
# Handle ExceptionGroup wrapping (Python 3.11+ async TaskGroup can wrap exceptions)
exception_to_check = e
if hasattr(e, "exceptions") and e.exceptions:
# If it's an ExceptionGroup with a single wrapped exception, unwrap it
if len(e.exceptions) == 1:
exception_to_check = e.exceptions[0]
if exception_to_check.__class__.__name__ in ("McpError", "ToolError"):
logger.debug(f"MCP tool '{tool_name}' execution failed: {str(exception_to_check)}")
raise
# Parse content from result
@@ -300,8 +308,16 @@ class AsyncFastMCPStreamableHTTPClient:
# McpError is raised for other MCP-related errors
# Both are expected user-facing issues from external MCP servers
# Log at debug level to avoid triggering production alerts for expected failures
if e.__class__.__name__ in ("McpError", "ToolError"):
logger.debug(f"MCP tool '{tool_name}' execution failed: {str(e)}")
# Handle ExceptionGroup wrapping (Python 3.11+ async TaskGroup can wrap exceptions)
exception_to_check = e
if hasattr(e, "exceptions") and e.exceptions:
# If it's an ExceptionGroup with a single wrapped exception, unwrap it
if len(e.exceptions) == 1:
exception_to_check = e.exceptions[0]
if exception_to_check.__class__.__name__ in ("McpError", "ToolError"):
logger.debug(f"MCP tool '{tool_name}' execution failed: {str(exception_to_check)}")
raise
# Parse content from result