diff --git a/src/fastmcp/resources/resource_manager.py b/src/fastmcp/resources/resource_manager.py index a7214a8d1..7367fb3e9 100644 --- a/src/fastmcp/resources/resource_manager.py +++ b/src/fastmcp/resources/resource_manager.py @@ -236,7 +236,7 @@ class ResourceManager: # Then check templates (local and mounted) only if not found in concrete resources templates = await self.get_resource_templates() for template_key in templates: - if match_uri_template(uri_str, template_key): + if match_uri_template(uri_str, template_key) is not None: return True return False @@ -262,7 +262,7 @@ class ResourceManager: templates = await self.get_resource_templates() for storage_key, template in templates.items(): # Try to match against the storage key (which might be a custom key) - if params := match_uri_template(uri_str, storage_key): + if (params := match_uri_template(uri_str, storage_key)) is not None: try: return await template.create_resource( uri_str, @@ -318,7 +318,7 @@ class ResourceManager: # 1b. Check local templates if not found in concrete resources for key, template in self._templates.items(): - if params := match_uri_template(uri_str, key): + if (params := match_uri_template(uri_str, key)) is not None: try: resource = await template.create_resource(uri_str, params=params) return await resource.read() diff --git a/tests/resources/test_resource_manager.py b/tests/resources/test_resource_manager.py index 95a050884..c895cf739 100644 --- a/tests/resources/test_resource_manager.py +++ b/tests/resources/test_resource_manager.py @@ -567,6 +567,112 @@ class TestCustomResourceKeys: await manager.get_resource("greet://world") +class TestQueryOnlyTemplates: + """Test resource templates with only query parameters (no path params).""" + + async def test_template_with_only_query_params_no_query_string(self): + """Test that templates with only query params work without query string. + + Regression test for bug where empty parameter dict {} was treated as falsy, + causing templates with only query parameters to fail when no query string + was provided in the URI. + """ + manager = ResourceManager() + + def get_config(format: str = "json") -> str: + return f"Config in {format} format" + + template = ResourceTemplate.from_function( + fn=get_config, + uri_template="data://config{?format}", + name="config", + ) + manager.add_template(template) + + # Should work without query param (uses default) + resource = await manager.get_resource("data://config") + content = await resource.read() + assert content == "Config in json format" + + # Should also work via read_resource + content = await manager.read_resource("data://config") + assert content == "Config in json format" + + async def test_template_with_only_query_params_with_query_string(self): + """Test that templates with only query params work with query string.""" + manager = ResourceManager() + + def get_config(format: str = "json") -> str: + return f"Config in {format} format" + + template = ResourceTemplate.from_function( + fn=get_config, + uri_template="data://config{?format}", + name="config", + ) + manager.add_template(template) + + # Should work with query param (overrides default) + resource = await manager.get_resource("data://config?format=xml") + content = await resource.read() + assert content == "Config in xml format" + + # Should also work via read_resource + content = await manager.read_resource("data://config?format=xml") + assert content == "Config in xml format" + + async def test_template_with_only_multiple_query_params(self): + """Test template with only multiple query parameters.""" + manager = ResourceManager() + + def get_data(format: str = "json", limit: int = 10) -> str: + return f"Data in {format} (limit: {limit})" + + template = ResourceTemplate.from_function( + fn=get_data, + uri_template="data://items{?format,limit}", + name="items", + ) + manager.add_template(template) + + # No query params - use all defaults + content = await manager.read_resource("data://items") + assert content == "Data in json (limit: 10)" + + # Partial query params + content = await manager.read_resource("data://items?format=xml") + assert content == "Data in xml (limit: 10)" + + # All query params + content = await manager.read_resource("data://items?format=xml&limit=20") + assert content == "Data in xml (limit: 20)" + + async def test_has_resource_with_query_only_template(self): + """Test that has_resource() works with query-only templates. + + Regression test for bug where empty parameter dict {} was treated as falsy, + causing has_resource() to return False for query-only templates when no + query string was provided. + """ + manager = ResourceManager() + + def get_config(format: str = "json") -> str: + return f"Config in {format} format" + + template = ResourceTemplate.from_function( + fn=get_config, + uri_template="data://config{?format}", + name="config", + ) + manager.add_template(template) + + # Should find resource without query param (uses default) + assert await manager.has_resource("data://config") + + # Should also find resource with query param + assert await manager.has_resource("data://config?format=xml") + + class TestResourceErrorHandling: """Test error handling in the ResourceManager."""