fix: preserve exception propagation through transport cleanup (#2591)

anyio task groups suppress exceptions when cancel_scope.cancel() is
called during cleanup. Capture exceptions before cleanup and re-raise
after task group exits cleanly.

Also preserve McpError type in client _connect() so callers can catch
protocol-level errors specifically.
This commit is contained in:
Jeremiah Lowin 2025-12-10 15:37:03 -05:00 committed by GitHub
commit 95e58e87aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 14 additions and 3 deletions

View file

@ -16,7 +16,7 @@ import httpx
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 @@ class Client(Generic[ClientTransportT]):
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}"

View file

@ -866,7 +866,11 @@ class FastMCPTransport(ClientTransport):
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 @@ class FastMCPTransport(ClientTransport):
**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"<FastMCPTransport(server='{self.server.name}')>"