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

This commit is contained in:
gaojingyu
2025-05-15 18:14:50 +08:00
committed by GitHub
parent c2f8730d6d
commit 1bdeed33c2
4 changed files with 73 additions and 9 deletions

View File

@@ -11,7 +11,7 @@ import anyio.to_thread
import httpx import httpx
import pydantic import pydantic
import pydantic_core import pydantic_core
from pydantic import Field, ValidationInfo from pydantic import AnyUrl, Field, ValidationInfo, validate_call
from mcp.server.fastmcp.resources.base import Resource from mcp.server.fastmcp.resources.base import Resource
@@ -68,6 +68,31 @@ class FunctionResource(Resource):
except Exception as e: except Exception as e:
raise ValueError(f"Error reading resource {self.uri}: {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): class FileResource(Resource):
"""A resource that reads from a file. """A resource that reads from a file.

View File

@@ -148,9 +148,11 @@ class FastMCP:
self._mcp_server = MCPServer( self._mcp_server = MCPServer(
name=name or "FastMCP", name=name or "FastMCP",
instructions=instructions, instructions=instructions,
lifespan=lifespan_wrapper(self, self.settings.lifespan) lifespan=(
if self.settings.lifespan lifespan_wrapper(self, self.settings.lifespan)
else default_lifespan, if self.settings.lifespan
else default_lifespan
),
) )
self._tool_manager = ToolManager( self._tool_manager = ToolManager(
warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
@@ -465,16 +467,16 @@ class FastMCP:
uri_template=uri, uri_template=uri,
name=name, name=name,
description=description, description=description,
mime_type=mime_type or "text/plain", mime_type=mime_type,
) )
else: else:
# Register as regular resource # Register as regular resource
resource = FunctionResource( resource = FunctionResource.from_function(
uri=AnyUrl(uri), fn=fn,
uri=uri,
name=name, name=name,
description=description, description=description,
mime_type=mime_type or "text/plain", mime_type=mime_type,
fn=fn,
) )
self.add_resource(resource) self.add_resource(resource)
return fn return fn

View File

@@ -136,3 +136,22 @@ class TestFunctionResource:
content = await resource.read() content = await resource.read()
assert content == "Hello, world!" assert content == "Hello, world!"
assert resource.mime_type == "text/plain" 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")

View File

@@ -441,6 +441,24 @@ class TestServerResources:
== base64.b64encode(b"Binary file data").decode() == 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: class TestServerResourceTemplates:
@pytest.mark.anyio @pytest.mark.anyio