Skip to content

Commit 8ba9b5e

Browse files
authored
Merge branch 'main' into modelcontextprotocol#552
2 parents fb09415 + 1bdeed3 commit 8ba9b5e

File tree

4 files changed

+73
-9
lines changed

4 files changed

+73
-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
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

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

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

7297
class FileResource(Resource):
7398
"""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
@@ -148,9 +148,11 @@ def __init__(
148148
self._mcp_server = MCPServer(
149149
name=name or "FastMCP",
150150
instructions=instructions,
151-
lifespan=lifespan_wrapper(self, self.settings.lifespan)
152-
if self.settings.lifespan
153-
else default_lifespan,
151+
lifespan=(
152+
lifespan_wrapper(self, self.settings.lifespan)
153+
if self.settings.lifespan
154+
else default_lifespan
155+
),
154156
)
155157
self._tool_manager = ToolManager(
156158
warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
@@ -465,16 +467,16 @@ def decorator(fn: AnyFunction) -> AnyFunction:
465467
uri_template=uri,
466468
name=name,
467469
description=description,
468-
mime_type=mime_type or "text/plain",
470+
mime_type=mime_type,
469471
)
470472
else:
471473
# Register as regular resource
472-
resource = FunctionResource(
473-
uri=AnyUrl(uri),
474+
resource = FunctionResource.from_function(
475+
fn=fn,
476+
uri=uri,
474477
name=name,
475478
description=description,
476-
mime_type=mime_type or "text/plain",
477-
fn=fn,
479+
mime_type=mime_type,
478480
)
479481
self.add_resource(resource)
480482
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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,24 @@ async def test_file_resource_binary(self, tmp_path: Path):
441441
== base64.b64encode(b"Binary file data").decode()
442442
)
443443

444+
@pytest.mark.anyio
445+
async def test_function_resource(self):
446+
mcp = FastMCP()
447+
448+
@mcp.resource("function://test", name="test_get_data")
449+
def get_data() -> str:
450+
"""get_data returns a string"""
451+
return "Hello, world!"
452+
453+
async with client_session(mcp._mcp_server) as client:
454+
resources = await client.list_resources()
455+
assert len(resources.resources) == 1
456+
resource = resources.resources[0]
457+
assert resource.description == "get_data returns a string"
458+
assert resource.uri == AnyUrl("function://test")
459+
assert resource.name == "test_get_data"
460+
assert resource.mimeType == "text/plain"
461+
444462

445463
class TestServerResourceTemplates:
446464
@pytest.mark.anyio

0 commit comments

Comments
 (0)