Skip to content

Commit e231f3e

Browse files
Refactor to InMemoryTransport class and delete create_connected_server_and_client_session
Breaking change for v2: - Replace create_connected_server_and_client_session() function with InMemoryTransport class - Add Client.from_server() for ergonomic testing - Migrate all tests to use new API patterns - Update stream spy fixture to patch both module locations High-level tests now use: async with Client.from_server(server) as client: result = await client.call_tool(...) Low-level tests use: transport = InMemoryTransport(server) async with transport.connect() as (read, write): async with ClientSession(read, write) as session: ... Github-Issue:#1728
1 parent b9955e0 commit e231f3e

23 files changed

+374
-508
lines changed

src/mcp/client/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
from mcp.client.client import Client
44
from mcp.client.session import ClientSession
5-
from mcp.client.transports.memory import create_in_memory_transport
5+
from mcp.client.transports.memory import InMemoryTransport
66

77
__all__ = [
88
"Client",
99
"ClientSession",
10-
"create_in_memory_transport",
10+
"InMemoryTransport",
1111
]

src/mcp/client/client.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
MessageHandlerFnT,
2020
SamplingFnT,
2121
)
22-
from mcp.client.transports.memory import create_in_memory_transport
22+
from mcp.client.transports.memory import InMemoryTransport
2323
from mcp.server import Server
2424
from mcp.server.fastmcp import FastMCP
2525
from mcp.shared.message import SessionMessage
@@ -132,9 +132,8 @@ def my_tool(arg: str) -> str:
132132
async with Client.from_server(server) as client:
133133
result = await client.call_tool("my_tool", {"arg": "value"})
134134
"""
135-
async with create_in_memory_transport(
136-
server, raise_exceptions=raise_exceptions
137-
) as (read_stream, write_stream):
135+
transport = InMemoryTransport(server, raise_exceptions=raise_exceptions)
136+
async with transport.connect() as (read_stream, write_stream):
138137
client = cls(
139138
read_stream=read_stream,
140139
write_stream=write_stream,
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Transport implementations for MCP clients."""
22

3-
from mcp.client.transports.memory import create_in_memory_transport
3+
from mcp.client.transports.memory import InMemoryTransport
44

5-
__all__ = ["create_in_memory_transport"]
5+
__all__ = ["InMemoryTransport"]

src/mcp/client/transports/memory.py

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,63 +15,83 @@
1515
from mcp.shared.message import SessionMessage
1616

1717

