Skip to content

Commit 310eb73

Browse files
committed
Update @mcp.resource to use function documentation as default description if not provided
1 parent 70115b9 commit 310eb73

File tree

4 files changed

+75
-9
lines changed

4 files changed

+75
-9
lines changed

src/mcp/server/fastmcp/resources/types.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import httpx
1212
import pydantic.json
1313
import pydantic_core
14-
from pydantic import Field, ValidationInfo
14+
from pydantic import AnyUrl, Field, ValidationInfo, validate_call
1515

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

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

74+
@classmethod
75+
def from_function(
76+
cls,
77+
fn: Callable[..., Any],
78+
uri: str,
79+
name: str | None = None,
80+
description: str | None = None,
81+
mime_type: str | None = None,
82+
):
83+
"""Create a template from a function."""
84+
func_name = name or fn.__name__
85+
if func_name == "<lambda>":
86+
raise ValueError("You must provide a name for lambda functions")
87+
88+
# ensure the arguments are properly cast
89+
fn = validate_call(fn)
90+
91+
return cls(
92+
uri=AnyUrl(uri),
93+
name=name,
94+
description=description or fn.__doc__ or "",
95+
mime_type=mime_type or "text/plain",
96+
fn=fn,
97+
)
98+
7499

75100
class FileResource(Resource):
76101
"""A resource that reads from a file.

src/mcp/server/fastmcp/server.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,11 @@ def __init__(
116116
self._mcp_server = MCPServer(
117117
name=name or "FastMCP",
118118
instructions=instructions,
119-
lifespan=lifespan_wrapper(self, self.settings.lifespan)
120-
if self.settings.lifespan
121-
else default_lifespan,
119+
lifespan=(
120+
lifespan_wrapper(self, self.settings.lifespan)
121+
if self.settings.lifespan
122+
else default_lifespan
123+
),
122124
)
123125
self._tool_manager = ToolManager(
124126
warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
@@ -381,16 +383,16 @@ def decorator(fn: AnyFunction) -> AnyFunction:
381383
uri_template=uri,
382384
name=name,
383385
description=description,
384-
mime_type=mime_type or "text/plain",
386+
mime_type=mime_type,
385387
)
386388
else:
387389
# Register as regular resource
388-
resource = FunctionResource(
389-
uri=AnyUrl(uri),
390+
resource = FunctionResource.from_function(
391+
fn=fn,
392+
uri=uri,
390393
name=name,
391394
description=description,
392-
mime_type=mime_type or "text/plain",
393-
fn=fn,
395+
mime_type=mime_type,
394396
)
395397
self.add_resource(resource)
396398
return fn

tests/server/fastmcp/resources/test_function_resources.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,22 @@ async def get_data() -> str:
136136
content = await resource.read()
137137
assert content == "Hello, world!"
138138
assert resource.mime_type == "text/plain"
139+
140+
@pytest.mark.anyio
141+
async def test_from_function(self):
142+
"""Test creating a FunctionResource from a function."""
143+
144+
async def get_data() -> str:
145+
"""get_data returns a string"""
146+
return "Hello, world!"
147+
148+
resource = FunctionResource.from_function(
149+
fn=get_data,
150+
uri="function://test",
151+
name="test",
152+
)
153+
154+
assert resource.description == "get_data returns a string"
155+
assert resource.mime_type == "text/plain"
156+
assert resource.name == "test"
157+
assert resource.uri == AnyUrl("function://test")

tests/server/fastmcp/test_server.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,26 @@ async def test_file_resource_binary(self, tmp_path: Path):
348348
== base64.b64encode(b"Binary file data").decode()
349349
)
350350

351+
@pytest.mark.anyio
352+
async def test_function_resource(self):
353+
mcp = FastMCP()
354+
355+
@mcp.resource("function://test", name="test_get_data")
356+
def get_data() -> str:
357+
"""get_data returns a string"""
358+
return "Hello, world!"
359+
360+
async with client_session(mcp._mcp_server) as client:
361+
resources = await client.list_resources()
362+
assert len(resources.resources) == 1
363+
resource = resources.resources[0]
364+
assert resource.description == "get_data returns a string"
365+
assert resource.uri == AnyUrl("function://test")
366+
assert resource.name == "test_get_data"
367+
assert resource.mimeType == "text/plain"
368+
result = await client.read_resource(AnyUrl("function://test"))
369+
assert result.contents[0].text == "Hello, world!"
370+
351371

352372
class TestServerResourceTemplates:
353373
@pytest.mark.anyio

0 commit comments

Comments
 (0)