diff --git a/src/mcp/server/fastmcp/resources/types.py b/src/mcp/server/fastmcp/resources/types.py index 2ab39b078..d3f10211d 100644 --- a/src/mcp/server/fastmcp/resources/types.py +++ b/src/mcp/server/fastmcp/resources/types.py @@ -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 @@ -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 == "": + 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. diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 8929eb6fd..09896dc6d 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -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 @@ -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 diff --git a/tests/server/fastmcp/resources/test_function_resources.py b/tests/server/fastmcp/resources/test_function_resources.py index f0fe22bfb..f59436ae3 100644 --- a/tests/server/fastmcp/resources/test_function_resources.py +++ b/tests/server/fastmcp/resources/test_function_resources.py @@ -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") diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index 64700d959..b817761ea 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -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