Jeremiah Lowin commited on
Commit
70727d0
·
unverified ·
2 Parent(s): c891ae9de6e19a

Merge pull request #220 from yihuang/main

Browse files
src/fastmcp/client/client.py CHANGED
@@ -45,7 +45,8 @@ class Client:
45
  ):
46
  self.transport = infer_transport(transport)
47
  self._session: ClientSession | None = None
48
- self._session_cms: list[AbstractAsyncContextManager[ClientSession]] = []
 
49
 
50
  self._session_kwargs: SessionKwargs = {
51
  "sampling_callback": None,
@@ -85,29 +86,21 @@ class Client:
85
  return self._session is not None
86
 
87
  async def __aenter__(self):
88
- if self.is_connected():
89
- # We're already connected, no need to add None to the session_cms list
90
- return self
91
-
92
- try:
93
- session_cm = self.transport.connect_session(**self._session_kwargs)
94
- self._session_cms.append(session_cm)
95
- self._session = await self._session_cms[-1].__aenter__()
96
- return self
97
- except Exception as e:
98
- # Ensure cleanup if __aenter__ fails partially
99
- self._session = None
100
- if self._session_cms:
101
- self._session_cms.pop()
102
- raise ConnectionError(
103
- f"Failed to connect using {self.transport}: {e}"
104
- ) from e
105
 
106
  async def __aexit__(self, exc_type, exc_val, exc_tb):
107
- if self._session_cms:
108
- await self._session_cms[-1].__aexit__(exc_type, exc_val, exc_tb)
 
 
 
109
  self._session = None
110
- self._session_cms.pop()
111
 
112
  # --- MCP Client Methods ---
113
  async def ping(self) -> None:
 
45
  ):
46
  self.transport = infer_transport(transport)
47
  self._session: ClientSession | None = None
48
+ self._session_cm: AbstractAsyncContextManager[ClientSession] | None = None
49
+ self._nesting_counter: int = 0
50
 
