Skip to content

Commit ae0ec16

Browse files
Nikita BelliniNikita Bellini
authored andcommitted
Implemented runtime tools
1 parent 7bc190b commit ae0ec16

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-0
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,36 @@ def get_temperature(city: str) -> float:
371371
# Returns: {"result": 22.5}
372372
```
373373

374+
#### Runtime tools
375+
376+
It is also possible to define tools at runtime, allowing for dynamic modification of the available tools, for example, to display specific tools based on the user making the request. This is done passing a function dedicated to the tools generation:
377+
378+
```python
379+
from mcp.server.fastmcp import FastMCP
380+
from mcp.server.fastmcp.tools.base import Tool
381+
382+
383+
async def runtime_mcp_tools_generator() -> list[Tool]:
384+
"""Generate runtime tools."""
385+
386+
def list_cities() -> list[str]:
387+
"""Get a list of cities"""
388+
return ["London", "Paris", "Tokyo"]
389+
# Returns: {"result": ["London", "Paris", "Tokyo"]}
390+
391+
def get_temperature(city: str) -> float:
392+
"""Get temperature as a simple float"""
393+
return 22.5
394+
# Returns: {"result": 22.5}
395+
396+
return [Tool.from_function(list_cities), Tool.from_function(get_temperature)]
397+
398+
399+
mcp = FastMCP(
400+
name="Weather Service", runtime_mcp_tools_generator=runtime_mcp_tools_generator
401+
)
402+
```
403+
374404
### Prompts
375405

376406
Prompts are reusable templates that help LLMs interact with your server effectively:

src/mcp/server/fastmcp/server.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def __init__(
140140
event_store: EventStore | None = None,
141141
*,
142142
tools: list[Tool] | None = None,
143+
runtime_mcp_tools_generator: Callable[[], Awaitable[list[Tool]]] | None = None,
143144
**settings: Any,
144145
):
145146
self.settings = Settings(**settings)
@@ -172,6 +173,7 @@ def __init__(
172173
self._custom_starlette_routes: list[Route] = []
173174
self.dependencies = self.settings.dependencies
174175
self._session_manager: StreamableHTTPSessionManager | None = None
176+
self._runtime_mcp_tools_generator = runtime_mcp_tools_generator
175177

176178
# Set up MCP protocol handlers
177179
self._setup_handlers()
@@ -245,6 +247,14 @@ def _setup_handlers(self) -> None:
245247
async def list_tools(self) -> list[MCPTool]:
246248
"""List all available tools."""
247249
tools = self._tool_manager.list_tools()
250+
251+
if self._runtime_mcp_tools_generator:
252+
tools.extend(await self._runtime_mcp_tools_generator())
253+
254+
# Check if there are no duplicated tools
255+
if len(tools) != len(set([tool.name for tool in tools])):
256+
raise Exception("There are duplicated tools. Check the for tools with the same name both static and generated at runtime.")
257+
248258
return [
249259
MCPTool(
250260
name=info.name,
@@ -271,6 +281,15 @@ def get_context(self) -> Context[ServerSession, object, Request]:
271281
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Sequence[ContentBlock] | dict[str, Any]:
272282
"""Call a tool by name with arguments."""
273283
context = self.get_context()
284+
285+
# Try to call a runtime tool
286+
if self._runtime_mcp_tools_generator:
287+
runtime_tools = await self._runtime_mcp_tools_generator()
288+
for tool in runtime_tools:
289+
if tool.name == name:
290+
return await tool.run(arguments=arguments, context=context, convert_result=True)
291+
292+
# Call a static tool if the runtime tool has not been called
274293
return await self._tool_manager.call_tool(name, arguments, context=context, convert_result=True)
275294

276295
async def list_resources(self) -> list[MCPResource]:
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Integration tests for runtime tools functionality."""
2+
3+
import pytest
4+
5+
from mcp.server.fastmcp import FastMCP
6+
from mcp.shared.memory import create_connected_server_and_client_session
7+
from mcp.server.fastmcp.tools.base import Tool
8+
from mcp.types import TextContent
9+
10+
@pytest.mark.anyio
11+
async def test_runtime_tools():
12+
"""Test that runtime tools work correctly."""
13+
async def runtime_mcp_tools_generator() -> list[Tool]:
14+
"""Generate runtime tools."""
15+
def runtime_tool_1(message: str):
16+
return message
17+
18+
def runtime_tool_2(message: str):
19+
return message
20+
21+
return [
22+
Tool.from_function(runtime_tool_1),
23+
Tool.from_function(runtime_tool_2)
24+
]
25+
26+
# Create server with various tool configurations, both static and runtime
27+
mcp = FastMCP(name="RuntimeToolsTestServer", runtime_mcp_tools_generator=runtime_mcp_tools_generator)
28+
29+
# Static tool
30+
@mcp.tool(description="Static tool")
31+
def static_tool(message: str) -> str:
32+
return message
33+
34+
# Start server and connect client
35+
async with create_connected_server_and_client_session(mcp._mcp_server) as client:
36+
await client.initialize()
37+
38+
# List tools
39+
tools_result = await client.list_tools()
40+
tool_names = {tool.name: tool for tool in tools_result.tools}
41+
42+
# Verify both tools
43+
assert "static_tool" in tool_names
44+
assert "runtime_tool_1" in tool_names
45+
assert "runtime_tool_2" in tool_names
46+
47+
# Check static tool
48+
result = await client.call_tool("static_tool", {"message": "This is a test"})
49+
assert len(result.content) == 1
50+
content = result.content[0]
51+
assert isinstance(content, TextContent)
52+
assert content.text == "This is a test"
53+
54+
# Check runtime tool 1
55+
result = await client.call_tool("runtime_tool_1", {"message": "This is a test"})
56+
assert len(result.content) == 1
57+
content = result.content[0]
58+
assert isinstance(content, TextContent)
59+
assert content.text == "This is a test"
60+
61+
# Check runtime tool 2
62+
result = await client.call_tool("runtime_tool_2", {"message": "This is a test"})
63+
assert len(result.content) == 1
64+
content = result.content[0]
65+
assert isinstance(content, TextContent)
66+
assert content.text == "This is a test"
67+
68+
# Check non existing tool
69+
result = await client.call_tool("non_existing_tool", {"message": "This is a test"})
70+
assert len(result.content) == 1
71+
content = result.content[0]
72+
assert isinstance(content, TextContent)
73+
assert content.text == "Unknown tool: non_existing_tool"

0 commit comments

Comments
 (0)