Jeremiah Lowin commited on
Commit
92fd239
·
1 Parent(s): 8544607

Update in memory client to use FastMCP

Browse files
src/fastmcp/client/memory.py CHANGED
@@ -1,11 +1,11 @@
1
  import contextlib
2
- from typing import Any, TypeVar
3
 
4
- from mcp.server import Server
5
  from mcp.shared.memory import create_connected_server_and_client_session
6
  from typing_extensions import Unpack
7
 
8
  from fastmcp.client.base import BaseClient, ClientKwargs
 
9
 
10
  T = TypeVar("T")
11
 
@@ -19,33 +19,29 @@ class InMemoryClient(BaseClient):
19
 
20
  def __init__(
21
  self,
22
- server: Server[Any],
23
- raise_exceptions: bool = False,
24
  **kwargs: Unpack[ClientKwargs],
25
  ):
26
  """Initialize an InMemoryClient that connects to an in-memory MCP server.
27
 
28
  Args:
29
- server: The MCP server instance to connect to
30
- raise_exceptions: Whether to raise exceptions from the server
31
  **kwargs: Additional arguments for BaseClient
32
  """
33
  super().__init__(**kwargs)
34
  self.server = server
35
- self.raise_exceptions = raise_exceptions
36
  self._cm_session = None
37
 
38
  @contextlib.asynccontextmanager
39
  async def _connect(self):
40
  """Set up in-memory connection and session"""
41
  self._cm_session = create_connected_server_and_client_session(
42
- server=self.server,
43
  read_timeout_seconds=self._read_timeout_seconds,
44
  sampling_callback=self._sampling_callback,
45
  list_roots_callback=self._list_roots_callback,
46
  logging_callback=self._logging_callback,
47
  message_handler=self._message_handler,
48
- raise_exceptions=self.raise_exceptions,
49
  )
50
 
51
  async with self._cm_session as session:
 
1
  import contextlib
2
+ from typing import TypeVar
3
 
 
4
  from mcp.shared.memory import create_connected_server_and_client_session
5
  from typing_extensions import Unpack
6
 
7
  from fastmcp.client.base import BaseClient, ClientKwargs
8
+ from fastmcp.server.server import FastMCP
9
 
10
  T = TypeVar("T")
11
 
 
19
 
20
  def __init__(
21
  self,
22
+ server: FastMCP,
 
23
  **kwargs: Unpack[ClientKwargs],
24
  ):
25
  """Initialize an InMemoryClient that connects to an in-memory MCP server.
26
 
27
  Args:
28
+ server: The FastMCP instance to connect to
 
29
  **kwargs: Additional arguments for BaseClient
30
  """
31
  super().__init__(**kwargs)
32
  self.server = server
 
33
  self._cm_session = None
34
 
35
  @contextlib.asynccontextmanager
36
  async def _connect(self):
37
  """Set up in-memory connection and session"""
38
  self._cm_session = create_connected_server_and_client_session(
39
+ server=self.server._mcp_server,
40
  read_timeout_seconds=self._read_timeout_seconds,
41
  sampling_callback=self._sampling_callback,
42
  list_roots_callback=self._list_roots_callback,
43
  logging_callback=self._logging_callback,
44
  message_handler=self._message_handler,
 
45
  )
46
 
47
  async with self._cm_session as session:
