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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user