Skip to content

Commit 08c49e6

Browse files
Fix query-only resource templates not matching URIs without query strings (#2323)
* Fix query-only resource templates not matching URIs without query strings * apply the same fix to `has_resource`
1 parent 443c44c commit 08c49e6

File tree

2 files changed

+109
-3
lines changed

2 files changed

+109
-3
lines changed

src/fastmcp/resources/resource_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ async def has_resource(self, uri: AnyUrl | str) -> bool:
236236
# Then check templates (local and mounted) only if not found in concrete resources
237237
templates = await self.get_resource_templates()
238238
for template_key in templates:
239-
if match_uri_template(uri_str, template_key):
239+
if match_uri_template(uri_str, template_key) is not None:
240240
return True
241241

242242
return False
@@ -262,7 +262,7 @@ async def get_resource(self, uri: AnyUrl | str) -> Resource:
262262
templates = await self.get_resource_templates()
263263
for storage_key, template in templates.items():
264264
# Try to match against the storage key (which might be a custom key)
265-
if params := match_uri_template(uri_str, storage_key):
265+
if (params := match_uri_template(uri_str, storage_key)) is not None:
266266
try:
267267
return await template.create_resource(
268268
uri_str,
@@ -318,7 +318,7 @@ async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
318318

319319
# 1b. Check local templates if not found in concrete resources
320320
for key, template in self._templates.items():
321-
if params := match_uri_template(uri_str, key):
321+
if (params := match_uri_template(uri_str, key)) is not None:
322322
try:
323323
resource = await template.create_resource(uri_str, params=params)
324324
return await resource.read()

tests/resources/test_resource_manager.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,112 @@ def greet(name: str) -> str:
567567
await manager.get_resource("greet://world")
568568

569569

570+
class TestQueryOnlyTemplates:
571+
"""Test resource templates with only query parameters (no path params)."""
572+
573+
async def test_template_with_only_query_params_no_query_string(self):
574+
"""Test that templates with only query params work without query string.
575+
576+
Regression test for bug where empty parameter dict {} was treated as falsy,
577+
causing templates with only query parameters to fail when no query string
578+
was provided in the URI.
579+
"""
580+
manager = ResourceManager()
581+
582+
def get_config(format: str = "json") -> str:
583+
return f"Config in {format} format"
584+
585+
template = ResourceTemplate.from_function(
586+
fn=get_config,
587+
uri_template="data://config{?format}",
588+
name="config",
589+
)
590+
manager.add_template(template)
591+
592+
# Should work without query param (uses default)
593+
resource = await manager.get_resource("data://config")
594+
content = await resource.read()
595+
assert content == "Config in json format"
596+
597+
# Should also work via read_resource
598+
content = await manager.read_resource("data://config")
599+
assert content == "Config in json format"
600+
601+
async def test_template_with_only_query_params_with_query_string(self):
602+
"""Test that templates with only query params work with query string."""
603+
manager = ResourceManager()
604+
605+
def get_config(format: str = "json") -> str:
606+
return f"Config in {format} format"
607+
608+
template = ResourceTemplate.from_function(
609+
fn=get_config,
610+
uri_template="data://config{?format}",
611+
name="config",
612+
)
613+
manager.add_template(template)
614+
615+
# Should work with query param (overrides default)
616+
resource = await manager.get_resource("data://config?format=xml")
617+
content = await resource.read()
618+
assert content == "Config in xml format"
619+
620+
# Should also work via read_resource
621+
content = await manager.read_resource("data://config?format=xml")
622+
assert content == "Config in xml format"
623+
624+
async def test_template_with_only_multiple_query_params(self):
625+
"""Test template with only multiple query parameters."""
626+
manager = ResourceManager()
627+
628+
def get_data(format: str = "json", limit: int = 10) -> str:
629+
return f"Data in {format} (limit: {limit})"
630+
631+
template = ResourceTemplate.from_function(
632+
fn=get_data,
633+
uri_template="data://items{?format,limit}",
634+
name="items",
635+
)
636+
manager.add_template(template)
637+
638+
# No query params - use all defaults
639+
content = await manager.read_resource("data://items")
640+
assert content == "Data in json (limit: 10)"
641+
642+
# Partial query params
643+
content = await manager.read_resource("data://items?format=xml")
644+
assert content == "Data in xml (limit: 10)"
645+
646+
# All query params
647+
content = await manager.read_resource("data://items?format=xml&limit=20")
648+
assert content == "Data in xml (limit: 20)"
649+
650+
async def test_has_resource_with_query_only_template(self):
651+
"""Test that has_resource() works with query-only templates.
652+
653+
Regression test for bug where empty parameter dict {} was treated as falsy,
654+
causing has_resource() to return False for query-only templates when no
655+
query string was provided.
656+
"""
657+
manager = ResourceManager()
658+
659+
def get_config(format: str = "json") -> str:
660+
return f"Config in {format} format"
661+
662+
template = ResourceTemplate.from_function(
663+
fn=get_config,
664+
uri_template="data://config{?format}",
665+
name="config",
666+
)
667+
manager.add_template(template)
668+
669+
# Should find resource without query param (uses default)
670+
assert await manager.has_resource("data://config")
671+
672+
# Should also find resource with query param
673+
assert await manager.has_resource("data://config?format=xml")
674+
675+
570676
class TestResourceErrorHandling:
571677
"""Test error handling in the ResourceManager."""
572678

0 commit comments

Comments
 (0)