From bc07264529fe108b43ae81d116a15d3e2808d23b Mon Sep 17 00:00:00 2001 From: Jake Kaplan <40362401+jakekaplan@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:23:33 -0400 Subject: [PATCH] Fix self-referential connection error causes (#4720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with OpenAI Codex --- fastmcp_slim/fastmcp/client/client.py | 5 ++- tests/client/auth/test_oauth_client.py | 4 +- tests/client/client/test_session.py | 55 ++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/fastmcp_slim/fastmcp/client/client.py b/fastmcp_slim/fastmcp/client/client.py index c9b9aa1eb..55aff6408 100644 --- a/fastmcp_slim/fastmcp/client/client.py +++ b/fastmcp_slim/fastmcp/client/client.py @@ -1022,7 +1022,10 @@ class Client( raise RuntimeError( "Session task completed without exception but connection failed" ) - raise _connection_failure(exception) from exception + failure = _connection_failure(exception) + if failure is exception: + raise exception + raise failure from exception self._session_state.nesting_counter += 1 diff --git a/tests/client/auth/test_oauth_client.py b/tests/client/auth/test_oauth_client.py index 1ded252f8..22ca63af0 100644 --- a/tests/client/auth/test_oauth_client.py +++ b/tests/client/auth/test_oauth_client.py @@ -96,10 +96,12 @@ async def test_unauthorized(client_unauthorized: Client): SDK v2 surfaces the server's 401 as an MCPError ("Server returned an error response") rather than re-raising the raw httpx2.HTTPStatusError. """ - with pytest.raises(MCPError, match="error response"): + with pytest.raises(MCPError, match="error response") as exc_info: async with client_unauthorized: pass + assert exc_info.value.__cause__ is not exc_info.value + async def test_ping(streamable_http_server: str): """Test that we can ping the server. diff --git a/tests/client/client/test_session.py b/tests/client/client/test_session.py index 0579aa6d5..116bff69c 100644 --- a/tests/client/client/test_session.py +++ b/tests/client/client/test_session.py @@ -1,18 +1,33 @@ """Client session and task error propagation tests.""" import asyncio +from collections.abc import AsyncIterator from contextlib import asynccontextmanager +from typing import Any +import httpx2 import pytest -from mcp import ClientSession -from mcp_types import TextContent +from mcp import ClientSession, MCPError +from mcp_types import INTERNAL_ERROR, TextContent from fastmcp import FastMCP from fastmcp.client import Client -from fastmcp.client.transports import PythonStdioTransport +from fastmcp.client.transports import ClientTransport, PythonStdioTransport from fastmcp.client.transports.base import TransportOptions +class _FailingTransport(ClientTransport): + def __init__(self, exception: Exception) -> None: + self._exception = exception + + @asynccontextmanager + async def connect_session( + self, **session_kwargs: Any + ) -> AsyncIterator[ClientSession]: + raise self._exception + yield + + class TestSessionTaskErrorPropagation: """Tests for ensuring session task errors propagate to client calls. @@ -143,6 +158,40 @@ class TestSessionTaskErrorPropagation: client._session_state.session_task = original_task +class TestConnectionFailurePropagation: + @pytest.mark.parametrize( + "failure", + [ + MCPError(code=INTERNAL_ERROR, message="upstream failed"), + httpx2.HTTPStatusError( + "upstream unavailable", + request=httpx2.Request("GET", "https://example.com"), + response=httpx2.Response(503), + ), + ], + ids=["mcp-error", "http-status-error"], + ) + async def test_preserves_passthrough_exception(self, failure: Exception): + client = Client(transport=_FailingTransport(failure)) + + with pytest.raises(type(failure)) as exc_info: + async with client: + pass + + assert exc_info.value is failure + assert exc_info.value.__cause__ is not failure + + async def test_wraps_other_failures_with_cause(self): + failure = OSError("connection refused") + client = Client(transport=_FailingTransport(failure)) + + with pytest.raises(RuntimeError, match="Client failed to connect") as exc_info: + async with client: + pass + + assert exc_info.value.__cause__ is failure + + class TestCustomSessionClass: """Transports build the session class the client asks for."""