fix(core): convert MCP ConnectionError to LettaMCPConnectionError for proper HTTP 502 responses (#9364)
MCP server connection failures were raising Python's builtin ConnectionError, which bypassed the LettaMCPConnectionError FastAPI exception handler and hit Datadog as unhandled 500 errors. Now all MCP client classes convert ConnectionError to LettaMCPConnectionError at the source, which the existing exception handler returns as a user-friendly 502. Datadog: https://us5.datadoghq.com/error-tracking/issue/93db4a82-fe5a-11f0-85f0-da7ad0900000 🐛 Generated with [Letta Code](https://letta.com) Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Request
|
||||
from httpx import HTTPStatusError
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from letta.errors import LettaMCPConnectionError
|
||||
from letta.functions.mcp_client.types import SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig
|
||||
from letta.log import get_logger
|
||||
from letta.schemas.letta_message import ToolReturnMessage
|
||||
@@ -268,8 +269,7 @@ async def connect_mcp_server(
|
||||
tools = await client.list_tools(serialize=True)
|
||||
yield oauth_stream_event(OauthStreamEvent.SUCCESS, tools=tools)
|
||||
return
|
||||
except ConnectionError:
|
||||
# TODO: jnjpng make this connection error check more specific to the 401 unauthorized error
|
||||
except (ConnectionError, LettaMCPConnectionError):
|
||||
if isinstance(client, AsyncStdioMCPClient):
|
||||
logger.warning("OAuth not supported for stdio")
|
||||
yield oauth_stream_event(OauthStreamEvent.ERROR, message="OAuth not supported for stdio")
|
||||
|
||||
@@ -643,7 +643,9 @@ async def test_mcp_server(
|
||||
tools = await client.list_tools()
|
||||
|
||||
return {"status": "success", "tools": tools}
|
||||
except ConnectionError as e:
|
||||
except (ConnectionError, LettaMCPConnectionError) as e:
|
||||
if isinstance(e, LettaMCPConnectionError):
|
||||
raise
|
||||
raise LettaMCPConnectionError(str(e), server_name=request.server_name)
|
||||
except MCPTimeoutError as e:
|
||||
raise LettaMCPTimeoutError(f"MCP server connection timed out: {str(e)}", server_name=request.server_name)
|
||||
@@ -705,8 +707,7 @@ async def connect_mcp_server(
|
||||
tools = await client.list_tools(serialize=True)
|
||||
yield oauth_stream_event(OauthStreamEvent.SUCCESS, tools=tools)
|
||||
return
|
||||
except ConnectionError as e:
|
||||
# Only trigger OAuth flow on explicit unauthorized failures
|
||||
except (ConnectionError, LettaMCPConnectionError) as e:
|
||||
unauthorized = False
|
||||
if isinstance(e.__cause__, HTTPStatusError):
|
||||
unauthorized = e.__cause__.response.status_code == 401
|
||||
|
||||
@@ -1799,9 +1799,14 @@ class SyncServer(object):
|
||||
raise LettaInvalidArgumentError(f"Invalid MCP server config: {server_config}", argument_name="server_config")
|
||||
try:
|
||||
await new_mcp_client.connect_to_server()
|
||||
except:
|
||||
except LettaMCPConnectionError:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception(f"Failed to connect to MCP server: {server_config.server_name}")
|
||||
raise RuntimeError(f"Failed to connect to MCP server: {server_config.server_name}")
|
||||
raise LettaMCPConnectionError(
|
||||
message=f"Failed to connect to MCP server: {server_config.server_name}",
|
||||
server_name=server_config.server_name,
|
||||
)
|
||||
# Print out the tools that are connected
|
||||
logger.info(f"Attempting to fetch tools from MCP server: {server_config.server_name}")
|
||||
new_mcp_tools = await new_mcp_client.list_tools()
|
||||
|
||||
@@ -5,6 +5,7 @@ from mcp import ClientSession, Tool as MCPTool
|
||||
from mcp.client.auth import OAuthClientProvider
|
||||
from mcp.types import TextContent
|
||||
|
||||
from letta.errors import LettaMCPConnectionError
|
||||
from letta.functions.mcp_client.types import BaseServerConfig
|
||||
from letta.log import get_logger
|
||||
|
||||
@@ -31,14 +32,12 @@ class AsyncBaseMCPClient:
|
||||
await self._initialize_connection(self.server_config)
|
||||
await self.session.initialize()
|
||||
self.initialized = True
|
||||
except LettaMCPConnectionError:
|
||||
raise
|
||||
except ConnectionError as e:
|
||||
# MCP connection failures are often due to user misconfiguration, not system errors
|
||||
# Log at debug level to avoid triggering Sentry alerts for expected configuration issues
|
||||
logger.debug(f"MCP connection failed: {str(e)}")
|
||||
raise e
|
||||
raise LettaMCPConnectionError(message=str(e), server_name=getattr(self.server_config, "server_name", None)) from e
|
||||
except Exception as e:
|
||||
# MCP connection failures are often due to user misconfiguration, not system errors
|
||||
# Log as warning for visibility in monitoring
|
||||
logger.warning(
|
||||
f"Connecting to MCP server failed. Please review your server config: {self.server_config.model_dump_json(indent=4)}. Error: {str(e)}"
|
||||
)
|
||||
@@ -48,8 +47,9 @@ class AsyncBaseMCPClient:
|
||||
server_info = f"command '{self.server_config.command}'"
|
||||
else:
|
||||
server_info = f"server '{self.server_config.server_name}'"
|
||||
raise ConnectionError(
|
||||
f"Failed to connect to MCP {server_info}. Please check your configuration and ensure the server is accessible."
|
||||
raise LettaMCPConnectionError(
|
||||
message=f"Failed to connect to MCP {server_info}. Please check your configuration and ensure the server is accessible.",
|
||||
server_name=getattr(self.server_config, "server_name", None),
|
||||
) from e
|
||||
|
||||
async def _initialize_connection(self, server_config: BaseServerConfig) -> None:
|
||||
|
||||
@@ -16,6 +16,7 @@ from fastmcp import Client
|
||||
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
||||
from mcp import Tool as MCPTool
|
||||
|
||||
from letta.errors import LettaMCPConnectionError
|
||||
from letta.functions.mcp_client.types import SSEServerConfig, StreamableHTTPServerConfig
|
||||
from letta.log import get_logger
|
||||
from letta.services.mcp.server_side_oauth import ServerSideOAuth
|
||||
@@ -76,22 +77,24 @@ class AsyncFastMCPSSEClient:
|
||||
await self.client._connect()
|
||||
self.initialized = True
|
||||
except httpx.HTTPStatusError as e:
|
||||
# Re-raise HTTP status errors for OAuth flow handling
|
||||
if e.response.status_code == 401:
|
||||
raise ConnectionError("401 Unauthorized") from e
|
||||
raise ConnectionError(f"HTTP error connecting to MCP server at {self.server_config.server_url}: {e}") from e
|
||||
except ConnectionError:
|
||||
# Re-raise ConnectionError as-is
|
||||
raise LettaMCPConnectionError(message="401 Unauthorized", server_name=self.server_config.server_name) from e
|
||||
raise LettaMCPConnectionError(
|
||||
message=f"HTTP error connecting to MCP server at {self.server_config.server_url}: {e}",
|
||||
server_name=self.server_config.server_name,
|
||||
) from e
|
||||
except LettaMCPConnectionError:
|
||||
raise
|
||||
except ConnectionError as e:
|
||||
raise LettaMCPConnectionError(message=str(e), server_name=self.server_config.server_name) from e
|
||||
except Exception as e:
|
||||
# MCP connection failures are often due to user misconfiguration, not system errors
|
||||
# Log as warning for visibility in monitoring
|
||||
logger.warning(
|
||||
f"Connecting to MCP server failed. Please review your server config: {self.server_config.model_dump_json(indent=4)}. Error: {str(e)}"
|
||||
)
|
||||
raise ConnectionError(
|
||||
f"Failed to connect to MCP server at '{self.server_config.server_url}'. "
|
||||
f"Please check your configuration and ensure the server is accessible. Error: {str(e)}"
|
||||
raise LettaMCPConnectionError(
|
||||
message=f"Failed to connect to MCP server at '{self.server_config.server_url}'. "
|
||||
f"Please check your configuration and ensure the server is accessible. Error: {str(e)}",
|
||||
server_name=self.server_config.server_name,
|
||||
) from e
|
||||
|
||||
async def list_tools(self, serialize: bool = False) -> List[MCPTool]:
|
||||
@@ -241,22 +244,24 @@ class AsyncFastMCPStreamableHTTPClient:
|
||||
await self.client._connect()
|
||||
self.initialized = True
|
||||
except httpx.HTTPStatusError as e:
|
||||
# Re-raise HTTP status errors for OAuth flow handling
|
||||
if e.response.status_code == 401:
|
||||
raise ConnectionError("401 Unauthorized") from e
|
||||
raise ConnectionError(f"HTTP error connecting to MCP server at {self.server_config.server_url}: {e}") from e
|
||||
except ConnectionError:
|
||||
# Re-raise ConnectionError as-is
|
||||
raise LettaMCPConnectionError(message="401 Unauthorized", server_name=self.server_config.server_name) from e
|
||||
raise LettaMCPConnectionError(
|
||||
message=f"HTTP error connecting to MCP server at {self.server_config.server_url}: {e}",
|
||||
server_name=self.server_config.server_name,
|
||||
) from e
|
||||
except LettaMCPConnectionError:
|
||||
raise
|
||||
except ConnectionError as e:
|
||||
raise LettaMCPConnectionError(message=str(e), server_name=self.server_config.server_name) from e
|
||||
except Exception as e:
|
||||
# MCP connection failures are often due to user misconfiguration, not system errors
|
||||
# Log as warning for visibility in monitoring
|
||||
logger.warning(
|
||||
f"Connecting to MCP server failed. Please review your server config: {self.server_config.model_dump_json(indent=4)}. Error: {str(e)}"
|
||||
)
|
||||
raise ConnectionError(
|
||||
f"Failed to connect to MCP server at '{self.server_config.server_url}'. "
|
||||
f"Please check your configuration and ensure the server is accessible. Error: {str(e)}"
|
||||
raise LettaMCPConnectionError(
|
||||
message=f"Failed to connect to MCP server at '{self.server_config.server_url}'. "
|
||||
f"Please check your configuration and ensure the server is accessible. Error: {str(e)}",
|
||||
server_name=self.server_config.server_name,
|
||||
) from e
|
||||
|
||||
async def list_tools(self, serialize: bool = False) -> List[MCPTool]:
|
||||
|
||||
Reference in New Issue
Block a user