diff --git a/src/fastmcp/resources/manager.py b/src/fastmcp/resources/manager.py index efbcf1b6a..3950236a1 100644 --- a/src/fastmcp/resources/manager.py +++ b/src/fastmcp/resources/manager.py @@ -62,6 +62,11 @@ class ResourceManager: logger.debug("Listing resources", extra={"count": len(self._resources)}) return list(self._resources.values()) + def list_templates(self) -> list[ResourceTemplate]: + """List all registered templates.""" + logger.debug("Listing templates", extra={"count": len(self._templates)}) + return list(self._templates.values()) + def add_resource(self, resource: Resource) -> Resource: """Add a resource to the manager. diff --git a/src/fastmcp/server.py b/src/fastmcp/server.py index 440752abc..b12fb1c52 100644 --- a/src/fastmcp/server.py +++ b/src/fastmcp/server.py @@ -13,7 +13,8 @@ from mcp.server.stdio import stdio_server from mcp.server.sse import SseServerTransport from mcp.types import ( Resource as MCPResource, - Tool, + Tool as MCPTool, + ResourceTemplate as MCPResourceTemplate, TextContent, ImageContent, ) @@ -95,12 +96,14 @@ class FastMCP: self._mcp_server.call_tool()(self.call_tool) self._mcp_server.list_resources()(self.list_resources) self._mcp_server.read_resource()(self.read_resource) + # TODO: This has not been added to MCP yet, see https://github.com/jlowin/fastmcp/issues/10 + # self._mcp_server.list_resource_templates()(self.list_resource_templates) - async def list_tools(self) -> list[Tool]: + async def list_tools(self) -> list[MCPTool]: """List all available tools.""" tools = self._tool_manager.list_tools() return [ - Tool( + MCPTool( name=info.name, description=info.description, inputSchema=info.parameters, @@ -139,6 +142,17 @@ class FastMCP: for resource in resources ] + async def list_resource_templates(self) -> list[MCPResourceTemplate]: + templates = self._resource_manager.list_templates() + return [ + MCPResourceTemplate( + uriTemplate=template.uri_template, + name=template.name, + description=template.description, + ) + for template in templates + ] + async def read_resource(self, uri: _BaseUrl) -> Union[str, bytes]: """Read a resource by URI.""" resource = await self._resource_manager.get_resource(uri) diff --git a/tests/test_server.py b/tests/test_server.py index a97405ce6..04b1b11e3 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -265,8 +265,11 @@ class TestServerResources: == base64.b64encode(b"Binary file data").decode() ) + +class TestServerResourceTemplates: async def test_resource_with_params(self): - """Test that a resource with function parameters is automatically a template""" + """Test that a resource with function parameters raises an error if the URI + parameters don't match""" mcp = FastMCP() with pytest.raises(ValueError, match="Mismatch between URI parameters"): @@ -285,6 +288,14 @@ class TestServerResources: def get_data() -> str: return "Data" + async def test_resource_with_untyped_params(self): + """Test that a resource with untyped parameters raises an error""" + mcp = FastMCP() + + @mcp.resource("resource://{param}") + def get_data(param) -> str: + return "Data" + async def test_resource_matching_params(self): """Test that a resource with matching URI and function parameters works""" mcp = FastMCP() @@ -319,7 +330,16 @@ class TestServerResources: result = await client.read_resource("resource://cursor/fastmcp/data") assert result.contents[0].text == "Data for cursor/fastmcp" - async def test_resource_no_params(self): + async def test_resource_multiple_mismatched_params(self): + """Test that mismatched parameters raise an error""" + mcp = FastMCP() + + with pytest.raises(ValueError, match="Mismatch between URI parameters"): + + @mcp.resource("resource://{org}/{repo}/data") + def get_data(org: str, repo_2: str) -> str: + return f"Data for {org}" + """Test that a resource with no parameters works as a regular resource""" mcp = FastMCP() @@ -341,7 +361,7 @@ class TestServerResources: # Should be registered as a template assert len(mcp._resource_manager._templates) == 1 - assert len(mcp._resource_manager.list_resources()) == 0 + assert len(await mcp.list_resources()) == 0 # When accessed, should create a concrete resource resource = await mcp._resource_manager.get_resource("resource://test/data")