Skip to content

fix: Update @mcp.resource to use function documentation as default descrip… #489

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 7 commits into from
May 15, 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
27 changes: 26 additions & 1 deletion src/mcp/server/fastmcp/resources/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import httpx
import pydantic
import pydantic_core
from pydantic import Field, ValidationInfo
from pydantic import AnyUrl, Field, ValidationInfo, validate_call

from mcp.server.fastmcp.resources.base import Resource

Expand Down Expand Up @@ -68,6 +68,31 @@ async def read(self) -> str | bytes:
except Exception as e:
raise ValueError(f"Error reading resource {self.uri}: {e}")

@classmethod
def from_function(
cls,
fn: Callable[..., Any],
uri: str,
name: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> "FunctionResource":
"""Create a FunctionResource from a function."""
func_name = name or fn.__name__
if func_name == "<lambda>":
raise ValueError("You must provide a name for lambda functions")

# ensure the arguments are properly cast
fn = validate_call(fn)

return cls(
uri=AnyUrl(uri),
name=func_name,
description=description or fn.__doc__ or "",
mime_type=mime_type or "text/plain",
fn=fn,
)


class FileResource(Resource):
"""A resource that reads from a file.
Expand Down
18 changes: 10 additions & 8 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,11 @@ def __init__(
self._mcp_server = MCPServer(
name=name or "FastMCP",
instructions=instructions,
lifespan=lifespan_wrapper(self, self.settings.lifespan)
if self.settings.lifespan
else default_lifespan,
lifespan=(
lifespan_wrapper(self, self.settings.lifespan)
if self.settings.lifespan
else default_lifespan
),
)
self._tool_manager = ToolManager(
warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
Expand Down Expand Up @@ -465,16 +467,16 @@ def decorator(fn: AnyFunction) -> AnyFunction:
uri_template=uri,
name=name,
description=description,
mime_type=mime_type or "text/plain",
mime_type=mime_type,
)
else:
# Register as regular resource
resource = FunctionResource(
uri=AnyUrl(uri),
resource = FunctionResource.from_function(
fn=fn,
uri=uri,
name=name,
description=description,
mime_type=mime_type or "text/plain",
fn=fn,
mime_type=mime_type,
)
self.add_resource(resource)
return fn
Expand Down
19 changes: 19 additions & 0 deletions tests/server/fastmcp/resources/test_function_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,22 @@ async def get_data() -> str:
content = await resource.read()
assert content == "Hello, world!"
assert resource.mime_type == "text/plain"

@pytest.mark.anyio
async def test_from_function(self):
"""Test creating a FunctionResource from a function."""

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

resource = FunctionResource.from_function(
fn=get_data,
uri="function://test",
name="test",
)

assert resource.description == "get_data returns a string"
assert resource.mime_type == "text/plain"
assert resource.name == "test"
assert resource.uri == AnyUrl("function://test")
18 changes: 18 additions & 0 deletions tests/server/fastmcp/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,24 @@ async def test_file_resource_binary(self, tmp_path: Path):
== base64.b64encode(b"Binary file data").decode()
)

@pytest.mark.anyio
async def test_function_resource(self):
mcp = FastMCP()

@mcp.resource("function://test", name="test_get_data")
def get_data() -> str:
"""get_data returns a string"""
return "Hello, world!"

async with client_session(mcp._mcp_server) as client:
resources = await client.list_resources()
assert len(resources.resources) == 1
resource = resources.resources[0]
assert resource.description == "get_data returns a string"
assert resource.uri == AnyUrl("function://test")
assert resource.name == "test_get_data"
assert resource.mimeType == "text/plain"


class TestServerResourceTemplates:
@pytest.mark.anyio
Expand Down
Loading