From 310eb730b36ac88242be1ae6737373c92f490fbf Mon Sep 17 00:00:00 2001 From: jingyugao <1121087373@qq.com> Date: Fri, 11 Apr 2025 15:56:06 +0800 Subject: [PATCH 1/5] Update @mcp.resource to use function documentation as default description if not provided --- src/mcp/server/fastmcp/resources/types.py | 27 ++++++++++++++++++- src/mcp/server/fastmcp/server.py | 18 +++++++------ .../resources/test_function_resources.py | 19 +++++++++++++ tests/server/fastmcp/test_server.py | 20 ++++++++++++++ 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/mcp/server/fastmcp/resources/types.py b/src/mcp/server/fastmcp/resources/types.py index d9fe2de6c..a6b1fa4fb 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.json import pydantic_core -from pydantic import Field, ValidationInfo +from pydantic import AnyUrl, Field, ValidationInfo, validate_call from mcp.server.fastmcp.resources.base import Resource @@ -71,6 +71,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, + ): + """Create a template 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=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 bf0ce880a..1cd04fdcf 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -116,9 +116,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 @@ -381,16 +383,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 5bfc72bf6..6bda4ac92 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 e76e59c52..5dd0664ab 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -348,6 +348,26 @@ 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" + result = await client.read_resource(AnyUrl("function://test")) + assert result.contents[0].text == "Hello, world!" + class TestServerResourceTemplates: @pytest.mark.anyio From 66d459bb7c486a358f4caf859135be9d9c33dd6f Mon Sep 17 00:00:00 2001 From: gaojingyu Date: Thu, 15 May 2025 11:51:05 +0800 Subject: [PATCH 2/5] Update @mcp.resource to use function documentation as default description if not provided --- src/mcp/server/fastmcp/resources/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mcp/server/fastmcp/resources/types.py b/src/mcp/server/fastmcp/resources/types.py index 627545b5b..1938823f9 100644 --- a/src/mcp/server/fastmcp/resources/types.py +++ b/src/mcp/server/fastmcp/resources/types.py @@ -4,7 +4,7 @@ import json from collections.abc import Callable from pathlib import Path -from typing import Any +from typing import Any, Self import anyio import anyio.to_thread @@ -76,7 +76,7 @@ def from_function( name: str | None = None, description: str | None = None, mime_type: str | None = None, - ): + ) -> Self: """Create a template from a function.""" func_name = name or fn.__name__ if func_name == "": @@ -87,7 +87,7 @@ def from_function( return cls( uri=AnyUrl(uri), - name=name, + name=func_name, description=description or fn.__doc__ or "", mime_type=mime_type or "text/plain", fn=fn, From c0adeb89d263319a7931238671a92ffc10d93dae Mon Sep 17 00:00:00 2001 From: gaojingyu Date: Thu, 15 May 2025 11:52:58 +0800 Subject: [PATCH 3/5] fix comment --- src/mcp/server/fastmcp/resources/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/server/fastmcp/resources/types.py b/src/mcp/server/fastmcp/resources/types.py index 1938823f9..db0ac742e 100644 --- a/src/mcp/server/fastmcp/resources/types.py +++ b/src/mcp/server/fastmcp/resources/types.py @@ -77,7 +77,7 @@ def from_function( description: str | None = None, mime_type: str | None = None, ) -> Self: - """Create a template from a function.""" + """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") From fa1b5fe7ddc906cf43be716f67e9ae52e7a566ba Mon Sep 17 00:00:00 2001 From: gaojingyu Date: Thu, 15 May 2025 12:49:16 +0800 Subject: [PATCH 4/5] fix build --- src/mcp/server/fastmcp/resources/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/server/fastmcp/resources/types.py b/src/mcp/server/fastmcp/resources/types.py index db0ac742e..fbf5ed57e 100644 --- a/src/mcp/server/fastmcp/resources/types.py +++ b/src/mcp/server/fastmcp/resources/types.py @@ -76,7 +76,7 @@ def from_function( name: str | None = None, description: str | None = None, mime_type: str | None = None, - ) -> Self: + ) -> "FunctionResource": """Create a FunctionResource from a function.""" func_name = name or fn.__name__ if func_name == "": From 0826fa98138db384f7fecfc2bc23a7f95cd959dc Mon Sep 17 00:00:00 2001 From: gaojingyu Date: Thu, 15 May 2025 12:50:39 +0800 Subject: [PATCH 5/5] fix import --- src/mcp/server/fastmcp/resources/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/server/fastmcp/resources/types.py b/src/mcp/server/fastmcp/resources/types.py index fbf5ed57e..d3f10211d 100644 --- a/src/mcp/server/fastmcp/resources/types.py +++ b/src/mcp/server/fastmcp/resources/types.py @@ -4,7 +4,7 @@ import json from collections.abc import Callable from pathlib import Path -from typing import Any, Self +from typing import Any import anyio import anyio.to_thread