From f67af1b13d3b5f5ac9ebb7838ca0f01c6e54d71a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:51:53 -0800 Subject: [PATCH] fix: Handle ExceptionGroup errors in MCP client cleanup (#8561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP library internally uses TaskGroup for async operations, which can raise ExceptionGroup when cleanup fails. This was causing unhandled errors to propagate in production. Changes: - Update cleanup() method in AsyncBaseMCPClient to catch ExceptionGroup using except* syntax and log errors at debug level (best-effort cleanup) - Remove redundant try/except blocks in mcp_manager.py and mcp_server_manager.py that incorrectly re-raised cleanup exceptions Fixes #8560 🐾 Generated with [Letta Code](https://letta.com) Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com> Co-authored-by: Letta Co-authored-by: Kian Jones <11655409+kianjones9@users.noreply.github.com> --- letta/services/mcp/base_client.py | 16 ++++++++++++++-- letta/services/mcp_manager.py | 7 +------ letta/services/mcp_server_manager.py | 6 +----- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/letta/services/mcp/base_client.py b/letta/services/mcp/base_client.py index 5e444e04..28f541ca 100644 --- a/letta/services/mcp/base_client.py +++ b/letta/services/mcp/base_client.py @@ -109,9 +109,21 @@ class AsyncBaseMCPClient: logger.error("MCPClient has not been initialized") raise RuntimeError("MCPClient has not been initialized") - # TODO: still hitting some async errors for voice agents, need to fix async def cleanup(self): - await self.exit_stack.aclose() + """Clean up resources used by the MCP client. + + This method handles ExceptionGroup errors that can occur when closing async context managers + (e.g., from the MCP library's internal TaskGroup usage). Cleanup is a best-effort operation + and errors are logged but not re-raised to prevent masking the original exception. + """ + try: + await self.exit_stack.aclose() + except* Exception as eg: + # ExceptionGroup can be raised when closing async context managers that use TaskGroup + # Log each sub-exception at debug level since cleanup errors are expected in some cases + # (e.g., connection already closed, server unavailable) + for exc in eg.exceptions: + logger.debug(f"MCP client cleanup error (suppressed): {type(exc).__name__}: {exc}") def to_sync_client(self): raise NotImplementedError("Subclasses must implement to_sync_client") diff --git a/letta/services/mcp_manager.py b/letta/services/mcp_manager.py index db740a5e..a4708484 100644 --- a/letta/services/mcp_manager.py +++ b/letta/services/mcp_manager.py @@ -93,12 +93,7 @@ class MCPManager: raise e finally: if mcp_client: - try: - await mcp_client.cleanup() - except* Exception as eg: - for e in eg.exceptions: - logger.warning(f"Error listing tools for MCP server {mcp_server_name}: {e}") - raise e + await mcp_client.cleanup() @enforce_types async def execute_mcp_server_tool( diff --git a/letta/services/mcp_server_manager.py b/letta/services/mcp_server_manager.py index b5fd06d1..6c71dedb 100644 --- a/letta/services/mcp_server_manager.py +++ b/letta/services/mcp_server_manager.py @@ -186,11 +186,7 @@ class MCPServerManager: raise e finally: if mcp_client: - try: - await mcp_client.cleanup() - except Exception as e: - logger.warning(f"Error listing tools for MCP server {mcp_server_id}: {e}") - raise e + await mcp_client.cleanup() @enforce_types async def execute_mcp_server_tool(