mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Add wildcard params
This commit is contained in:
parent
f64dde1bbf
commit
4e076c7c8f
3 changed files with 130 additions and 7 deletions
|
|
@ -5,6 +5,8 @@ description: Expose data sources and dynamic content generators to your MCP clie
|
|||
icon: database
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
Resources represent data or files that an MCP client can read, and resource templates extend this concept by allowing clients to request dynamically generated resources based on parameters passed in the URI.
|
||||
|
||||
FastMCP simplifies defining both static and dynamic resources, primarily using the `@mcp.resource` decorator.
|
||||
|
|
@ -183,6 +185,8 @@ Use these when the content is static or sourced directly from a file/URL, bypass
|
|||
|
||||
#### Custom Resource Keys
|
||||
|
||||
<VersionBadge version="2.2.0" />
|
||||
|
||||
When adding resources directly with `mcp.add_resource()`, you can optionally provide a custom storage key:
|
||||
|
||||
```python
|
||||
|
|
@ -201,6 +205,10 @@ Note that this parameter is only available when using `add_resource()` directly
|
|||
|
||||
Resource Templates allow clients to request resources whose content depends on parameters embedded in the URI. Define a template using the **same `@mcp.resource` decorator**, but include `{parameter_name}` placeholders in the URI string and add corresponding arguments to your function signature.
|
||||
|
||||
Resource templates generate a new resource for each unique set of parameters, which means that resources can be dynamically created on-demand. For example, if the resource template `"user://profile/{name}"` is registered, MCP clients could request `"user://profile/ford"` or `"user://profile/marvin"` to retrieve either of those two user profiles as resources, without having to register each resource individually.
|
||||
|
||||
Here is a complete example that shows how to define two resource templates:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
|
|
@ -233,11 +241,61 @@ def get_repo_info(owner: str, repo: str) -> dict:
|
|||
}
|
||||
```
|
||||
|
||||
With these templates defined, clients can request:
|
||||
With these two templates defined, clients can request a variety of resources:
|
||||
- `weather://london/current` → Returns weather for London
|
||||
- `repos://fastmcp/docs/info` → Returns info about the fastmcp/docs repository
|
||||
- `weather://paris/current` → Returns weather for Paris
|
||||
- `repos://jlowin/fastmcp/info` → Returns info about the jlowin/fastmcp repository
|
||||
- `repos://prefecthq/prefect/info` → Returns info about the prefecthq/prefect repository
|
||||
|
||||
### Parameters and Default Values
|
||||
### Wildcard Parameters
|
||||
|
||||
<VersionBadge version="2.2.3" />
|
||||
|
||||
Resource templates support wildcard parameters that can match multiple path segments. While standard parameters (`{param}`) only match a single path segment and don't cross "/" boundaries, wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(name="DataServer")
|
||||
|
||||
# Standard parameter only matches one segment
|
||||
@mcp.resource("files://{filename}")
|
||||
def get_file(filename: str) -> str:
|
||||
"""Retrieves a file by name."""
|
||||
# Will only match files://<single-segment>
|
||||
return f"File content for: {filename}"
|
||||
|
||||
# Wildcard parameter can match multiple segments
|
||||
@mcp.resource("path://{filepath*}")
|
||||
def get_path_content(filepath: str) -> str:
|
||||
"""Retrieves content at a specific path."""
|
||||
# Can match path://docs/server/resources.mdx
|
||||
return f"Content at path: {filepath}"
|
||||
|
||||
# Mixing standard and wildcard parameters
|
||||
@mcp.resource("repo://{owner}/{path*}/template.py")
|
||||
def get_template_file(owner: str, path: str) -> dict:
|
||||
"""Retrieves a file from a specific repository and path, but
|
||||
only if the resource ends with `template.py`"""
|
||||
# Can match repo://jlowin/fastmcp/src/resources/template.py
|
||||
return {
|
||||
"owner": owner,
|
||||
"path": path + "/template.py",
|
||||
"content": f"File at {path}/template.py in {owner}'s repository"
|
||||
}
|
||||
```
|
||||
|
||||
Wildcard parameters are useful when:
|
||||
|
||||
- Working with file paths or hierarchical data
|
||||
- Creating APIs that need to capture variable-length path segments
|
||||
- Building URL-like patterns similar to REST APIs
|
||||
|
||||
Note that like regular parameters, each wildcard parameter must still be a named parameter in your function signature, and all required function parameters must appear in the URI template.
|
||||
|
||||
### Default Values
|
||||
|
||||
<VersionBadge version="2.2.0" />
|
||||
|
||||
When creating resource templates, FastMCP enforces two rules for the relationship between URI template parameters and function parameters:
|
||||
|
||||
|
|
@ -315,6 +373,8 @@ Templates provide a powerful way to expose parameterized data access points foll
|
|||
|
||||
### Custom Template Keys
|
||||
|
||||
<VersionBadge version="2.2.0" />
|
||||
|
||||
Similar to resources, you can provide custom keys when directly adding templates:
|
||||
|
||||
```python
|
||||
|
|
|
|||
|
|
@ -24,13 +24,16 @@ from fastmcp.utilities.types import _convert_set_defaults
|
|||
|
||||
|
||||
def build_regex(template: str) -> re.Pattern:
|
||||
# Escape all non-brace characters, then restore {var} placeholders
|
||||
parts = re.split(r"(\{[^}]+\})", template)
|
||||
pattern = ""
|
||||
for part in parts:
|
||||
if part.startswith("{") and part.endswith("}"):
|
||||
name = part[1:-1]
|
||||
pattern += f"(?P<{name}>[^/]+)"
|
||||
if name.endswith("*"):
|
||||
name = name[:-1]
|
||||
pattern += f"(?P<{name}>.+)"
|
||||
else:
|
||||
pattern += f"(?P<{name}>[^/]+)"
|
||||
else:
|
||||
pattern += re.escape(part)
|
||||
return re.compile(f"^{pattern}$")
|
||||
|
|
|
|||
|
|
@ -301,6 +301,23 @@ class TestResourceTemplate:
|
|||
class TestMatchUriTemplate:
|
||||
"""Test match_uri_template function."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uri, expected_params",
|
||||
[
|
||||
("test://a/b", None),
|
||||
("test://a/b/c", None),
|
||||
("test://a/x/b", {"x": "x"}),
|
||||
("test://a/x/y/b", None),
|
||||
],
|
||||
)
|
||||
def test_match_uri_template_single_param(
|
||||
self, uri: str, expected_params: dict[str, str]
|
||||
):
|
||||
"""Test that match_uri_template uses the slash delimiter."""
|
||||
uri_template = "test://a/{x}/b"
|
||||
result = match_uri_template(uri=uri, uri_template=uri_template)
|
||||
assert result == expected_params
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uri, expected_params",
|
||||
[
|
||||
|
|
@ -361,7 +378,7 @@ class TestMatchUriTemplate:
|
|||
("other+prefix+test://foo/test/123", None),
|
||||
],
|
||||
)
|
||||
def test_match_prefixed_uri_template(
|
||||
def test_match_uri_template_with_prefix(
|
||||
self, uri: str, expected_params: dict[str, str] | None
|
||||
):
|
||||
"""Test matching URIs against a template with a prefix."""
|
||||
|
|
@ -369,10 +386,53 @@ class TestMatchUriTemplate:
|
|||
result = match_uri_template(uri=uri, uri_template=uri_template)
|
||||
assert result == expected_params
|
||||
|
||||
def test_quoted_params(self):
|
||||
def test_match_uri_template_quoted_params(self):
|
||||
uri_template = "user://{name}/{email}"
|
||||
quoted_name = quote("John Doe", safe="")
|
||||
quoted_email = quote("john@example.com", safe="")
|
||||
uri = f"user://{quoted_name}/{quoted_email}"
|
||||
result = match_uri_template(uri=uri, uri_template=uri_template)
|
||||
assert result == {"name": "John Doe", "email": "john@example.com"}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uri, expected_params",
|
||||
[
|
||||
("test://a/b", None),
|
||||
("test://a/b/c", None),
|
||||
("test://a/x/b", {"x": "x"}),
|
||||
("test://a/x/y/b", {"x": "x/y"}),
|
||||
("bad-prefix://a/x/y/b", None),
|
||||
("test://a/x/y/z", None),
|
||||
],
|
||||
)
|
||||
def test_match_uri_template_wildcard_param(
|
||||
self, uri: str, expected_params: dict[str, str]
|
||||
):
|
||||
"""Test that match_uri_template uses the slash delimiter."""
|
||||
uri_template = "test://a/{x*}/b"
|
||||
result = match_uri_template(uri=uri, uri_template=uri_template)
|
||||
assert result == expected_params
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uri, expected_params",
|
||||
[
|
||||
("test://a/x/y/b/c/d", {"x": "x/y", "y": "c/d"}),
|
||||
("bad-prefix://a/x/y/b/c/d", None),
|
||||
("test://a/x/y/c/d", None),
|
||||
("test://a/x/b/y", {"x": "x", "y": "y"}),
|
||||
],
|
||||
)
|
||||
def test_match_uri_template_multiple_wildcard_params(
|
||||
self, uri: str, expected_params: dict[str, str]
|
||||
):
|
||||
"""Test that match_uri_template uses the slash delimiter."""
|
||||
uri_template = "test://a/{x*}/b/{y*}"
|
||||
result = match_uri_template(uri=uri, uri_template=uri_template)
|
||||
assert result == expected_params
|
||||
|
||||
def test_match_uri_template_wildcard_and_literal_param(self):
|
||||
"""Test that match_uri_template uses the slash delimiter."""
|
||||
uri = "test://a/x/y/b"
|
||||
uri_template = "test://a/{x*}/{y}"
|
||||
result = match_uri_template(uri=uri, uri_template=uri_template)
|
||||
assert result == {"x": "x/y", "y": "b"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue