Jeremiah Lowin commited on
Commit
bb68df7
·
unverified ·
2 Parent(s): eea96ed7615e10

Merge pull request #605 from davenpi/fix-unreachable-mounted-servers

Browse files
src/fastmcp/server/server.py CHANGED
@@ -257,9 +257,15 @@ class FastMCP(Generic[LifespanResultT]):
257
  """Get all registered tools, indexed by registered key."""
258
  if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
259
  tools: dict[str, Tool] = {}
260
- for server in self._mounted_servers.values():
261
- server_tools = await server.get_tools()
262
- tools.update(server_tools)
 
 
 
 
 
 
263
  tools.update(self._tool_manager.get_tools())
264
  self._cache.set("tools", tools)
265
  return tools
@@ -268,9 +274,15 @@ class FastMCP(Generic[LifespanResultT]):
268
  """Get all registered resources, indexed by registered key."""
269
  if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
270
  resources: dict[str, Resource] = {}
271
- for server in self._mounted_servers.values():
272
- server_resources = await server.get_resources()
273
- resources.update(server_resources)
 
 
 
 
 
 
274
  resources.update(self._resource_manager.get_resources())
275
  self._cache.set("resources", resources)
276
  return resources
@@ -281,9 +293,16 @@ class FastMCP(Generic[LifespanResultT]):
281
  templates := self._cache.get("resource_templates")
282
  ) is self._cache.NOT_FOUND:
283
  templates: dict[str, ResourceTemplate] = {}
284
- for server in self._mounted_servers.values():
285
- server_templates = await server.get_resource_templates()
286
- templates.update(server_templates)
 
 
 
 
 
 
 
287
  templates.update(self._resource_manager.get_templates())
288
  self._cache.set("resource_templates", templates)
289
  return templates
@@ -294,9 +313,15 @@ class FastMCP(Generic[LifespanResultT]):
294
  """
295
  if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
296
  prompts: dict[str, Prompt] = {}
297
- for server in self._mounted_servers.values():
298
- server_prompts = await server.get_prompts()
299
- prompts.update(server_prompts)
 
 
 
 
 
 
300
  prompts.update(self._prompt_manager.get_prompts())
301
  self._cache.set("prompts", prompts)
302
  return prompts
 
257
  """Get all registered tools, indexed by registered key."""
258
  if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
259
  tools: dict[str, Tool] = {}
260
+ for prefix, server in self._mounted_servers.items():
261
+ try:
262
+ server_tools = await server.get_tools()
263
+ tools.update(server_tools)
264
+ except Exception as e:
265
+ logger.warning(
266
+ f"Failed to get tools from mounted server '{prefix}': {e}"
267
+ )
268
+ continue
269
  tools.update(self._tool_manager.get_tools())
270
  self._cache.set("tools", tools)
271
  return tools
 
274
  """Get all registered resources, indexed by registered key."""
275
  if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
276
  resources: dict[str, Resource] = {}
277
+ for prefix, server in self._mounted_servers.items():
278
+ try:
279
+ server_resources = await server.get_resources()
280
+ resources.update(server_resources)
281
+ except Exception as e:
282
+ logger.warning(
283
+ f"Failed to get resources from mounted server '{prefix}': {e}"
284
+ )
285
+ continue
286
  resources.update(self._resource_manager.get_resources())
287
  self._cache.set("resources", resources)
288
  return resources
 
293
  templates := self._cache.get("resource_templates")
294
  ) is self._cache.NOT_FOUND:
295
  templates: dict[str, ResourceTemplate] = {}
296
+ for prefix, server in self._mounted_servers.items():
297
+ try:
298
+ server_templates = await server.get_resource_templates()
299
+ templates.update(server_templates)
300
+ except Exception as e:
301
+ logger.warning(
302
+ "Failed to get resource templates from mounted server "
303
+ f"'{prefix}': {e}"
304
+ )
305
+ continue
306
  templates.update(self._resource_manager.get_templates())
307
  self._cache.set("resource_templates", templates)
308
  return templates
 
313
  """
314
  if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
315
  prompts: dict[str, Prompt] = {}
316
+ for prefix, server in self._mounted_servers.items():
317
+ try:
318
+ server_prompts = await server.get_prompts()
319
+ prompts.update(server_prompts)
320
+ except Exception as e:
321
+ logger.warning(
322
+ f"Failed to get prompts from mounted server '{prefix}': {e}"
323
+ )
324
+ continue
325
  prompts.update(self._prompt_manager.get_prompts())
