Jeremiah Lowin commited on
Commit
736b52b
·
1 Parent(s): 72c9ff1

Add `get_tools()` and update tests to include proxy tools

Browse files
src/fastmcp/server/server.py CHANGED
@@ -185,7 +185,12 @@ class FastMCP(Generic[LifespanResultT]):
185
  self._mcp_server.get_prompt()(self._mcp_get_prompt)
186
  self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
187
 
 
 
 
 
188
  def list_tools(self) -> list[Tool]:
 
189
  return self._tool_manager.list_tools()
190
 
191
  async def _mcp_list_tools(self) -> list[MCPTool]:
 
185
  self._mcp_server.get_prompt()(self._mcp_get_prompt)
186
  self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
187
 
188
+ def get_tools(self) -> dict[str, Tool]:
189
+ """Get all registered tools, keyed by registered name."""
190
+ return self._tool_manager.get_tools()
191
+
192
  def list_tools(self) -> list[Tool]:
193
+ """List all registered tools."""
194
  return self._tool_manager.list_tools()
195
 
196
  async def _mcp_list_tools(self) -> list[MCPTool]:
src/fastmcp/tools/tool_manager.py CHANGED
@@ -29,9 +29,13 @@ class ToolManager:
29
  """Get tool by name."""
30
  return self._tools.get(name)
31
 
 
 
 
 
32
  def list_tools(self) -> list[Tool]:
33
  """List all registered tools."""
34
- return list(self._tools.values())
35
 
36
  def list_mcp_tools(self) -> list[MCPTool]:
37
  """List all registered tools in the format expected by the low-level MCP server."""
 
29
  """Get tool by name."""
30
  return self._tools.get(name)
31
 
32
+ def get_tools(self) -> dict[str, Tool]:
33
+ """Get all registered tools, keyed by registered name."""
34
+ return self._tools
35
+
36
  def list_tools(self) -> list[Tool]:
37
  """List all registered tools."""
38
+ return list(self.get_tools().values())
39
 
40
  def list_mcp_tools(self) -> list[MCPTool]:
41
  """List all registered tools in the format expected by the low-level MCP server."""
tests/server/test_mount.py CHANGED
@@ -1,6 +1,7 @@
1
  import contextlib
2
 
3
  import pytest
 
4
 
5
  from fastmcp.server.server import FastMCP
6
 
@@ -233,33 +234,36 @@ async def test_mount_lifespan():
233
  ]
234
 
235
 
236
- async def test_mount_with_proxy_tools():
237
- """Test mounting with tools that have custom names (proxy tools)."""
238
- # Create apps
239
  main_app = FastMCP("MainApp")
240
  api_app = FastMCP("APIApp")
241
 
242
- # Create a tool function
243
  def fetch_data(query: str) -> str:
244
  return f"Data for query: {query}"
245
 
246
- # Add the tool to the API app with a custom name
247
  api_app.add_tool(fetch_data, name="get_data")
248
-
249
- # Verify the tool is registered with the custom name in the source app
250
- assert api_app._tool_manager.get_tool("get_data") is not None
251
-
252
- # Mount the API app to the main app
253
  main_app.mount("api", api_app)
254
 
255
- # Verify the tool was imported with the prefixed custom name
256
  tool = main_app._tool_manager.get_tool("api_get_data")
257
  assert tool is not None
258
 
259
- # The internal function name should be preserved
260
  assert tool.fn.__name__ == "fetch_data"
261
 
262
- # The tool should be callable through the mounted name
 
 
 
 
 
 
 
 
 
 
 
263
  context = main_app.get_context()
264
  result = await main_app._tool_manager.call_tool(
265
  "api_get_data", {"query": "test"}, context=context
@@ -267,42 +271,78 @@ async def test_mount_with_proxy_tools():
267
  assert result == "Data for query: test"
268
 
269
 
270
- async def test_mount_nested_prefixed_tools():
271
- """Test mounting tools with multiple layers of prefixes."""
272
- # Create apps
273
- main_app = FastMCP("MainApp")
274
  service_app = FastMCP("ServiceApp")
275
  provider_app = FastMCP("ProviderApp")
276
 
277
- # Create a tool function
278
  def calculate_value(input: int) -> int:
279
  return input * 2
280
 
281
- # Add the tool to the provider app with a custom name
282
  provider_app.add_tool(calculate_value, name="compute")
 
283
 
284
- # The provider has a tool registered with a custom name
285
- assert provider_app._tool_manager.get_tool("compute") is not None
 
 
286
 
287
- # First mount: Mount the provider app to the service app
288
- service_app.mount("provider", provider_app)
289
 
290
- # Verify the tool is accessible in the service app with the first prefix
291
- assert service_app._tool_manager.get_tool("provider_compute") is not None
 
 
 
 
 
 
292
 
293
- # Second mount: Mount the service app to the main app
 
294
  main_app.mount("service", service_app)
295
 
296
- # Verify the tool is accessible in the main app with both prefixes
297
- nested_tool = main_app._tool_manager.get_tool("service_provider_compute")
298
- assert nested_tool is not None
299
 
300
- # The internal function name should still be preserved after multiple mounts
301
- assert nested_tool.fn.__name__ == "calculate_value"
302
 
303
- # The tool should be callable through the fully-qualified name
304
- context = main_app.get_context()
 
 
 
 
 
 
 
 
 
 
 
305
  result = await main_app._tool_manager.call_tool(
306
- "service_provider_compute", {"input": 21}, context=context
307
  )
308
  assert result == 42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import contextlib
2
 
3
  import pytest
4
+ from mcp.types import TextContent
5
 
6
  from fastmcp.server.server import FastMCP
7
 
 
234
  ]