51
  self._session_kwargs: SessionKwargs = {
52
  "sampling_callback": None,
 
86
  return self._session is not None
87
 
88
  async def __aenter__(self):
89
+ if self._nesting_counter == 0:
90
+ # create new session
91
+ self._session_cm = self.transport.connect_session(**self._session_kwargs)
92
+ self._session = await self._session_cm.__aenter__()
93
+
94
+ self._nesting_counter += 1
95
+ return self
 
 
 
 
 
 
 
 
 
 
96
 
97
  async def __aexit__(self, exc_type, exc_val, exc_tb):
98
+ self._nesting_counter -= 1
99
+
100
+ if self._nesting_counter == 0 and self._session_cm is not None:
101
+ await self._session_cm.__aexit__(exc_type, exc_val, exc_tb)
102
+ self._session_cm = None
103
  self._session = None
 
104
 
105
  # --- MCP Client Methods ---
106
  async def ping(self) -> None:
src/fastmcp/server/proxy.py CHANGED
@@ -3,7 +3,9 @@ from urllib.parse import quote
3
 
4
  import mcp.types
5
  from mcp.server.lowlevel.helper_types import ReadResourceContents
 
6
  from mcp.types import (
 
7
  BlobResourceContents,
8
  EmbeddedResource,
9
  GetPromptResult,
@@ -173,7 +175,14 @@ class FastMCPProxy(FastMCP):
173
  tools = await super().get_tools()
174
 
175
  async with self.client:
176
- for tool in await self.client.list_tools():
 
 
 
 
 
 
 
177
  tool_proxy = await ProxyTool.from_client(self.client, tool)
178
  tools[tool_proxy.name] = tool_proxy
179
 
@@ -183,7 +192,14 @@ class FastMCPProxy(FastMCP):
183
  resources = await super().get_resources()
184
 
185
  async with self.client:
186
- for resource in await self.client.list_resources():
 
 
 
 
 
 
 
187
  resource_proxy = await ProxyResource.from_client(self.client, resource)
188
  resources[str(resource_proxy.uri)] = resource_proxy
189
 
@@ -193,7 +209,14 @@ class FastMCPProxy(FastMCP):
193
  templates = await super().get_resource_templates()
194
 
195
  async with self.client:
196
- for template in await self.client.list_resource_templates():
 
 
 
 
 
 
 
197
  template_proxy = await ProxyTemplate.from_client(self.client, template)
198
  templates[template_proxy.uri_template] = template_proxy
199
 
@@ -203,7 +226,14 @@ class FastMCPProxy(FastMCP):
203
  prompts = await super().get_prompts()
204
 
205
  async with self.client:
206
- for prompt in await self.client.list_prompts():
 
 
 
 
 
 
 
207
  prompt_proxy = await ProxyPrompt.from_client(self.client, prompt)
208
  prompts[prompt_proxy.name] = prompt_proxy
209
  return prompts
 
3
 
4
  import mcp.types
5
  from mcp.server.lowlevel.helper_types import ReadResourceContents
6
+ from mcp.shared.exceptions import McpError
7
  from mcp.types import (
8
+ METHOD_NOT_FOUND,
9
  BlobResourceContents,
10
  EmbeddedResource,
11
  GetPromptResult,
 
175
  tools = await super().get_tools()
176
 
177
  async with self.client:
178
+ try:
179
+ client_tools = await self.client.list_tools()
180
+ except McpError as e:
181
+ if e.error.code == METHOD_NOT_FOUND:
182
+ client_tools = []
183
+ else:
184
+ raise e
185
+ for tool in client_tools:
186
  tool_proxy = await ProxyTool.from_client(self.client, tool)
187
  tools[tool_proxy.name] = tool_proxy
188
 
 
192
  resources = await super().get_resources()
193
 
194
  async with self.client:
195
+ try:
196
+ client_resources = await self.client.list_resources()
197
+ except McpError as e:
198
+ if e.error.code == METHOD_NOT_FOUND:
199
+ client_resources = []
200
+ else:
201
+ raise e
202
+ for resource in client_resources:
203
  resource_proxy = await ProxyResource.from_client(self.client, resource)
204
  resources[str(resource_proxy.uri)] = resource_proxy
205
 
 
209
  templates = await super().get_resource_templates()
210
 
211
  async with self.client:
212
+ try:
213
+ client_templates = await self.client.list_resource_templates()
214
+ except McpError as e:
215
+ if e.error.code == METHOD_NOT_FOUND:
216
+ client_templates = []
217
+ else:
218
+ raise e
219
+ for template in client_templates:
220
  template_proxy = await ProxyTemplate.from_client(self.client, template)
221
  templates[template_proxy.uri_template] = template_proxy
222
 
 
226
  prompts = await super().get_prompts()
227
 
228
  async with self.client:
229
+ try:
230
+ client_prompts = await self.client.list_prompts()
231
+ except McpError as e:
232
+ if e.error.code == METHOD_NOT_FOUND:
233
+ client_prompts = []
234
+ else:
235
+ raise e
236
+ for prompt in client_prompts:
237
  prompt_proxy = await ProxyPrompt.from_client(self.client, prompt)
238
  prompts[prompt_proxy.name] = prompt_proxy
239
  return prompts
tests/client/test_client.py CHANGED
@@ -1,10 +1,15 @@
 
 
1
  from typing import cast
2
 
3
  import pytest
 
 
4
  from pydantic import AnyUrl
 
5
 
6
  from fastmcp.client import Client
7
- from fastmcp.client.transports import FastMCPTransport
8
  from fastmcp.server.server import FastMCP
9
 
10
 
@@ -160,6 +165,45 @@ async def test_client_connection(fastmcp_server):
160
  assert not client.is_connected()
161
 
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  async def test_resource_template(fastmcp_server):
164
  """Test using a resource template with InMemoryClient."""
165
  client = Client(transport=FastMCPTransport(fastmcp_server))
 
1
+ import contextlib
2
+ from collections.abc import AsyncIterator
3
  from typing import cast
4
 
5
  import pytest
6
+ from mcp import ClientSession
7
+ from mcp.shared.memory import create_client_server_memory_streams
8
  from pydantic import AnyUrl
9
+ from typing_extensions import Unpack
10
 
11
  from fastmcp.client import Client
12
+ from fastmcp.client.transports import ClientTransport, FastMCPTransport, SessionKwargs
13
  from fastmcp.server.server import FastMCP
14
 
15
 
 
165
  assert not client.is_connected()
166
 
167
 
168
+ async def test_client_nested_context_manager(fastmcp_server):
169
+ """Test that the client connects and disconnects once in nested context manager."""
170
+
171
+ class MockTransport(ClientTransport):
172
+ def __init__(self):
173
+ self._connected = False
174
+
175
+ @contextlib.asynccontextmanager
176
+ async def connect_session(
177
+ self,
178
+ **session_kwargs: Unpack[SessionKwargs],
179
+ ) -> AsyncIterator[ClientSession]:
180
+ assert not self._connected, "Transport is connected multiple times"
181
+ self._connected = True
182
+ async with create_client_server_memory_streams() as (
183
+ _,
184
+ server_streams,
185
+ ):
186
+ yield ClientSession(*server_streams)
187
+
188
+ client = Client(transport=MockTransport())
189
+
190
+ # Before connection
191
+ assert not client.is_connected()
192
+
193
+ # During connection
194
+ async with client:
195
+ assert client.is_connected()
196
+
197
+ async with client:
198
+ assert client.is_connected()
199
+
200
+ async with client:
201
+ assert client.is_connected()
202
+
203
+ # After connection
204
+ assert not client.is_connected()
205
+
206
+
207
  async def test_resource_template(fastmcp_server):
208
  """Test using a resource template with InMemoryClient."""
209
  client = Client(transport=FastMCPTransport(fastmcp_server))