diff --git a/letta/server/rest_api/routers/v1/mcp_servers.py b/letta/server/rest_api/routers/v1/mcp_servers.py index 8037966e..0b037777 100644 --- a/letta/server/rest_api/routers/v1/mcp_servers.py +++ b/letta/server/rest_api/routers/v1/mcp_servers.py @@ -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") diff --git a/letta/server/rest_api/routers/v1/tools.py b/letta/server/rest_api/routers/v1/tools.py index 1884b425..0960ca46 100644 --- a/letta/server/rest_api/routers/v1/tools.py +++ b/letta/server/rest_api/routers/v1/tools.py @@ -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 diff --git a/letta/server/server.py b/letta/server/server.py index 023694ff..0d7584bb 100644 --- a/letta/server/server.py +++ b/letta/server/server.py @@ -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() diff --git a/letta/services/mcp/base_client.py b/letta/services/mcp/base_client.py index a5e76ce2..31b25e29 100644 --- a/letta/services/mcp/base_client.py +++ b/letta/services/mcp/base_client.py @@ -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: diff --git a/letta/services/mcp/fastmcp_client.py b/letta/services/mcp/fastmcp_client.py index cc20f0a1..01190e56 100644 --- a/letta/services/mcp/fastmcp_client.py +++ b/letta/services/mcp/fastmcp_client.py @@ -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]: