Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
ceea07c
1
Parent(s): 46c2da8
Ensure servers expose template wildcards
Browse files
src/fastmcp/resources/template.py
CHANGED
|
@@ -95,7 +95,7 @@ class ResourceTemplate(BaseModel):
|
|
| 95 |
raise ValueError("You must provide a name for lambda functions")
|
| 96 |
|
| 97 |
# Validate that URI params match function params
|
| 98 |
-
uri_params = set(re.findall(r"{(\w+)}", uri_template))
|
| 99 |
if not uri_params:
|
| 100 |
raise ValueError("URI template must contain at least one parameter")
|
| 101 |
|
|
|
|
| 95 |
raise ValueError("You must provide a name for lambda functions")
|
| 96 |
|
| 97 |
# Validate that URI params match function params
|
| 98 |
+
uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
|
| 99 |
if not uri_params:
|
| 100 |
raise ValueError("URI template must contain at least one parameter")
|
| 101 |
|
tests/resources/test_resource_template.py
CHANGED
|
@@ -297,6 +297,66 @@ class TestResourceTemplate:
|
|
| 297 |
content = await resource.read()
|
| 298 |
assert content == "hello"
|
| 299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
class TestMatchUriTemplate:
|
| 302 |
"""Test match_uri_template function."""
|
|
|
|
| 297 |
content = await resource.read()
|
| 298 |
assert content == "hello"
|
| 299 |
|
| 300 |
+
async def test_wildcard_param_can_create_resource(self):
|
| 301 |
+
"""Test that wildcard parameters are valid."""
|
| 302 |
+
|
| 303 |
+
def identity(path: str) -> str:
|
| 304 |
+
return path
|
| 305 |
+
|
| 306 |
+
template = ResourceTemplate.from_function(
|
| 307 |
+
fn=identity,
|
| 308 |
+
uri_template="test://{path*}.py",
|
| 309 |
+
name="test",
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
assert await template.create_resource(
|
| 313 |
+
"test://path/to/test.py",
|
| 314 |
+
{"path": "path/to/test.py"},
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
async def test_wildcard_param_matches(self):
|
| 318 |
+
def identify(path: str) -> str:
|
| 319 |
+
return path
|
| 320 |
+
|
| 321 |
+
template = ResourceTemplate.from_function(
|
| 322 |
+
fn=identify,
|
| 323 |
+
uri_template="test://src/{path*}.py",
|
| 324 |
+
name="test",
|
| 325 |
+
)
|
| 326 |
+
# Valid match
|
| 327 |
+
params = template.matches("test://src/path/to/test.py")
|
| 328 |
+
assert params == {"path": "path/to/test"}
|
| 329 |
+
|
| 330 |
+
async def test_multiple_wildcard_params(self):
|
| 331 |
+
"""Test that multiple wildcard parameters are valid."""
|
| 332 |
+
|
| 333 |
+
def identity(path: str, path2: str) -> str:
|
| 334 |
+
return f"{path}/{path2}"
|
| 335 |
+
|
| 336 |
+
template = ResourceTemplate.from_function(
|
| 337 |
+
fn=identity,
|
| 338 |
+
uri_template="test://{path*}/xyz/{path2*}",
|
| 339 |
+
name="test",
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
params = template.matches("test://path/to/xyz/abc")
|
| 343 |
+
assert params == {"path": "path/to", "path2": "abc"}
|
| 344 |
+
|
| 345 |
+
async def test_wildcard_param_with_regular_param(self):
|
| 346 |
+
"""Test that a wildcard parameter can be used with a regular parameter."""
|
| 347 |
+
|
| 348 |
+
def identity(prefix: str, path: str) -> str:
|
| 349 |
+
return f"{prefix}/{path}"
|
| 350 |
+
|
| 351 |
+
template = ResourceTemplate.from_function(
|
| 352 |
+
fn=identity,
|
| 353 |
+
uri_template="test://{prefix}/{path*}",
|
| 354 |
+
name="test",
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
params = template.matches("test://src/path/to/test.py")
|
| 358 |
+
assert params == {"prefix": "src", "path": "path/to/test.py"}
|
| 359 |
+
|
| 360 |
|
| 361 |
class TestMatchUriTemplate:
|
| 362 |
"""Test match_uri_template function."""
|
tests/server/test_server.py
CHANGED
|
@@ -531,6 +531,18 @@ class TestTemplateDecorator:
|
|
| 531 |
template = templates_dict["resource://{param}"]
|
| 532 |
assert template.tags == {"template", "test-tag"}
|
| 533 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
class TestPromptDecorator:
|
| 536 |
async def test_prompt_decorator(self):
|
|
@@ -1143,6 +1155,67 @@ class TestServerResourceTemplates:
|
|
| 1143 |
template = templates_dict["resource://{param}"]
|
| 1144 |
assert template.tags == {"template", "test-tag"}
|
| 1145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1146 |
|
| 1147 |
class TestContextInjection:
|
| 1148 |
"""Test context injection in tools."""
|
|
|
|
| 531 |
template = templates_dict["resource://{param}"]
|
| 532 |
assert template.tags == {"template", "test-tag"}
|
| 533 |
|
| 534 |
+
async def test_template_decorator_wildcard_param(self):
|
| 535 |
+
mcp = FastMCP()
|
| 536 |
+
|
| 537 |
+
@mcp.resource("resource://{param*}")
|
| 538 |
+
def template_resource(param: str) -> str:
|
| 539 |
+
return f"Template resource: {param}"
|
| 540 |
+
|
| 541 |
+
templates_dict = await mcp.get_resource_templates()
|
| 542 |
+
template = templates_dict["resource://{param*}"]
|
| 543 |
+
assert template.uri_template == "resource://{param*}"
|
| 544 |
+
assert template.name == "template_resource"
|
| 545 |
+
|
| 546 |
|
| 547 |
class TestPromptDecorator:
|
| 548 |
async def test_prompt_decorator(self):
|
|
|
|
| 1155 |
template = templates_dict["resource://{param}"]
|
| 1156 |
assert template.tags == {"template", "test-tag"}
|
| 1157 |
|
| 1158 |
+
async def test_template_decorator_wildcard_param(self):
|
| 1159 |
+
mcp = FastMCP()
|
| 1160 |
+
|
| 1161 |
+
@mcp.resource("resource://{param*}")
|
| 1162 |
+
def template_resource(param: str) -> str:
|
| 1163 |
+
return f"Template resource: {param}"
|
| 1164 |
+
|
| 1165 |
+
async with Client(mcp) as client:
|
| 1166 |
+
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 1167 |
+
assert isinstance(result[0], TextResourceContents)
|
| 1168 |
+
assert result[0].text == "Template resource: test/data"
|
| 1169 |
+
|
| 1170 |
+
async def test_templates_match_in_order_of_definition(self):
|
| 1171 |
+
"""
|
| 1172 |
+
If a wildcard template is defined first, it will take priority over another
|
| 1173 |
+
matching template.
|
| 1174 |
+
|
| 1175 |
+
"""
|
| 1176 |
+
mcp = FastMCP()
|
| 1177 |
+
|
| 1178 |
+
@mcp.resource("resource://{param*}")
|
| 1179 |
+
def template_resource(param: str) -> str:
|
| 1180 |
+
return f"Template resource 1: {param}"
|
| 1181 |
+
|
| 1182 |
+
@mcp.resource("resource://{x}/{y}")
|
| 1183 |
+
def template_resource_with_params(x: str, y: str) -> str:
|
| 1184 |
+
return f"Template resource 2: {x}/{y}"
|
| 1185 |
+
|
| 1186 |
+
async with Client(mcp) as client:
|
| 1187 |
+
result = await client.read_resource(AnyUrl("resource://a/b/c"))
|
| 1188 |
+
assert isinstance(result[0], TextResourceContents)
|
| 1189 |
+
assert result[0].text == "Template resource 1: a/b/c"
|
| 1190 |
+
|
| 1191 |
+
result = await client.read_resource(AnyUrl("resource://a/b"))
|
| 1192 |
+
assert isinstance(result[0], TextResourceContents)
|
| 1193 |
+
assert result[0].text == "Template resource 1: a/b"
|
| 1194 |
+
|
| 1195 |
+
async def test_templates_shadow_each_other_reorder(self):
|
| 1196 |
+
"""
|
| 1197 |
+
If a wildcard template is defined second, it will *not* take priority over
|
| 1198 |
+
another matching template.
|
| 1199 |
+
"""
|
| 1200 |
+
mcp = FastMCP()
|
| 1201 |
+
|
| 1202 |
+
@mcp.resource("resource://{x}/{y}")
|
| 1203 |
+
def template_resource_with_params(x: str, y: str) -> str:
|
| 1204 |
+
return f"Template resource 1: {x}/{y}"
|
| 1205 |
+
|
| 1206 |
+
@mcp.resource("resource://{param*}")
|
| 1207 |
+
def template_resource(param: str) -> str:
|
| 1208 |
+
return f"Template resource 2: {param}"
|
| 1209 |
+
|
| 1210 |
+
async with Client(mcp) as client:
|
| 1211 |
+
result = await client.read_resource(AnyUrl("resource://a/b/c"))
|
| 1212 |
+
assert isinstance(result[0], TextResourceContents)
|
| 1213 |
+
assert result[0].text == "Template resource 2: a/b/c"
|
| 1214 |
+
|
| 1215 |
+
result = await client.read_resource(AnyUrl("resource://a/b"))
|
| 1216 |
+
assert isinstance(result[0], TextResourceContents)
|
| 1217 |
+
assert result[0].text == "Template resource 1: a/b"
|
| 1218 |
+
|
| 1219 |
|
| 1220 |
class TestContextInjection:
|
| 1221 |
"""Test context injection in tools."""
|