mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Ensure servers expose template wildcards
This commit is contained in:
parent
04aa074516
commit
713864aa9a
3 changed files with 134 additions and 1 deletions
|
|
@ -95,7 +95,7 @@ class ResourceTemplate(BaseModel):
|
|||
raise ValueError("You must provide a name for lambda functions")
|
||||
|
||||
# Validate that URI params match function params
|
||||
uri_params = set(re.findall(r"{(\w+)}", uri_template))
|
||||
uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
|
||||
if not uri_params:
|
||||
raise ValueError("URI template must contain at least one parameter")
|
||||
|
||||
|
|
|
|||
|
|
@ -297,6 +297,66 @@ class TestResourceTemplate:
|
|||
content = await resource.read()
|
||||
assert content == "hello"
|
||||
|
||||
async def test_wildcard_param_can_create_resource(self):
|
||||
"""Test that wildcard parameters are valid."""
|
||||
|
||||
def identity(path: str) -> str:
|
||||
return path
|
||||
|
||||
template = ResourceTemplate.from_function(
|
||||
fn=identity,
|
||||
uri_template="test://{path*}.py",
|
||||
name="test",
|
||||
)
|
||||
|
||||
assert await template.create_resource(
|
||||
"test://path/to/test.py",
|
||||
{"path": "path/to/test.py"},
|
||||
)
|
||||
|
||||
async def test_wildcard_param_matches(self):
|
||||
def identify(path: str) -> str:
|
||||
return path
|
||||
|
||||
template = ResourceTemplate.from_function(
|
||||
fn=identify,
|
||||
uri_template="test://src/{path*}.py",
|
||||
name="test",
|
||||
)
|
||||
# Valid match
|
||||
params = template.matches("test://src/path/to/test.py")
|
||||
assert params == {"path": "path/to/test"}
|
||||
|
||||
async def test_multiple_wildcard_params(self):
|
||||
"""Test that multiple wildcard parameters are valid."""
|
||||
|
||||
def identity(path: str, path2: str) -> str:
|
||||
return f"{path}/{path2}"
|
||||
|
||||
template = ResourceTemplate.from_function(
|
||||
fn=identity,
|
||||
uri_template="test://{path*}/xyz/{path2*}",
|
||||
name="test",
|
||||
)
|
||||
|
||||
params = template.matches("test://path/to/xyz/abc")
|
||||
assert params == {"path": "path/to", "path2": "abc"}
|
||||
|
||||
async def test_wildcard_param_with_regular_param(self):
|
||||
"""Test that a wildcard parameter can be used with a regular parameter."""
|
||||
|
||||
def identity(prefix: str, path: str) -> str:
|
||||
return f"{prefix}/{path}"
|
||||
|
||||
template = ResourceTemplate.from_function(
|
||||
fn=identity,
|
||||
uri_template="test://{prefix}/{path*}",
|
||||
name="test",
|
||||
)
|
||||
|
||||
params = template.matches("test://src/path/to/test.py")
|
||||
assert params == {"prefix": "src", "path": "path/to/test.py"}
|
||||
|
||||
|
||||
class TestMatchUriTemplate:
|
||||
"""Test match_uri_template function."""
|
||||
|
|
|
|||
|
|
@ -531,6 +531,18 @@ class TestTemplateDecorator:
|
|||
template = templates_dict["resource://{param}"]
|
||||
assert template.tags == {"template", "test-tag"}
|
||||
|
||||
async def test_template_decorator_wildcard_param(self):
|
||||
mcp = FastMCP()
|
||||
|
||||
@mcp.resource("resource://{param*}")
|
||||
def template_resource(param: str) -> str:
|
||||
return f"Template resource: {param}"
|
||||
|
||||
templates_dict = await mcp.get_resource_templates()
|
||||
template = templates_dict["resource://{param*}"]
|
||||
assert template.uri_template == "resource://{param*}"
|
||||
assert template.name == "template_resource"
|
||||
|
||||
|
||||
class TestPromptDecorator:
|
||||
async def test_prompt_decorator(self):
|
||||
|
|
@ -1143,6 +1155,67 @@ class TestServerResourceTemplates:
|
|||
template = templates_dict["resource://{param}"]
|
||||
assert template.tags == {"template", "test-tag"}
|
||||
|
||||
async def test_template_decorator_wildcard_param(self):
|
||||
mcp = FastMCP()
|
||||
|
||||
@mcp.resource("resource://{param*}")
|
||||
def template_resource(param: str) -> str:
|
||||
return f"Template resource: {param}"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.read_resource(AnyUrl("resource://test/data"))
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
assert result[0].text == "Template resource: test/data"
|
||||
|
||||
async def test_templates_match_in_order_of_definition(self):
|
||||
"""
|
||||
If a wildcard template is defined first, it will take priority over another
|
||||
matching template.
|
||||
|
||||
"""
|
||||
mcp = FastMCP()
|
||||
|
||||
@mcp.resource("resource://{param*}")
|
||||
def template_resource(param: str) -> str:
|
||||
return f"Template resource 1: {param}"
|
||||
|
||||
@mcp.resource("resource://{x}/{y}")
|
||||
def template_resource_with_params(x: str, y: str) -> str:
|
||||
return f"Template resource 2: {x}/{y}"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.read_resource(AnyUrl("resource://a/b/c"))
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
assert result[0].text == "Template resource 1: a/b/c"
|
||||
|
||||
result = await client.read_resource(AnyUrl("resource://a/b"))
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
assert result[0].text == "Template resource 1: a/b"
|
||||
|
||||
async def test_templates_shadow_each_other_reorder(self):
|
||||
"""
|
||||
If a wildcard template is defined second, it will *not* take priority over
|
||||
another matching template.
|
||||
"""
|
||||
mcp = FastMCP()
|
||||
|
||||
@mcp.resource("resource://{x}/{y}")
|
||||
def template_resource_with_params(x: str, y: str) -> str:
|
||||
return f"Template resource 1: {x}/{y}"
|
||||
|
||||
@mcp.resource("resource://{param*}")
|
||||
def template_resource(param: str) -> str:
|
||||
return f"Template resource 2: {param}"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.read_resource(AnyUrl("resource://a/b/c"))
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
assert result[0].text == "Template resource 2: a/b/c"
|
||||
|
||||
result = await client.read_resource(AnyUrl("resource://a/b"))
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
assert result[0].text == "Template resource 1: a/b"
|
||||
|
||||
|
||||
class TestContextInjection:
|
||||
"""Test context injection in tools."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue