diff --git a/letta/services/mcp/base_client.py b/letta/services/mcp/base_client.py index 6b4a0536..a5e76ce2 100644 --- a/letta/services/mcp/base_client.py +++ b/letta/services/mcp/base_client.py @@ -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 diff --git a/letta/services/mcp/fastmcp_client.py b/letta/services/mcp/fastmcp_client.py index e3c901de..cc20f0a1 100644 --- a/letta/services/mcp/fastmcp_client.py +++ b/letta/services/mcp/fastmcp_client.py @@ -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