Skip to content

Commit 1934f8e

Browse files
micpstdsp-ant
authored andcommitted
add support for async resources
1 parent e391f4b commit 1934f8e

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
@@ -299,9 +298,19 @@ def resource(
299298
def get_data() -> str:
300299
return "Hello, world!"
301300
301+
@server.resource("resource://my-resource")
302+
async get_data() -> str:
303+
data = await fetch_data()
304+
return f"Hello, world! {data}"
305+
302306
@server.resource("resource://{city}/weather")
303307
def get_weather(city: str) -> str:
304308
return f"Weather for {city}"
309+
310+
@server.resource("resource://{city}/weather")
311+
async def get_weather(city: str) -> str:
312+
data = await fetch_weather(city)
313+
return f"Weather for {city}: {data}"
305314
"""
306315
# Check if user passed function directly instead of calling decorator
307316
if callable(uri):
@@ -311,10 +320,6 @@ def get_weather(city: str) -> str:
311320
)
312321

313322
def decorator(fn: Callable) -> Callable:
314-
@functools.wraps(fn)
315-
def wrapper(*args: Any, **kwargs: Any) -> Any:
316-
return fn(*args, **kwargs)
317-
318323
# Check if this should be a template
319324
has_uri_params = "{" in uri and "}" in uri
320325
has_func_params = bool(inspect.signature(fn).parameters)
@@ -332,7 +337,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
332337

333338
# Register as template
334339
self._resource_manager.add_template(
335-
wrapper,
340+
fn=fn,
336341
uri_template=uri,
337342
name=name,
338343
description=description,
@@ -345,10 +350,10 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
345350
name=name,
346351
description=description,
347352
mime_type=mime_type or "text/plain",
348-
fn=wrapper,
353+
fn=fn,
349354
)
350355
self.add_resource(resource)
351-
return wrapper
356+
return fn
352357

353358
return decorator
354359

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)