Spaces:
Running
Running
Jeremiah Lowin commited on
Update docs to reflect sync tools (#1234)
Browse files- docs/servers/tools.mdx +55 -27
docs/servers/tools.mdx
CHANGED
|
@@ -108,6 +108,61 @@ def search_products_implementation(query: str, category: str | None = None) -> l
|
|
| 108 |
</Expandable>
|
| 109 |
</ParamField>
|
| 110 |
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
### Tool Parameters
|
| 112 |
|
| 113 |
#### Type Annotations
|
|
@@ -259,33 +314,6 @@ dynamic_tool.disable()
|
|
| 259 |
dynamic_tool.enable()
|
| 260 |
```
|
| 261 |
|
| 262 |
-
### Async Tools
|
| 263 |
-
|
| 264 |
-
FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`) functions as tools.
|
| 265 |
-
|
| 266 |
-
```python
|
| 267 |
-
# Synchronous tool (suitable for CPU-bound or quick tasks)
|
| 268 |
-
@mcp.tool
|
| 269 |
-
def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
| 270 |
-
"""Calculate the distance between two coordinates."""
|
| 271 |
-
# Implementation...
|
| 272 |
-
return 42.5
|
| 273 |
-
|
| 274 |
-
# Asynchronous tool (ideal for I/O-bound operations)
|
| 275 |
-
@mcp.tool
|
| 276 |
-
async def fetch_weather(city: str) -> dict:
|
| 277 |
-
"""Retrieve current weather conditions for a city."""
|
| 278 |
-
# Use 'async def' for operations involving network calls, file I/O, etc.
|
| 279 |
-
# This prevents blocking the server while waiting for external operations.
|
| 280 |
-
async with aiohttp.ClientSession() as session:
|
| 281 |
-
async with session.get(f"https://api.example.com/weather/{city}") as response:
|
| 282 |
-
# Check response status before returning
|
| 283 |
-
response.raise_for_status()
|
| 284 |
-
return await response.json()
|
| 285 |
-
```
|
| 286 |
-
|
| 287 |
-
Use `async def` when your tool needs to perform operations that might wait for external systems (network requests, database queries, file access) to keep your server responsive.
|
| 288 |
-
|
| 289 |
### Return Values
|
| 290 |
|
| 291 |
|
|
|
|
| 108 |
</Expandable>
|
| 109 |
</ParamField>
|
| 110 |
</Card>
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
### Async and Synchronous Tools
|
| 114 |
+
|
| 115 |
+
FastMCP is an async-first framework that seamlessly supports both asynchronous (`async def`) and synchronous (`def`) functions as tools. Async tools are preferred for I/O-bound operations to keep your server responsive.
|
| 116 |
+
|
| 117 |
+
While synchronous tools work seamlessly in FastMCP, they can block the event loop during execution. For CPU-intensive or potentially blocking synchronous operations, consider alternative strategies. One approach is to use `anyio` (which FastMCP already uses internally) to wrap them as async functions, for example:
|
| 118 |
+
|
| 119 |
+
```python {1, 13}
|
| 120 |
+
import anyio
|
| 121 |
+
from fastmcp import FastMCP
|
| 122 |
+
|
| 123 |
+
mcp = FastMCP()
|
| 124 |
+
|
| 125 |
+
def cpu_intensive_task(data: str) -> str:
|
| 126 |
+
# Some heavy computation that could block the event loop
|
| 127 |
+
return processed_data
|
| 128 |
+
|
| 129 |
+
@mcp.tool
|
| 130 |
+
async def wrapped_cpu_task(data: str) -> str:
|
| 131 |
+
"""CPU-intensive task wrapped to prevent blocking."""
|
| 132 |
+
return await anyio.to_thread.run_sync(cpu_intensive_task, data)
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
Alternative approaches include using `asyncio.get_event_loop().run_in_executor()` or other threading techniques to manage blocking operations without impacting server responsiveness. For example, here's a recipe for using the `asyncer` library (not included in FastMCP) to create a decorator that wraps synchronous functions, courtesy of [@hsheth2](https://github.com/jlowin/fastmcp/issues/864#issuecomment-3103678258):
|
| 136 |
+
|
| 137 |
+
<CodeGroup>
|
| 138 |
+
```python Decorator Recipe
|
| 139 |
+
import asyncer
|
| 140 |
+
import functools
|
| 141 |
+
from typing import Callable, ParamSpec, TypeVar, Awaitable
|
| 142 |
+
|
| 143 |
+
_P = ParamSpec("_P")
|
| 144 |
+
_R = TypeVar("_R")
|
| 145 |
+
|
| 146 |
+
def make_async_background(fn: Callable[_P, _R]) -> Callable[_P, Awaitable[_R]]:
|
| 147 |
+
@functools.wraps(fn)
|
| 148 |
+
async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
| 149 |
+
return await asyncer.asyncify(fn)(*args, **kwargs)
|
| 150 |
+
|
| 151 |
+
return wrapper
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
```python Using the Decorator {6}
|
| 155 |
+
from fastmcp import FastMCP
|
| 156 |
+
|
| 157 |
+
mcp = FastMCP()
|
| 158 |
+
|
| 159 |
+
@mcp.tool()
|
| 160 |
+
@make_async_background
|
| 161 |
+
def my_tool() -> None:
|
| 162 |
+
time.sleep(5)
|
| 163 |
+
```
|
| 164 |
+
</CodeGroup>
|
| 165 |
+
|
| 166 |
### Tool Parameters
|
| 167 |
|
| 168 |
#### Type Annotations
|
|
|
|
| 314 |
dynamic_tool.enable()
|
| 315 |
```
|
| 316 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
### Return Values
|
| 318 |
|
| 319 |
|