tests/client/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Client tests package."""
tests/client/test_memory_client.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import cast
2
+
3
+ import pytest
4
+ from pydantic import AnyUrl
5
+
6
+ from fastmcp.client.memory import InMemoryClient
7
+ from fastmcp.server.server import FastMCP
8
+
9
+
10
+ class _TestException(Exception):
11
+ """Test exception for testing raise_exceptions behavior."""
12
+
13
+ pass
14
+
15
+
16
+ @pytest.fixture
17
+ def fastmcp_server():
18
+ """Fixture that creates a FastMCP server with tools, resources, and prompts."""
19
+ server = FastMCP("TestServer")
20
+
21
+ # Add a tool
22
+ @server.tool()
23
+ def greet(name: str) -> str:
24
+ """Greet someone by name."""
25
+ return f"Hello, {name}!"
26
+
27
+ # Add a tool that raises an exception
28
+ @server.tool()
29
+ def error_tool() -> str:
30
+ """A tool that always raises an exception."""
31
+ raise _TestException("Deliberate test exception")
32
+
33
+ # Add a resource
34
+ @server.resource(uri="data://users")
35
+ async def get_users():
36
+ return ["Alice", "Bob", "Charlie"]
37
+
38
+ # Add a resource template
39
+ @server.resource(uri="data://user/{user_id}")
40
+ async def get_user(user_id: str):
41
+ return {"id": user_id, "name": f"User {user_id}", "active": True}
42
+
43
+ # Add a prompt
44
+ @server.prompt()
45
+ def welcome(name: str) -> str:
46
+ return f"Welcome to FastMCP, {name}!"
47
+
48
+ return server
49
+
50
+
51
+ @pytest.mark.asyncio
52
+ async def test_list_tools(fastmcp_server):
53
+ """Test listing tools with InMemoryClient."""
54
+ client = InMemoryClient(server=fastmcp_server)
55
+
56
+ async with client:
57
+ result = await client.list_tools()
58
+
59
+ # Check that our tools are available
60
+ assert len(result.tools) == 2
61
+ tool_names = [tool.name for tool in result.tools]
62
+ assert "greet" in tool_names
63
+ assert "error_tool" in tool_names
64
+
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_call_tool(fastmcp_server):
68
+ """Test calling a tool with InMemoryClient."""
69
+ client = InMemoryClient(server=fastmcp_server)
70
+
71
+ async with client:
72
+ result = await client.call_tool("greet", {"name": "World"})
73
+
74
+ # The result content should contain our greeting
75
+ content_str = str(result.content[0])
76
+ assert "Hello, World!" in content_str
77
+
78
+
79
+ @pytest.mark.asyncio
80
+ async def test_list_resources(fastmcp_server):
81
+ """Test listing resources with InMemoryClient."""
82
+ client = InMemoryClient(server=fastmcp_server)
83
+
84
+ async with client:
85
+ result = await client.list_resources()
86
+
87
+ # Check that our resource is available
88
+ assert len(result.resources) == 1
89
+ assert str(result.resources[0].uri) == "data://users"
90
+
91
+
92
+ @pytest.mark.asyncio
93
+ async def test_list_prompts(fastmcp_server):
94
+ """Test listing prompts with InMemoryClient."""
95
+ client = InMemoryClient(server=fastmcp_server)
96
+
97
+ async with client:
98
+ result = await client.list_prompts()
99
+
100
+ # Check that our prompt is available
101
+ assert len(result.prompts) == 1
102
+ assert result.prompts[0].name == "welcome"
103
+
104
+
105
+ @pytest.mark.asyncio
106
+ async def test_get_prompt(fastmcp_server):
107
+ """Test getting a prompt with InMemoryClient."""
108
+ client = InMemoryClient(server=fastmcp_server)
109
+
110
+ async with client:
111
+ result = await client.get_prompt("welcome", {"name": "Developer"})
112
+
113
+ # The result should contain our welcome message
114
+ result_str = str(result)
115
+ assert "Welcome to FastMCP, Developer!" in result_str
116
+
117
+
118
+ @pytest.mark.asyncio
119
+ async def test_read_resource(fastmcp_server):
120
+ """Test reading a resource with InMemoryClient."""
121
+ client = InMemoryClient(server=fastmcp_server)
122
+
123
+ async with client:
124
+ # Use the URI from the resource we know exists in our server
125
+ uri = cast(
126
+ AnyUrl, "data://users"
127
+ ) # Use cast for type hint only, the URI is valid
128
+ result = await client.read_resource(uri)
129
+
130
+ # The contents should include our user list
131
+ contents_str = str(result.contents[0])
132
+ assert "Alice" in contents_str
133
+ assert "Bob" in contents_str
134
+ assert "Charlie" in contents_str
135
+
136
+
137
+ @pytest.mark.asyncio
138
+ async def test_client_connection(fastmcp_server):
139
+ """Test that the client connects and disconnects properly."""
140
+ client = InMemoryClient(server=fastmcp_server)
141
+
142
+ # Before connection
143
+ assert not client.is_connected()
144
+
145
+ # During connection
146
+ async with client:
147
+ assert client.is_connected()
148
+
149
+ # After connection
150
+ assert not client.is_connected()
151
+
152
+
153
+ @pytest.mark.asyncio
154
+ async def test_resource_template(fastmcp_server):
155
+ """Test using a resource template with InMemoryClient."""
156
+ client = InMemoryClient(server=fastmcp_server)
157
+
158
+ async with client:
159
+ # First, list templates
160
+ result = await client.list_resource_templates()
161
+
162
+ # Check that our template is available
163
+ assert len(result.resourceTemplates) == 1
164
+ assert "data://user/{user_id}" in result.resourceTemplates[0].uriTemplate
165
+
166
+ # Now use the template with a specific user_id
167
+ uri = cast(AnyUrl, "data://user/123")
168
+ result = await client.read_resource(uri)
169
+
170
+ # Check the content matches what we expect for the provided user_id
171
+ content_str = str(result.contents[0])
172
+ assert '"id": "123"' in content_str
173
+ assert '"name": "User 123"' in content_str
174
+ assert '"active": true' in content_str