18-
@asynccontextmanager
19-
async def create_in_memory_transport(
20-
server: Server[Any] | FastMCP,
21-
*,
22-
raise_exceptions: bool = False,
23-
) -> AsyncGenerator[
24-
tuple[
25-
MemoryObjectReceiveStream[SessionMessage | Exception],
26-
MemoryObjectSendStream[SessionMessage],
27-
],
28-
None,
29-
]:
18+
class InMemoryTransport:
3019
"""
31-
Create an in-memory transport connected to a server.
20+
In-memory transport for testing MCP servers without network overhead.
3221
33-
This starts the server in a background task and returns streams
34-
for client-side communication. The server is automatically stopped
35-
when the context manager exits.
36-
37-
Args:
38-
server: The MCP server to connect to (Server or FastMCP instance)
39-
raise_exceptions: Whether to raise exceptions from the server
40-
41-
Yields:
42-
A tuple of (read_stream, write_stream) for communicating with the server
22+
This transport starts the server in a background task and provides
23+
streams for client-side communication. The server is automatically
24+
stopped when the context manager exits.
4325
4426
Example:
4527
server = FastMCP("test")
28+
transport = InMemoryTransport(server)
4629
47-
async with create_in_memory_transport(server) as (read, write):
48-
async with ClientSession(read, write) as session:
30+
async with transport.connect() as (read_stream, write_stream):
31+
async with ClientSession(read_stream, write_stream) as session:
4932
await session.initialize()
5033
# Use the session...
34+
35+
Or more commonly, use with Client:
36+
async with Client.from_server(server) as client:
37+
result = await client.call_tool("my_tool", {...})
5138
"""
52-
# Unwrap FastMCP to get underlying Server
53-
actual_server: Server[Any]
54-
if isinstance(server, FastMCP):
55-
actual_server = server._mcp_server # type: ignore[reportPrivateUsage]
56-
else:
57-
actual_server = server
58-
59-
async with create_client_server_memory_streams() as (client_streams, server_streams):
60-
client_read, client_write = client_streams
61-
server_read, server_write = server_streams
62-
63-
async with anyio.create_task_group() as tg:
64-
# Start server in background
65-
tg.start_soon(
66-
lambda: actual_server.run(
67-
server_read,
68-
server_write,
69-
actual_server.create_initialization_options(),
70-
raise_exceptions=raise_exceptions,
39+
40+
def __init__(
41+
self,
42+
server: Server[Any] | FastMCP,
43+
*,
44+
raise_exceptions: bool = False,
45+
) -> None:
46+
"""
47+
Initialize the in-memory transport.
48+
49+
Args:
50+
server: The MCP server to connect to (Server or FastMCP instance)
51+
raise_exceptions: Whether to raise exceptions from the server
52+
"""
53+
self._server = server
54+
self._raise_exceptions = raise_exceptions
55+
56+
@asynccontextmanager
57+
async def connect(
58+
self,
59+
) -> AsyncGenerator[
60+
tuple[
61+
MemoryObjectReceiveStream[SessionMessage | Exception],
62+
MemoryObjectSendStream[SessionMessage],
63+
],
64+
None,
65+
]:
66+
"""
67+
Connect to the server and return streams for communication.
68+
69+
Yields:
70+
A tuple of (read_stream, write_stream) for bidirectional communication
71+
"""
72+
# Unwrap FastMCP to get underlying Server
73+
actual_server: Server[Any]
74+
if isinstance(self._server, FastMCP):
75+
actual_server = self._server._mcp_server # type: ignore[reportPrivateUsage]
76+
else:
77+
actual_server = self._server
78+
79+
async with create_client_server_memory_streams() as (client_streams, server_streams):
80+
client_read, client_write = client_streams
81+
server_read, server_write = server_streams
82+
83+
async with anyio.create_task_group() as tg:
84+
# Start server in background
85+
tg.start_soon(
86+
lambda: actual_server.run(
87+
server_read,
88+
server_write,
89+
actual_server.create_initialization_options(),
90+
raise_exceptions=self._raise_exceptions,
91+
)
7192
)
72-
)
7393

74-
try:
75-
yield client_read, client_write
76-
finally:
77-
tg.cancel_scope.cancel()
94+
try:
95+
yield client_read, client_write
96+
finally:
97+
tg.cancel_scope.cancel()

src/mcp/shared/memory.py

Lines changed: 0 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,12 @@
44

55
from __future__ import annotations
66

7-
import warnings
87
from collections.abc import AsyncGenerator
98
from contextlib import asynccontextmanager
10-
from typing import Any
119

1210
import anyio
1311
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
1412

15-
import mcp.types as types
16-
from mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
17-
from mcp.server import Server
18-
from mcp.server.fastmcp import FastMCP
1913
from mcp.shared.message import SessionMessage
2014

2115
MessageStream = tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]]
@@ -44,75 +38,3 @@ async def create_client_server_memory_streams() -> AsyncGenerator[tuple[MessageS
4438
server_to_client_send,
4539
):
4640
yield client_streams, server_streams
47-
48-
49-
@asynccontextmanager
50-
async def create_connected_server_and_client_session(
51-
server: Server[Any] | FastMCP,
52-
read_timeout_seconds: float | None = None,
53-
sampling_callback: SamplingFnT | None = None,
54-
list_roots_callback: ListRootsFnT | None = None,
55-
logging_callback: LoggingFnT | None = None,
56-
message_handler: MessageHandlerFnT | None = None,
57-
client_info: types.Implementation | None = None,
58-
raise_exceptions: bool = False,
59-
elicitation_callback: ElicitationFnT | None = None,
60-
) -> AsyncGenerator[ClientSession, None]:
61-
"""Creates a ClientSession that is connected to a running MCP server.
62-
63-
.. deprecated::
64-
Use :class:`mcp.client.Client` with :meth:`Client.from_server` instead.
65-
This function will be removed in a future version.
66-
67-
Example migration::
68-
69-
# Before
70-
async with create_connected_server_and_client_session(server) as session:
71-
result = await session.call_tool("my_tool", {...})
72-
73-
# After
74-
from mcp import Client
75-
async with Client.from_server(server) as client:
76-
result = await client.call_tool("my_tool", {...})
77-
"""
78-
warnings.warn(
79-
"create_connected_server_and_client_session is deprecated. "
80-
"Use Client.from_server(server) instead.",
81-
DeprecationWarning,
82-
stacklevel=2,
83-
)
84-
85-
if isinstance(server, FastMCP): # pragma: no cover
86-
server = server._mcp_server # type: ignore[reportPrivateUsage]
87-
88-
async with create_client_server_memory_streams() as (client_streams, server_streams):
89-
client_read, client_write = client_streams
90-
server_read, server_write = server_streams
91-
92-
# Create a cancel scope for the server task
93-
async with anyio.create_task_group() as tg:
94-
tg.start_soon(
95-
lambda: server.run(
96-
server_read,
97-
server_write,
98-
server.create_initialization_options(),
99-
raise_exceptions=raise_exceptions,
100-
)
101-
)
102-
103-
try:
104-
async with ClientSession(
105-
read_stream=client_read,
106-
write_stream=client_write,
107-
read_timeout_seconds=read_timeout_seconds,
108-
sampling_callback=sampling_callback,
109-
list_roots_callback=list_roots_callback,
110-
logging_callback=logging_callback,
111-
message_handler=message_handler,
112-
client_info=client_info,
113-
elicitation_callback=elicitation_callback,
114-
) as client_session:
115-
await client_session.initialize()
116-
yield client_session
117-
finally: # pragma: no cover
118-
tg.cancel_scope.cancel()

tests/client/conftest.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,13 @@ async def patched_create_streams():
123123
yield (client_read, spy_client_write), (server_read, spy_server_write)
124124

125125
# Apply the patch for the duration of the test
126+
# Patch both locations since InMemoryTransport imports it directly
126127
with patch("mcp.shared.memory.create_client_server_memory_streams", patched_create_streams):
127-
# Return a collection with helper methods
128-
def get_spy_collection() -> StreamSpyCollection:
129-
assert client_spy is not None, "client_spy was not initialized"
130-
assert server_spy is not None, "server_spy was not initialized"
131-
return StreamSpyCollection(client_spy, server_spy)
132-
133-
yield get_spy_collection
128+
with patch("mcp.client.transports.memory.create_client_server_memory_streams", patched_create_streams):
129+
# Return a collection with helper methods
130+
def get_spy_collection() -> StreamSpyCollection:
131+
assert client_spy is not None, "client_spy was not initialized"
132+
assert server_spy is not None, "server_spy was not initialized"
133+
return StreamSpyCollection(client_spy, server_spy)
134+
135+
yield get_spy_collection

0 commit comments

Comments
 (0)