Skip to content

feat: add async resources for FastMCP #157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/mcp/server/fastmcp/resources/types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Concrete resource implementations."""

import inspect
import json
from collections.abc import Callable
from pathlib import Path
Expand Down Expand Up @@ -53,7 +54,9 @@ class FunctionResource(Resource):
async def read(self) -> str | bytes:
"""Read the resource by calling the wrapped function."""
try:
result = self.fn()
result = (
await self.fn() if inspect.iscoroutinefunction(self.fn) else self.fn()
)
if isinstance(result, Resource):
return await result.read()
if isinstance(result, bytes):
Expand Down
21 changes: 13 additions & 8 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""FastMCP - A more ergonomic interface for MCP servers."""

import functools
import inspect
import json
import re
Expand Down Expand Up @@ -305,9 +304,19 @@ def resource(
def get_data() -> str:
return "Hello, world!"

@server.resource("resource://my-resource")
async get_data() -> str:
data = await fetch_data()
return f"Hello, world! {data}"

@server.resource("resource://{city}/weather")
def get_weather(city: str) -> str:
return f"Weather for {city}"

@server.resource("resource://{city}/weather")
async def get_weather(city: str) -> str:
data = await fetch_weather(city)
return f"Weather for {city}: {data}"
"""
# Check if user passed function directly instead of calling decorator
if callable(uri):
Expand All @@ -317,10 +326,6 @@ def get_weather(city: str) -> str:
)

def decorator(fn: Callable) -> Callable:
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
return fn(*args, **kwargs)

# Check if this should be a template
has_uri_params = "{" in uri and "}" in uri
has_func_params = bool(inspect.signature(fn).parameters)
Expand All @@ -338,7 +343,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:

# Register as template
self._resource_manager.add_template(
wrapper,
fn=fn,
uri_template=uri,
name=name,
description=description,
Expand All @@ -351,10 +356,10 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
name=name,
description=description,
mime_type=mime_type or "text/plain",
fn=wrapper,
fn=fn,
)
self.add_resource(resource)
return wrapper
return fn

return decorator

Expand Down
16 changes: 16 additions & 0 deletions tests/server/fastmcp/resources/test_function_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,19 @@ def get_data() -> CustomData:
)
content = await resource.read()
assert isinstance(content, str)

@pytest.mark.anyio
async def test_async_read_text(self):
"""Test reading text from async FunctionResource."""

async def get_data() -> str:
return "Hello, world!"

resource = FunctionResource(
uri=AnyUrl("function://test"),
name="test",
fn=get_data,
)
content = await resource.read()
assert content == "Hello, world!"
assert resource.mime_type == "text/plain"
Loading