diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index 4651edadb..c498039e9 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -16,7 +16,7 @@ import mcp.types import pydantic_core from exceptiongroup import catch -from mcp import ClientSession +from mcp import ClientSession, McpError from mcp.types import ( CancelTaskRequest, CancelTaskRequestParams, @@ -514,7 +514,8 @@ async def _connect(self): raise RuntimeError( "Session task completed without exception but connection failed" ) - if isinstance(exception, httpx.HTTPStatusError): + # Preserve specific exception types that clients may want to handle + if isinstance(exception, httpx.HTTPStatusError | McpError): raise exception raise RuntimeError( f"Client failed to connect: {exception}" diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index a6336d123..eff3e2ba9 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -866,7 +866,11 @@ async def connect_session( client_read, client_write = client_streams server_read, server_write = server_streams - # Create a cancel scope for the server task + # Capture exceptions to re-raise after task group cleanup. + # anyio task groups can suppress exceptions when cancel_scope.cancel() + # is called during cleanup, so we capture and re-raise manually. + exception_to_raise: BaseException | None = None + async with ( anyio.create_task_group() as tg, _enter_server_lifespan(server=self.server), @@ -892,9 +896,15 @@ async def connect_session( **session_kwargs, ) as client_session: yield client_session + except BaseException as e: + exception_to_raise = e finally: tg.cancel_scope.cancel() + # Re-raise after task group has exited cleanly + if exception_to_raise is not None: + raise exception_to_raise + def __repr__(self) -> str: return f""