235
 
236
 
237
+ async def test_tool_custom_name_preserved_when_mounted():
238
+ """Test that a tool's custom name is preserved when mounted."""
 
239
  main_app = FastMCP("MainApp")
240
  api_app = FastMCP("APIApp")
241
 
 
242
  def fetch_data(query: str) -> str:
243
  return f"Data for query: {query}"
244
 
 
245
  api_app.add_tool(fetch_data, name="get_data")
 
 
 
 
 
246
  main_app.mount("api", api_app)
247
 
248
+ # Check that the tool is accessible by its prefixed name
249
  tool = main_app._tool_manager.get_tool("api_get_data")
250
  assert tool is not None
251
 
252
+ # Check that the function name is preserved
253
  assert tool.fn.__name__ == "fetch_data"
254
 
255
+
256
+ async def test_call_mounted_custom_named_tool():
257
+ """Test calling a mounted tool with a custom name."""
258
+ main_app = FastMCP("MainApp")
259
+ api_app = FastMCP("APIApp")
260
+
261
+ def fetch_data(query: str) -> str:
262
+ return f"Data for query: {query}"
263
+
264
+ api_app.add_tool(fetch_data, name="get_data")
265
+ main_app.mount("api", api_app)
266
+
267
  context = main_app.get_context()
268
  result = await main_app._tool_manager.call_tool(
269
  "api_get_data", {"query": "test"}, context=context
 
271
  assert result == "Data for query: test"
272
 
273
 
274
+ async def test_first_level_mounting_with_custom_name():
275
+ """Test that a tool with a custom name is correctly mounted at the first level."""
 
 
276
  service_app = FastMCP("ServiceApp")
277
  provider_app = FastMCP("ProviderApp")
278
 
 
279
  def calculate_value(input: int) -> int:
280
  return input * 2
281
 
 
282
  provider_app.add_tool(calculate_value, name="compute")
283
+ service_app.mount("provider", provider_app)
284
 
285
+ # Tool is accessible in the service app with the first prefix
286
+ tool = service_app._tool_manager.get_tool("provider_compute")
287
+ assert tool is not None
288
+ assert tool.fn.__name__ == "calculate_value"
289
 
 
 
290
 
291
+ async def test_nested_mounting_preserves_prefixes():
292
+ """Test that mounting a previously mounted app preserves prefixes."""
293
+ main_app = FastMCP("MainApp")
294
+ service_app = FastMCP("ServiceApp")
295
+ provider_app = FastMCP("ProviderApp")
296
+
297
+ def calculate_value(input: int) -> int:
298
+ return input * 2
299
 
300
+ provider_app.add_tool(calculate_value, name="compute")
301
+ service_app.mount("provider", provider_app)
302
  main_app.mount("service", service_app)
303
 
304
+ # Tool is accessible in the main app with both prefixes
305
+ tool = main_app._tool_manager.get_tool("service_provider_compute")
306
+ assert tool is not None
307
 
 
 
308
 
309
+ async def test_call_nested_mounted_tool():
310
+ """Test calling a tool through multiple levels of mounting."""
311
+ main_app = FastMCP("MainApp")
312
+ service_app = FastMCP("ServiceApp")
313
+ provider_app = FastMCP("ProviderApp")
314
+
315
+ def calculate_value(input: int) -> int:
316
+ return input * 2
317
+
318
+ provider_app.add_tool(calculate_value, name="compute")
319
+ service_app.mount("provider", provider_app)
320
+ main_app.mount("service", service_app)
321
+
322
  result = await main_app._tool_manager.call_tool(
323
+ "service_provider_compute", {"input": 21}
324
  )
325
  assert result == 42
326
+
327
+
328
+ async def test_mount_with_proxy_tools():
329
+ """
330
+ Test mounting with tools that have custom names (proxy tools).
331
+
332
+ This tests that the tool's name doesn't change even though the registered
333
+ name does, which is important because we need to forward that name to the
334
+ proxy server correctly.
335
+ """
336
+ # Create apps
337
+ main_app = FastMCP("MainApp")
338
+ api_app = FastMCP("APIApp")
339
+
340
+ @api_app.tool()
341
+ def get_data(query: str) -> str:
342
+ return f"Data for query: {query}"
343
+
344
+ main_app.mount("api", await FastMCP.as_proxy(api_app))
345
+
346
+ result = await main_app.call_tool("api_get_data", {"query": "test"})
347
+ assert isinstance(result[0], TextContent)
348
+ assert result[0].text == "Data for query: test"