mirror of
https://github.com/aljazceru/mcp-python-sdk.git
synced 2025-12-19 23:04:25 +01:00
fix: Update @mcp.resource to use function documentation as default descrip… (#489)
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user