Support different transports in Client (#1972)

This commit is contained in:
Marcelo Trylesinski 2026-01-30 13:11:27 +01:00 • committed by GitHub
commit 21822053df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 544 additions and 631 deletions

View file

@ -2,11 +2,14 @@
from __future__ import annotations
from unittest.mock import patch
import anyio
import pytest
from inline_snapshot import snapshot
import mcp.types as types
from mcp.client._memory import InMemoryTransport
from mcp.client.client import Client
from mcp.server import Server
from mcp.server.mcpserver import MCPServer
@ -285,3 +288,21 @@ async def test_complete_with_prompt_reference(simple_server: Server):
ref = types.PromptReference(type="ref/prompt", name="test_prompt")
result = await client.complete(ref=ref, argument={"name": "arg", "value": "test"})
assert result == snapshot(types.CompleteResult(completion=types.Completion(values=[])))
def test_client_with_url_initializes_streamable_http_transport():
with patch("mcp.client.client.streamable_http_client") as mock:
_ = Client("http://localhost:8000/mcp")
mock.assert_called_once_with("http://localhost:8000/mcp")
async def test_client_uses_transport_directly(app: MCPServer):
transport = InMemoryTransport(app)
async with Client(transport) as client:
result = await client.call_tool("greet", {"name": "Transport"})
assert result == snapshot(
CallToolResult(
content=[TextContent(text="Hello, Transport!")],
structured_content={"result": "Hello, Transport!"},
)
)

View file

@ -173,7 +173,7 @@ async def test_streamable_http_client_unicode_tool_call(running_unicode_server:
base_url = running_unicode_server
endpoint_url = f"{base_url}/mcp"
async with streamable_http_client(endpoint_url) as (read_stream, write_stream, _get_session_id):
async with streamable_http_client(endpoint_url) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
@ -205,7 +205,7 @@ async def test_streamable_http_client_unicode_prompts(running_unicode_server: st
base_url = running_unicode_server
endpoint_url = f"{base_url}/mcp"
async with streamable_http_client(endpoint_url) as (read_stream, write_stream, _get_session_id):
async with streamable_http_client(endpoint_url) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()

View file

@ -125,12 +125,8 @@ async def test_non_compliant_notification_response(non_sdk_server: None, non_sdk
if isinstance(message, Exception):
returned_exception = message
async with streamable_http_client(server_url) as (read_stream, write_stream, _):
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
async with streamable_http_client(server_url) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
# Initialize should work normally
await session.initialize()

View file

@ -278,6 +278,7 @@ async def test_client_session_group_disconnect_non_existent_server():
await group.disconnect_from_server(session)
# TODO(Marcelo): This is horrible. We should drop this test.
@pytest.mark.anyio
@pytest.mark.parametrize(
"server_params_instance, client_type_name, patch_target_for_client_func",
@ -310,19 +311,8 @@ async def test_client_session_group_establish_session_parameterized(
mock_read_stream = mock.AsyncMock(name=f"{client_type_name}Read")
mock_write_stream = mock.AsyncMock(name=f"{client_type_name}Write")
# streamable_http_client's __aenter__ returns three values
if client_type_name == "streamablehttp":
mock_extra_stream_val = mock.AsyncMock(name="StreamableExtra")
mock_client_cm_instance.__aenter__.return_value = (
mock_read_stream,
mock_write_stream,
mock_extra_stream_val,
)
else:
mock_client_cm_instance.__aenter__.return_value = (
mock_read_stream,
mock_write_stream,
)
# All client context managers return (read_stream, write_stream)
mock_client_cm_instance.__aenter__.return_value = (mock_read_stream, mock_write_stream)
mock_client_cm_instance.__aexit__ = mock.AsyncMock(return_value=None)
mock_specific_client_func.return_value = mock_client_cm_instance

View file

@ -53,7 +53,7 @@ pytestmark = pytest.mark.anyio
async def test_with_server(simple_server: Server):
"""Test creating transport with a Server instance."""
transport = InMemoryTransport(simple_server)
async with transport.connect() as (read_stream, write_stream):
async with transport as (read_stream, write_stream):
assert read_stream is not None
assert write_stream is not None
@ -61,7 +61,7 @@ async def test_with_server(simple_server: Server):
async def test_with_mcpserver(mcpserver_server: MCPServer):
"""Test creating transport with an MCPServer instance."""
transport = InMemoryTransport(mcpserver_server)
async with transport.connect() as (read_stream, write_stream):
async with transport as (read_stream, write_stream):
assert read_stream is not None
assert write_stream is not None
@ -93,5 +93,5 @@ async def test_call_tool(mcpserver_server: MCPServer):
async def test_raise_exceptions(mcpserver_server: MCPServer):
"""Test that raise_exceptions parameter is passed through."""
transport = InMemoryTransport(mcpserver_server, raise_exceptions=True)
async with transport.connect() as (read_stream, _write_stream):
async with transport as (read_stream, _write_stream):
assert read_stream is not None

View file

@ -16,7 +16,6 @@ from collections.abc import Generator
import pytest
import uvicorn
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from inline_snapshot import snapshot
from examples.snippets.servers import (
@ -33,9 +32,8 @@ from examples.snippets.servers import (
)
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client
from mcp.client.streamable_http import GetSessionIdCallback, streamable_http_client
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.context import RequestContext
from mcp.shared.message import SessionMessage
from mcp.shared.session import RequestResponder
from mcp.types import (
ClientResult,
@ -185,32 +183,6 @@ def create_client_for_transport(transport: str, server_url: str):
raise ValueError(f"Invalid transport: {transport}")
def unpack_streams(
client_streams: tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]]
| tuple[
MemoryObjectReceiveStream[SessionMessage | Exception],
MemoryObjectSendStream[SessionMessage],
GetSessionIdCallback,
],
):
"""Unpack client streams handling different return values from SSE vs StreamableHTTP.
SSE client returns (read_stream, write_stream)
StreamableHTTP client returns (read_stream, write_stream, session_id_callback)
Args:
client_streams: Tuple from client context manager
Returns:
Tuple of (read_stream, write_stream)
"""
if len(client_streams) == 2:
return client_streams
else:
read_stream, write_stream, _ = client_streams
return read_stream, write_stream
# Callback functions for testing
async def sampling_callback(
context: RequestContext[ClientSession, None], params: CreateMessageRequestParams
@ -253,8 +225,7 @@ async def test_basic_tools(server_transport: str, server_url: str) -> None:
transport = server_transport
client_cm = create_client_for_transport(transport, server_url)
async with client_cm as client_streams:
read_stream, write_stream = unpack_streams(client_streams)
async with client_cm as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Test initialization
result = await session.initialize()
@ -290,8 +261,7 @@ async def test_basic_resources(server_transport: str, server_url: str) -> None:
transport = server_transport
client_cm = create_client_for_transport(transport, server_url)
async with client_cm as client_streams:
read_stream, write_stream = unpack_streams(client_streams)
async with client_cm as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Test initialization
result = await session.initialize()
@ -331,8 +301,7 @@ async def test_basic_prompts(server_transport: str, server_url: str) -> None:
transport = server_transport
client_cm = create_client_for_transport(transport, server_url)
async with client_cm as client_streams:
read_stream, write_stream = unpack_streams(client_streams)
async with client_cm as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Test initialization
result = await session.initialize()
@ -391,8 +360,7 @@ async def test_tool_progress(server_transport: str, server_url: str) -> None:
client_cm = create_client_for_transport(transport, server_url)
async with client_cm as client_streams:
read_stream, write_stream = unpack_streams(client_streams)
async with client_cm as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
# Test initialization
result = await session.initialize()
@ -441,8 +409,7 @@ async def test_sampling(server_transport: str, server_url: str) -> None:
transport = server_transport
client_cm = create_client_for_transport(transport, server_url)
async with client_cm as client_streams:
read_stream, write_stream = unpack_streams(client_streams)
async with client_cm as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, sampling_callback=sampling_callback) as session:
# Test initialization
result = await session.initialize()
@ -472,8 +439,7 @@ async def test_elicitation(server_transport: str, server_url: str) -> None:
transport = server_transport
client_cm = create_client_for_transport(transport, server_url)
async with client_cm as client_streams:
read_stream, write_stream = unpack_streams(client_streams)
async with client_cm as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, elicitation_callback=elicitation_callback) as session:
# Test initialization
result = await session.initialize()
@ -529,8 +495,7 @@ async def test_notifications(server_transport: str, server_url: str) -> None:
client_cm = create_client_for_transport(transport, server_url)
async with client_cm as client_streams:
read_stream, write_stream = unpack_streams(client_streams)
async with client_cm as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
# Test initialization
result = await session.initialize()
@ -570,8 +535,7 @@ async def test_completion(server_transport: str, server_url: str) -> None:
transport = server_transport
client_cm = create_client_for_transport(transport, server_url)
async with client_cm as client_streams:
read_stream, write_stream = unpack_streams(client_streams)
async with client_cm as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Test initialization
result = await session.initialize()
@ -623,8 +587,7 @@ async def test_mcpserver_quickstart(server_transport: str, server_url: str) -> N
transport = server_transport
client_cm = create_client_for_transport(transport, server_url)
async with client_cm as client_streams:
read_stream, write_stream = unpack_streams(client_streams)
async with client_cm as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Test initialization
result = await session.initialize()
@ -659,8 +622,7 @@ async def test_structured_output(server_transport: str, server_url: str) -> None
transport = server_transport
client_cm = create_client_for_transport(transport, server_url)
async with client_cm as client_streams:
read_stream, write_stream = unpack_streams(client_streams)
async with client_cm as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Test initialization
result = await session.initialize()

View file

@ -43,7 +43,11 @@ from mcp.server.streamable_http import (
)
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from mcp.server.transport_security import TransportSecuritySettings
from mcp.shared._httpx_utils import create_mcp_http_client
from mcp.shared._httpx_utils import (
MCP_DEFAULT_SSE_READ_TIMEOUT,
MCP_DEFAULT_TIMEOUT,
create_mcp_http_client,
)
from mcp.shared.context import RequestContext
from mcp.shared.message import ClientMessageMetadata, ServerMessageMetadata, SessionMessage
from mcp.shared.session import RequestResponder
@ -965,15 +969,8 @@ async def http_client(basic_server: None, basic_server_url: str): # pragma: no
@pytest.fixture
async def initialized_client_session(basic_server: None, basic_server_url: str):
"""Create initialized StreamableHTTP client session."""
async with streamable_http_client(f"{basic_server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with ClientSession(
read_stream,
write_stream,
) as session:
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
yield session
@ -981,11 +978,8 @@ async def initialized_client_session(basic_server: None, basic_server_url: str):
@pytest.mark.anyio
async def test_streamable_http_client_basic_connection(basic_server: None, basic_server_url: str):
"""Test basic client connection with initialization."""
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream, _):
async with ClientSession(
read_stream,
write_stream,
) as session:
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Test initialization
result = await session.initialize()
assert isinstance(result, InitializeResult)
@ -1029,15 +1023,8 @@ async def test_streamable_http_client_error_handling(initialized_client_session:
@pytest.mark.anyio
async def test_streamable_http_client_session_persistence(basic_server: None, basic_server_url: str):
"""Test that session ID persists across requests."""
async with streamable_http_client(f"{basic_server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with ClientSession(
read_stream,
write_stream,
) as session:
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Initialize the session
result = await session.initialize()
assert isinstance(result, InitializeResult)
@ -1057,7 +1044,7 @@ async def test_streamable_http_client_session_persistence(basic_server: None, ba
@pytest.mark.anyio
async def test_streamable_http_client_json_response(json_response_server: None, json_server_url: str):
"""Test client with JSON response mode."""
async with streamable_http_client(f"{json_server_url}/mcp") as (read_stream, write_stream, _):
async with streamable_http_client(f"{json_server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Initialize the session
result = await session.initialize()
@ -1087,7 +1074,7 @@ async def test_streamable_http_client_get_stream(basic_server: None, basic_serve
if isinstance(message, types.ServerNotification): # pragma: no branch
notifications_received.append(message)
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream, _):
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
# Initialize the session - this triggers the GET stream setup
result = await session.initialize()
@ -1109,34 +1096,54 @@ async def test_streamable_http_client_get_stream(basic_server: None, basic_serve
assert resource_update_found, "ResourceUpdatedNotification not received via GET stream"
def create_session_id_capturing_client() -> tuple[httpx.AsyncClient, list[str]]:
"""Create an httpx client that captures the session ID from responses."""
captured_ids: list[str] = []
async def capture_session_id(response: httpx.Response) -> None:
session_id = response.headers.get(MCP_SESSION_ID_HEADER)
if session_id:
captured_ids.append(session_id)
client = httpx.AsyncClient(
follow_redirects=True,
timeout=httpx.Timeout(MCP_DEFAULT_TIMEOUT, read=MCP_DEFAULT_SSE_READ_TIMEOUT),
event_hooks={"response": [capture_session_id]},
)
return client, captured_ids
@pytest.mark.anyio
async def test_streamable_http_client_session_termination(basic_server: None, basic_server_url: str):
"""Test client session termination functionality."""
# Use httpx client with event hooks to capture session ID
httpx_client, captured_ids = create_session_id_capturing_client()
captured_session_id = None
async with httpx_client:
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=httpx_client) as (
read_stream,
write_stream,
):
async with ClientSession(read_stream, write_stream) as session:
# Initialize the session
result = await session.initialize()
assert isinstance(result, InitializeResult)
assert len(captured_ids) > 0
captured_session_id = captured_ids[0]
assert captured_session_id is not None
# Create the streamable_http_client with a custom httpx client to capture headers
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream, get_session_id):
async with ClientSession(read_stream, write_stream) as session:
# Initialize the session
result = await session.initialize()
assert isinstance(result, InitializeResult)
captured_session_id = get_session_id()
assert captured_session_id is not None
# Make a request to confirm session is working
tools = await session.list_tools()
assert len(tools.tools) == 10
# Make a request to confirm session is working
tools = await session.list_tools()
assert len(tools.tools) == 10
headers: dict[str, str] = {} # pragma: lax no cover
if captured_session_id: # pragma: lax no cover
headers[MCP_SESSION_ID_HEADER] = captured_session_id
async with create_mcp_http_client(headers=headers) as httpx_client:
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=httpx_client) as (
async with create_mcp_http_client(headers=headers) as httpx_client2:
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=httpx_client2) as (
read_stream,
write_stream,
_,
):
async with ClientSession(read_stream, write_stream) as session: # pragma: no branch
# Attempt to make a request after termination
@ -1173,30 +1180,34 @@ async def test_streamable_http_client_session_termination_204(
# Apply the patch to the httpx client
monkeypatch.setattr(httpx.AsyncClient, "delete", mock_delete)
captured_session_id = None
# Use httpx client with event hooks to capture session ID
httpx_client, captured_ids = create_session_id_capturing_client()
# Create the streamable_http_client with a custom httpx client to capture headers
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream, get_session_id):
async with ClientSession(read_stream, write_stream) as session:
# Initialize the session
result = await session.initialize()
assert isinstance(result, InitializeResult)
captured_session_id = get_session_id()
assert captured_session_id is not None
async with httpx_client:
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=httpx_client) as (
read_stream,
write_stream,
):
async with ClientSession(read_stream, write_stream) as session:
# Initialize the session
result = await session.initialize()
assert isinstance(result, InitializeResult)
assert len(captured_ids) > 0
captured_session_id = captured_ids[0]
assert captured_session_id is not None
# Make a request to confirm session is working
tools = await session.list_tools()
assert len(tools.tools) == 10
# Make a request to confirm session is working
tools = await session.list_tools()
assert len(tools.tools) == 10
headers: dict[str, str] = {} # pragma: lax no cover
if captured_session_id: # pragma: lax no cover
headers[MCP_SESSION_ID_HEADER] = captured_session_id
async with create_mcp_http_client(headers=headers) as httpx_client:
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=httpx_client) as (
async with create_mcp_http_client(headers=headers) as httpx_client2:
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=httpx_client2) as (
read_stream,
write_stream,
_,
):
async with ClientSession(read_stream, write_stream) as session: # pragma: no branch
# Attempt to make a request after termination
@ -1210,10 +1221,9 @@ async def test_streamable_http_client_resumption(event_server: tuple[SimpleEvent
_, server_url = event_server
# Variables to track the state
captured_session_id = None
captured_resumption_token = None
captured_resumption_token: str | None = None
captured_notifications: list[types.ServerNotification] = []
captured_protocol_version = None
captured_protocol_version: str | int | None = None
first_notification_received = False
async def message_handler( # pragma: no branch
@ -1231,50 +1241,56 @@ async def test_streamable_http_client_resumption(event_server: tuple[SimpleEvent
nonlocal captured_resumption_token
captured_resumption_token = token
# Use httpx client with event hooks to capture session ID
httpx_client, captured_ids = create_session_id_capturing_client()
# First, start the client session and begin the tool that waits on lock
async with streamable_http_client(f"{server_url}/mcp", terminate_on_close=False) as (
read_stream,
write_stream,
get_session_id,
):
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
# Initialize the session
result = await session.initialize()
assert isinstance(result, InitializeResult)
captured_session_id = get_session_id()
assert captured_session_id is not None
# Capture the negotiated protocol version
captured_protocol_version = result.protocol_version
async with httpx_client:
async with streamable_http_client(f"{server_url}/mcp", terminate_on_close=False, http_client=httpx_client) as (
read_stream,
write_stream,
):
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
# Initialize the session
result = await session.initialize()
assert isinstance(result, InitializeResult)
assert len(captured_ids) > 0
captured_session_id = captured_ids[0]
assert captured_session_id is not None
# Capture the negotiated protocol version
captured_protocol_version = result.protocol_version
# Start the tool that will wait on lock in a task
async with anyio.create_task_group() as tg: # pragma: no branch
# Start the tool that will wait on lock in a task
async with anyio.create_task_group() as tg: # pragma: no branch
async def run_tool():
metadata = ClientMessageMetadata(
on_resumption_token_update=on_resumption_token_update,
)
await session.send_request(
types.CallToolRequest(
params=types.CallToolRequestParams(name="wait_for_lock_with_notification", arguments={}),
),
types.CallToolResult,
metadata=metadata,
)
async def run_tool():
metadata = ClientMessageMetadata(
on_resumption_token_update=on_resumption_token_update,
)
await session.send_request(
types.CallToolRequest(
params=types.CallToolRequestParams(
name="wait_for_lock_with_notification", arguments={}
),
),
types.CallToolResult,
metadata=metadata,
)
tg.start_soon(run_tool)
tg.start_soon(run_tool)
# Wait for the first notification and resumption token
while not first_notification_received or not captured_resumption_token:
await anyio.sleep(0.1)
# Wait for the first notification and resumption token
while not first_notification_received or not captured_resumption_token:
await anyio.sleep(0.1)
# Kill the client session while tool is waiting on lock
tg.cancel_scope.cancel()
# Kill the client session while tool is waiting on lock
tg.cancel_scope.cancel()
# Verify we received exactly one notification (inside ClientSession
# so coverage tracks these on Python 3.11, see PR #1897 for details)
assert len(captured_notifications) == 1 # pragma: lax no cover
assert isinstance(captured_notifications[0], types.LoggingMessageNotification) # pragma: lax no cover
assert captured_notifications[0].params.data == "First notification before lock" # pragma: lax no cover
# Verify we received exactly one notification (inside ClientSession
# so coverage tracks these on Python 3.11, see PR #1897 for details)
assert len(captured_notifications) == 1 # pragma: lax no cover
assert isinstance(captured_notifications[0], types.LoggingMessageNotification) # pragma: lax no cover
assert captured_notifications[0].params.data == "First notification before lock" # pragma: lax no cover
# Clear notifications and set up headers for phase 2 (between connections,
# not tracked by coverage on Python 3.11 due to cancel scope + sys.settrace bug)
@ -1286,11 +1302,10 @@ async def test_streamable_http_client_resumption(event_server: tuple[SimpleEvent
MCP_PROTOCOL_VERSION_HEADER: captured_protocol_version,
}
async with create_mcp_http_client(headers=headers) as httpx_client:
async with streamable_http_client(f"{server_url}/mcp", http_client=httpx_client) as (
async with create_mcp_http_client(headers=headers) as httpx_client2:
async with streamable_http_client(f"{server_url}/mcp", http_client=httpx_client2) as (
read_stream,
write_stream,
_,
):
async with ClientSession(
read_stream, write_stream, message_handler=message_handler
@ -1349,16 +1364,8 @@ async def test_streamablehttp_server_sampling(basic_server: None, basic_server_u
)
# Create client with sampling callback
async with streamable_http_client(f"{basic_server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with ClientSession(
read_stream,
write_stream,
sampling_callback=sampling_callback,
) as session:
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, sampling_callback=sampling_callback) as session:
# Initialize the session
result = await session.initialize()
assert isinstance(result, InitializeResult)
@ -1498,7 +1505,6 @@ async def test_streamablehttp_request_context_propagation(context_aware_server:
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=httpx_client) as (
read_stream,
write_stream,
_,
):
async with ClientSession(read_stream, write_stream) as session: # pragma: no branch
result = await session.initialize()
@ -1536,7 +1542,6 @@ async def test_streamablehttp_request_context_isolation(context_aware_server: No
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=httpx_client) as (
read_stream,
write_stream,
_,
):
async with ClientSession(read_stream, write_stream) as session: # pragma: no branch
await session.initialize()
@ -1561,11 +1566,7 @@ async def test_streamablehttp_request_context_isolation(context_aware_server: No
@pytest.mark.anyio
async def test_client_includes_protocol_version_header_after_init(context_aware_server: None, basic_server_url: str):
"""Test that client includes mcp-protocol-version header after initialization."""
async with streamable_http_client(f"{basic_server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Initialize and get the negotiated version
init_result = await session.initialize()
@ -1677,7 +1678,7 @@ async def test_client_crash_handled(basic_server: None, basic_server_url: str):
# Simulate bad client that crashes after init
async def bad_client():
"""Client that triggers ClosedResourceError"""
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream, _):
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
raise Exception("client crash")
@ -1691,7 +1692,7 @@ async def test_client_crash_handled(basic_server: None, basic_server_url: str):
await anyio.sleep(0.1)
# Try a good client, it should still be able to connect and list tools
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream, _):
async with streamable_http_client(f"{basic_server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
result = await session.initialize()
assert isinstance(result, InitializeResult)
@ -1847,11 +1848,7 @@ async def test_streamable_http_client_receives_priming_event(
async def on_resumption_token_update(token: str) -> None:
captured_resumption_tokens.append(token)
async with streamable_http_client(f"{server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with streamable_http_client(f"{server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
@ -1884,11 +1881,7 @@ async def test_server_close_sse_stream_via_context(
"""Server tool can call ctx.close_sse_stream() to close connection."""
_, server_url = event_server
async with streamable_http_client(f"{server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with streamable_http_client(f"{server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
@ -1921,16 +1914,8 @@ async def test_streamable_http_client_auto_reconnects(
if isinstance(message, types.LoggingMessageNotification): # pragma: no branch
captured_notifications.append(str(message.params.data))
async with streamable_http_client(f"{server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
async with streamable_http_client(f"{server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
await session.initialize()
# Call tool that:
@ -1956,11 +1941,7 @@ async def test_streamable_http_client_respects_retry_interval(
"""Client MUST respect retry field, waiting specified ms before reconnecting."""
_, server_url = event_server
async with streamable_http_client(f"{server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with streamable_http_client(f"{server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
@ -1997,16 +1978,8 @@ async def test_streamable_http_sse_polling_full_cycle(
if isinstance(message, types.LoggingMessageNotification): # pragma: no branch
all_notifications.append(str(message.params.data))
async with streamable_http_client(f"{server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
async with streamable_http_client(f"{server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
await session.initialize()
# Call tool that simulates polling pattern:
@ -2045,16 +2018,8 @@ async def test_streamable_http_events_replayed_after_disconnect(
if isinstance(message, types.LoggingMessageNotification): # pragma: no branch
notification_data.append(str(message.params.data))
async with streamable_http_client(f"{server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
async with streamable_http_client(f"{server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
await session.initialize()
# Tool sends: notification1, close_stream, notification2, notification3, response
@ -2097,7 +2062,7 @@ async def test_streamable_http_multiple_reconnections(
async def on_resumption_token(token: str) -> None:
resumption_tokens.append(token)
async with streamable_http_client(f"{server_url}/mcp") as (read_stream, write_stream, _):
async with streamable_http_client(f"{server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
@ -2128,9 +2093,7 @@ async def test_streamable_http_multiple_reconnections(
@pytest.mark.anyio
async def test_standalone_get_stream_reconnection(
event_server: tuple[SimpleEventStore, str],
) -> None:
async def test_standalone_get_stream_reconnection(event_server: tuple[SimpleEventStore, str]) -> None:
"""Test that standalone GET stream automatically reconnects after server closes it.
Verifies:
@ -2154,16 +2117,8 @@ async def test_standalone_get_stream_reconnection(
if isinstance(message, types.ResourceUpdatedNotification): # pragma: no branch
received_notifications.append(str(message.params.uri))
async with streamable_http_client(f"{server_url}/mcp") as (
read_stream,
write_stream,
_,
):
async with ClientSession(
read_stream,
write_stream,
message_handler=message_handler,
) as session:
async with streamable_http_client(f"{server_url}/mcp") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:
await session.initialize()
# Call tool that:
@ -2203,7 +2158,6 @@ async def test_streamable_http_client_does_not_mutate_provided_client(
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=custom_client) as (
read_stream,
write_stream,
_,
):
async with ClientSession(read_stream, write_stream) as session:
result = await session.initialize()
@ -2233,11 +2187,7 @@ async def test_streamable_http_client_mcp_headers_override_defaults(
# Verify client has default accept header
assert client.headers.get("accept") == "*/*"
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=client) as (
read_stream,
write_stream,
_,
):
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=client) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session: # pragma: no branch
await session.initialize()
@ -2268,11 +2218,7 @@ async def test_streamable_http_client_preserves_custom_with_mcp_headers(
}
async with httpx.AsyncClient(headers=custom_headers, follow_redirects=True) as client:
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=client) as (
read_stream,
write_stream,
_,
):
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=client) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session: # pragma: no branch
await session.initialize()