326
  self._cache.set("prompts", prompts)
327
  return prompts
tests/server/test_mount.py CHANGED
@@ -1,4 +1,5 @@
1
  import json
 
2
  from contextlib import asynccontextmanager
3
 
4
  import pytest
@@ -7,7 +8,7 @@ from mcp.types import TextContent, TextResourceContents
7
 
8
  from fastmcp import FastMCP
9
  from fastmcp.client import Client
10
- from fastmcp.client.transports import FastMCPTransport
11
  from fastmcp.exceptions import NotFoundError
12
  from fastmcp.server.proxy import FastMCPProxy
13
 
@@ -182,6 +183,80 @@ class TestMultipleServerMount:
182
  # Second app's tool should be accessible
183
  assert "api_second_tool" in tools
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
  class TestDynamicChanges:
187
  """Test that changes to mounted servers are reflected dynamically."""
 
1
  import json
2
+ import sys
3
  from contextlib import asynccontextmanager
4
 
5
  import pytest
 
8
 
9
  from fastmcp import FastMCP
10
  from fastmcp.client import Client
11
+ from fastmcp.client.transports import FastMCPTransport, SSETransport
12
  from fastmcp.exceptions import NotFoundError
13
  from fastmcp.server.proxy import FastMCPProxy
14
 
 
183
  # Second app's tool should be accessible
184
  assert "api_second_tool" in tools
185
 
186
+ @pytest.mark.skipif(
187
+ sys.platform == "win32", reason="Windows asyncio networking timeouts."
188
+ )
189
+ async def test_mount_with_unreachable_proxy_servers(self, caplog):
190
+ """Test graceful handling when multiple mounted servers fail to connect."""
191
+
192
+ main_app = FastMCP("MainApp")
193
+ working_app = FastMCP("WorkingApp")
194
+
195
+ @working_app.tool()
196
+ def working_tool() -> str:
197
+ return "Working tool"
198
+
199
+ @working_app.resource(uri="working://data")
200
+ def working_resource():
201
+ return "Working resource"
202
+
203
+ @working_app.prompt()
204
+ def working_prompt() -> str:
205
+ return "Working prompt"
206
+
207
+ # Mount the working server
208
+ main_app.mount("working", working_app)
209
+
210
+ # Use an unreachable port
211
+ unreachable_client = Client(
212
+ transport=SSETransport("http://127.0.0.1:99999/sse")
213
+ )
214
+
215
+ # Create a proxy server that will fail to connect
216
+ unreachable_proxy = FastMCP.as_proxy(unreachable_client)
217
+
218
+ # Mount the unreachable proxy
219
+ main_app.mount("unreachable", unreachable_proxy)
220
+
221
+ # All object types should work from working server despite unreachable proxy
222
+ async with Client(main_app) as client:
223
+ # Test tools
224
+ tools = await client.list_tools()
225
+ tool_names = [tool.name for tool in tools]
226
+ assert "working_working_tool" in tool_names
227
+
228
+ # Test calling a tool
229
+ result = await client.call_tool("working_working_tool", {})
230
+ assert isinstance(result[0], TextContent)
231
+ assert result[0].text == "Working tool"
232
+
233
+ # Test resources
234
+ resources = await client.list_resources()
235
+ resource_uris = [str(resource.uri) for resource in resources]
236
+ assert "working://working/data" in resource_uris
237
+
238
+ # Test prompts
239
+ prompts = await client.list_prompts()
240
+ prompt_names = [prompt.name for prompt in prompts]
241
+ assert "working_working_prompt" in prompt_names
242
+
243
+ # Verify that warnings were logged for the unreachable server
244
+ warning_messages = [
245
+ record.message for record in caplog.records if record.levelname == "WARNING"
246
+ ]
247
+ assert any(
248
+ "Failed to get tools from mounted server 'unreachable'" in msg
249
+ for msg in warning_messages
250
+ )
251
+ assert any(
252
+ "Failed to get resources from mounted server 'unreachable'" in msg
253
+ for msg in warning_messages
254
+ )
255
+ assert any(
256
+ "Failed to get prompts from mounted server 'unreachable'" in msg
257
+ for msg in warning_messages
258
+ )
259
+
260
 
261
  class TestDynamicChanges:
262
  """Test that changes to mounted servers are reflected dynamically."""