Skip to content

Commit 0cb3b26

Browse files
authored
Merge pull request #157 from micpst/async-resources
feat: add async resources for FastMCP
2 parents 73c62c5 + f5f19b2 commit 0cb3b26

File tree

3 files changed

+33
-9
lines changed

3 files changed

+33
-9
lines changed

src/mcp/server/fastmcp/resources/types.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Concrete resource implementations."""
22

3+
import inspect
34
import json
45
from collections.abc import Callable
56
from pathlib import Path
@@ -53,7 +54,9 @@ class FunctionResource(Resource):
5354
async def read(self) -> str | bytes:
5455
"""Read the resource by calling the wrapped function."""
5556
try:
56-
result = self.fn()
57+
result = (
58+
await self.fn() if inspect.iscoroutinefunction(self.fn) else self.fn()
59+
)
5760
if isinstance(result, Resource):
5861
return await result.read()
5962
if isinstance(result, bytes):

src/mcp/server/fastmcp/server.py

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""FastMCP - A more ergonomic interface for MCP servers."""
22

3-
import functools
43
import inspect
54
import json
65
import re
@@ -305,9 +304,19 @@ def resource(
305304
def get_data() -> str:
306305
return "Hello, world!"
307306
307+
@server.resource("resource://my-resource")
308+
async get_data() -> str:
309+
data = await fetch_data()
310+
return f"Hello, world! {data}"
311+
308312
@server.resource("resource://{city}/weather")
309313
def get_weather(city: str) -> str:
310314
return f"Weather for {city}"
315+
316+
@server.resource("resource://{city}/weather")
317+
async def get_weather(city: str) -> str:
318+
data = await fetch_weather(city)
319+
return f"Weather for {city}: {data}"
311320
"""
312321
# Check if user passed function directly instead of calling decorator
313322
if callable(uri):
@@ -317,10 +326,6 @@ def get_weather(city: str) -> str:
317326
)
318327

319328
def decorator(fn: Callable) -> Callable:
320-
@functools.wraps(fn)
321-
def wrapper(*args: Any, **kwargs: Any) -> Any:
322-
return fn(*args, **kwargs)
323-
324329
# Check if this should be a template
325330
has_uri_params = "{" in uri and "}" in uri
326331
has_func_params = bool(inspect.signature(fn).parameters)
@@ -338,7 +343,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
338343

339344
# Register as template
340345
self._resource_manager.add_template(
341-
wrapper,
346+
fn=fn,
342347
uri_template=uri,
343348
name=name,
344349
description=description,
@@ -351,10 +356,10 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
351356
name=name,
352357
description=description,
353358
mime_type=mime_type or "text/plain",
354-
fn=wrapper,
359+
fn=fn,
355360
)
356361
self.add_resource(resource)
357-
return wrapper
362+
return fn
358363

359364
return decorator
360365

tests/server/fastmcp/resources/test_function_resources.py

+16
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,19 @@ def get_data() -> CustomData:
120120
)
121121
content = await resource.read()
122122
assert isinstance(content, str)
123+
124+
@pytest.mark.anyio
125+
async def test_async_read_text(self):
126+
"""Test reading text from async FunctionResource."""
127+
128+
async def get_data() -> str:
129+
return "Hello, world!"
130+
131+
resource = FunctionResource(
132+
uri=AnyUrl("function://test"),
133+
name="test",
134+
fn=get_data,
135+
)
136+
content = await resource.read()
137+
assert content == "Hello, world!"
138+
assert resource.mime_type == "text/plain"

0 commit comments

Comments
 (0)