Skip to content

Commit 8aac6b3

Browse files
Implement Client and create_in_memory_transport
- Client class wraps ClientSession with transport management - Client.from_server() provides ergonomic in-memory testing - create_in_memory_transport() reuses existing memory stream logic - All 23 Client tests pass Github-Issue:#1728
1 parent 6ec1f81 commit 8aac6b3

File tree

5 files changed

+159
-181
lines changed

5 files changed

+159
-181
lines changed

src/mcp/client/client.py

Lines changed: 118 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import logging
66
from collections.abc import AsyncGenerator
7-
from contextlib import asynccontextmanager
7+
from contextlib import AsyncExitStack, asynccontextmanager
88
from typing import Any
99

1010
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
@@ -19,6 +19,7 @@
1919
MessageHandlerFnT,
2020
SamplingFnT,
2121
)
22+
from mcp.client.transports.memory import create_in_memory_transport
2223
from mcp.server import Server
2324
from mcp.server.fastmcp import FastMCP
2425
from mcp.shared.message import SessionMessage
@@ -35,14 +36,14 @@ class Client:
3536
using different transports (in-memory, stdio, HTTP, etc.) and exposes
3637
all ClientSession functionality with simpler lifecycle management.
3738
38-
Example with in-memory transport:
39+
Example with in-memory transport (for testing):
3940
server = FastMCP("test")
4041
4142
async with Client.from_server(server) as client:
4243
tools = await client.list_tools()
4344
result = await client.call_tool("my_tool", {"arg": "value"})
4445
45-
Example with custom transport:
46+
Example with custom streams:
4647
async with Client(read_stream, write_stream) as client:
4748
await client.initialize()
4849
# Use client...
@@ -74,7 +75,18 @@ def __init__(
7475
client_info: Client implementation info to send to server
7576
elicitation_callback: Callback for handling elicitation requests
7677
"""
77-
raise NotImplementedError("Client.__init__ is not yet implemented")
78+
self._read_stream = read_stream
79+
self._write_stream = write_stream
80+
self._read_timeout_seconds = read_timeout_seconds
81+
self._sampling_callback = sampling_callback
82+
self._list_roots_callback = list_roots_callback
83+
self._logging_callback = logging_callback
84+
self._message_handler = message_handler
85+
self._client_info = client_info
86+
self._elicitation_callback = elicitation_callback
87+
88+
self._session: ClientSession | None = None
89+
self._exit_stack: AsyncExitStack | None = None
7890

7991
@classmethod
8092
@asynccontextmanager
@@ -120,16 +132,43 @@ def my_tool(arg: str) -> str:
120132
async with Client.from_server(server) as client:
121133
result = await client.call_tool("my_tool", {"arg": "value"})
122134
"""
123-
# Silence unused parameter warnings in stub
124-
_ = (server, read_timeout_seconds, sampling_callback, list_roots_callback,
125-
logging_callback, message_handler, client_info, raise_exceptions, elicitation_callback)
126-
# Stub: yield fake value, actual implementation will provide real client
127-
yield None # type: ignore[misc]
128-
raise NotImplementedError("Client.from_server is not yet implemented")
135+
async with create_in_memory_transport(
136+
server, raise_exceptions=raise_exceptions
137+
) as (read_stream, write_stream):
138+
client = cls(
139+
read_stream=read_stream,
140+
write_stream=write_stream,
141+
read_timeout_seconds=read_timeout_seconds,
142+
sampling_callback=sampling_callback,
143+
list_roots_callback=list_roots_callback,
144+
logging_callback=logging_callback,
145+
message_handler=message_handler,
146+
client_info=client_info,
147+
elicitation_callback=elicitation_callback,
148+
)
149+
async with client:
150+
await client.initialize()
151+
yield client
129152

130153
async def __aenter__(self) -> Client:
131154
"""Enter the async context manager."""
132-
raise NotImplementedError("Client.__aenter__ is not yet implemented")
155+
self._exit_stack = AsyncExitStack()
156+
await self._exit_stack.__aenter__()
157+
158+
self._session = await self._exit_stack.enter_async_context(
159+
ClientSession(
160+
read_stream=self._read_stream,
161+
write_stream=self._write_stream,
162+
read_timeout_seconds=self._read_timeout_seconds,
163+
sampling_callback=self._sampling_callback,
164+
list_roots_callback=self._list_roots_callback,
165+
logging_callback=self._logging_callback,
166+
message_handler=self._message_handler,
167+
client_info=self._client_info,
168+
elicitation_callback=self._elicitation_callback,
169+
)
170+
)
171+
return self
133172

134173
async def __aexit__(
135174
self,
@@ -138,7 +177,23 @@ async def __aexit__(
138177
exc_tb: Any,
139178
) -> None:
140179
"""Exit the async context manager."""
141-
raise NotImplementedError("Client.__aexit__ is not yet implemented")
180+
if self._exit_stack:
181+
await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
182+
self._session = None
183+
184+
@property
185+
def session(self) -> ClientSession:
186+
"""
187+
Get the underlying ClientSession.
188+
189+
This provides access to the full ClientSession API for advanced use cases.
190+
191+
Raises:
192+
RuntimeError: If accessed before entering the context manager.
193+
"""
194+
if self._session is None:
195+
raise RuntimeError("Client must be used within an async context manager")
196+
return self._session
142197

143198
async def initialize(self) -> types.InitializeResult:
144199
"""
@@ -149,7 +204,7 @@ async def initialize(self) -> types.InitializeResult:
149204
Returns:
150205
The initialization result from the server
151206
"""
152-
raise NotImplementedError("Client.initialize is not yet implemented")
207+
return await self.session.initialize()
153208

154209
def get_server_capabilities(self) -> types.ServerCapabilities | None:
155210
"""
@@ -158,11 +213,11 @@ def get_server_capabilities(self) -> types.ServerCapabilities | None:
158213
Returns:
159214
The server capabilities, or None if not yet initialized
160215
"""
161-
raise NotImplementedError("Client.get_server_capabilities is not yet implemented")
216+
return self.session.get_server_capabilities()
162217

163218
async def send_ping(self) -> types.EmptyResult:
164219
"""Send a ping request to the server."""
165-
raise NotImplementedError("Client.send_ping is not yet implemented")
220+
return await self.session.send_ping()
166221

167222
async def send_progress_notification(
168223
self,
@@ -172,11 +227,16 @@ async def send_progress_notification(
172227
message: str | None = None,
173228
) -> None:
174229
"""Send a progress notification to the server."""
175-
raise NotImplementedError("Client.send_progress_notification is not yet implemented")
230+
await self.session.send_progress_notification(
231+
progress_token=progress_token,
232+
progress=progress,
233+
total=total,
234+
message=message,
235+
)
176236

177237
async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResult:
178238
"""Set the logging level on the server."""
179-
raise NotImplementedError("Client.set_logging_level is not yet implemented")
239+
return await self.session.set_logging_level(level)
180240

181241
async def list_resources(
182242
self,
@@ -194,7 +254,12 @@ async def list_resources(
194254
Returns:
195255
List of available resources
196256
"""
197-
raise NotImplementedError("Client.list_resources is not yet implemented")
257+
if params is not None:
258+
return await self.session.list_resources(params=params)
259+
elif cursor is not None:
260+
return await self.session.list_resources(cursor)
261+
else:
262+
return await self.session.list_resources()
198263

199264
async def list_resource_templates(
200265
self,
@@ -212,7 +277,12 @@ async def list_resource_templates(
212277
Returns:
213278
List of available resource templates
214279
"""
215-
raise NotImplementedError("Client.list_resource_templates is not yet implemented")
280+
if params is not None:
281+
return await self.session.list_resource_templates(params=params)
282+
elif cursor is not None:
283+
return await self.session.list_resource_templates(cursor)
284+
else:
285+
return await self.session.list_resource_templates()
216286

217287
async def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult:
218288
"""
@@ -224,15 +294,15 @@ async def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult:
224294
Returns:
225295
The resource content
226296
"""
227-
raise NotImplementedError("Client.read_resource is not yet implemented")
297+
return await self.session.read_resource(uri)
228298

229299
async def subscribe_resource(self, uri: AnyUrl) -> types.EmptyResult:
230300
"""Subscribe to resource updates."""
231-
raise NotImplementedError("Client.subscribe_resource is not yet implemented")
301+
return await self.session.subscribe_resource(uri)
232302

233303
async def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult:
234304
"""Unsubscribe from resource updates."""
235-
raise NotImplementedError("Client.unsubscribe_resource is not yet implemented")
305+
return await self.session.unsubscribe_resource(uri)
236306

237307
async def call_tool(
238308
self,
@@ -256,7 +326,13 @@ async def call_tool(
256326
Returns:
257327
The tool result
258328
"""
259-
raise NotImplementedError("Client.call_tool is not yet implemented")
329+
return await self.session.call_tool(
330+
name=name,
331+
arguments=arguments,
332+
read_timeout_seconds=read_timeout_seconds,
333+
progress_callback=progress_callback,
334+
meta=meta,
335+
)
260336

261337
async def list_prompts(
262338
self,
@@ -274,7 +350,12 @@ async def list_prompts(
274350
Returns:
275351
List of available prompts
276352
"""
277-
raise NotImplementedError("Client.list_prompts is not yet implemented")
353+
if params is not None:
354+
return await self.session.list_prompts(params=params)
355+
elif cursor is not None:
356+
return await self.session.list_prompts(cursor)
357+
else:
358+
return await self.session.list_prompts()
278359

279360
async def get_prompt(
280361
self,
@@ -291,7 +372,7 @@ async def get_prompt(
291372
Returns:
292373
The prompt content
293374
"""
294-
raise NotImplementedError("Client.get_prompt is not yet implemented")
375+
return await self.session.get_prompt(name=name, arguments=arguments)
295376

296377
async def complete(
297378
self,
@@ -310,7 +391,11 @@ async def complete(
310391
Returns:
311392
Completion suggestions
312393
"""
313-
raise NotImplementedError("Client.complete is not yet implemented")
394+
return await self.session.complete(
395+
ref=ref,
396+
argument=argument,
397+
context_arguments=context_arguments,
398+
)
314399

315400
async def list_tools(
316401
self,
@@ -328,17 +413,13 @@ async def list_tools(
328413
Returns:
329414
List of available tools
330415
"""
331-
raise NotImplementedError("Client.list_tools is not yet implemented")
416+
if params is not None:
417+
return await self.session.list_tools(params=params)
418+
elif cursor is not None:
419+
return await self.session.list_tools(cursor)
420+
else:
421+
return await self.session.list_tools()
332422

333423
async def send_roots_list_changed(self) -> None:
334424
"""Send a notification that the roots list has changed."""
335-
raise NotImplementedError("Client.send_roots_list_changed is not yet implemented")
336-
337-
@property
338-
def session(self) -> ClientSession:
339-
"""
340-
Get the underlying ClientSession.
341-
342-
This provides access to the full ClientSession API for advanced use cases.
343-
"""
344-
raise NotImplementedError("Client.session is not yet implemented")
425+
await self.session.send_roots_list_changed()
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
"""In-memory transport implementations for MCP clients."""
1+
"""Transport implementations for MCP clients."""
22

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

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

0 commit comments

Comments
 (0)