fix(core): handle ExceptionGroup-wrapped ToolError and McpError in MCP tool execution (#9328)

* fix(core): handle ExceptionGroup-wrapped ToolError and McpError in MCP tool execution

Fixes 3 related Datadog bugs (all fastmcp.exceptions.ToolError):
- 75d43daa-ff04-11f0-81b2-da7ad0900000
- 7af6373e-0080-11f1-9855-da7ad0900000
- a322edc8-fffa-11f0-b26c-da7ad0900000

These errors were caused by ToolError and McpError exceptions bubbling up
unhandled from the MCP REST endpoint. This fix combines the approaches from
PRs #9320 and #9321:

1. Handle ExceptionGroup wrapping (Python 3.11+ async TaskGroup)
2. Check for ToolError by class name to handle module variations
3. Convert ToolError to LettaInvalidArgumentError for proper client response
4. Catch McpError and return HTTP 500 with proper error message

Issue-IDs: 75d43daa-ff04-11f0-81b2-da7ad0900000, 7af6373e-0080-11f1-9855-da7ad0900000, a322edc8-fffa-11f0-b26c-da7ad0900000

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

Co-Authored-By: Letta <noreply@letta.com>

* fix: return 422 instead of 500 for McpError (user config issue)

* fix: use LettaMCPConnectionError instead of HTTPException for McpError

---------

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Kian Jones
2026-02-05 21:17:16 -08:00
committed by Caren Thomas
parent 70d749e859
commit e7039470e9

View File

@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Literal, Optional, Union
from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request
from fastmcp.exceptions import ToolError as FastMCPToolError
from httpx import ConnectError, HTTPStatusError
from mcp.shared.exceptions import McpError
from pydantic import BaseModel, Field
from starlette.responses import StreamingResponse
@@ -822,8 +823,21 @@ async def execute_mcp_tool(
# Execute the tool
try:
result, success = await client.execute_tool(tool_name, request.args)
except FastMCPToolError as e:
raise LettaInvalidArgumentError(f"Invalid arguments for MCP tool '{tool_name}': {str(e)}", argument_name="args")
except Exception as e:
# Handle ExceptionGroup wrapping (Python 3.11+ async TaskGroup can wrap exceptions)
exception_to_check = e
if hasattr(e, "exceptions") and e.exceptions:
if len(e.exceptions) == 1:
exception_to_check = e.exceptions[0]
# Check by class name to handle both fastmcp.exceptions.ToolError and potential module variations
if exception_to_check.__class__.__name__ == "ToolError":
raise LettaInvalidArgumentError(
f"Invalid arguments for MCP tool '{tool_name}': {str(exception_to_check)}", argument_name="args"
)
elif isinstance(exception_to_check, McpError):
raise LettaMCPConnectionError(f"MCP tool execution failed: {str(exception_to_check)}", server_name=tool_name)
raise
return {
"result": result,