Jeremiah Lowin commited on
Commit
b770cbf
·
1 Parent(s): 8f5c5b9

Add wildcard params

Browse files
docs/servers/resources.mdx CHANGED
@@ -5,6 +5,8 @@ description: Expose data sources and dynamic content generators to your MCP clie
5
  icon: database
6
  ---
7
 
 
 
8
  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.
9
 
10
  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
183
 
184
  #### Custom Resource Keys
185
 
 
 
186
  When adding resources directly with `mcp.add_resource()`, you can optionally provide a custom storage key:
187
 
188
  ```python
@@ -201,6 +205,10 @@ Note that this parameter is only available when using `add_resource()` directly
201
 
202
  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.
203
 
 
 
 
 
204
  ```python
205
  from fastmcp import FastMCP
206
 
@@ -233,11 +241,61 @@ def get_repo_info(owner: str, repo: str) -> dict:
233
  }
234
  ```
235
 
236
- With these templates defined, clients can request:
237
  - `weather://london/current` → Returns weather for London
238
- - `repos://fastmcp/docs/info` → Returns info about the fastmcp/docs repository
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
- ### Parameters and Default Values
 
 
 
 
 
 
 
 
 
 
241
 
242
  When creating resource templates, FastMCP enforces two rules for the relationship between URI template parameters and function parameters:
243
 
@@ -315,6 +373,8 @@ Templates provide a powerful way to expose parameterized data access points foll
315
 
316
  ### Custom Template Keys
317
 
 
 
318
  Similar to resources, you can provide custom keys when directly adding templates:
319
 
320
  ```python
 
5
  icon: database
6
  ---
7
 
8
+ import { VersionBadge } from "/snippets/version-badge.mdx"
9
+
10
  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.
11
 
12
  FastMCP simplifies defining both static and dynamic resources, primarily using the `@mcp.resource` decorator.
 
185
 
186
  #### Custom Resource Keys
187
 
188
+ <VersionBadge version="2.2.0" />
189
+
190
  When adding resources directly with `mcp.add_resource()`, you can optionally provide a custom storage key:
191
 
192
  ```python
 
205
 
206
  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.
207
 
208
+ 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.
209
+
210
+ Here is a complete example that shows how to define two resource templates:
211
+
212
  ```python
213
  from fastmcp import FastMCP
214
 
 
241
  }
242
  ```
243
 
244
+ With these two templates defined, clients can request a variety of resources:
245
  - `weather://london/current` → Returns weather for London
246
+ - `weather://paris/current` → Returns weather for Paris
247
+ - `repos://jlowin/fastmcp/info` → Returns info about the jlowin/fastmcp repository
248
+ - `repos://prefecthq/prefect/info` → Returns info about the prefecthq/prefect repository
249
+
250
+ ### Wildcard Parameters
251
+
252
+ <VersionBadge version="2.2.3" />
253
+
254
+ 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.
255
+
256
+ ```python
257
+ from fastmcp import FastMCP
258
+
259
+ mcp = FastMCP(name="DataServer")
260
+
261
+ # Standard parameter only matches one segment
262
+ @mcp.resource("files://{filename}")
263
+ def get_file(filename: str) -> str:
264
+ """Retrieves a file by name."""
265
+ # Will only match files://<single-segment>
266
+ return f"File content for: {filename}"
267
+
268
+ # Wildcard parameter can match multiple segments
269
+ @mcp.resource("path://{filepath*}")
270
+ def get_path_content(filepath: str) -> str:
271
+ """Retrieves content at a specific path."""
272
+ # Can match path://docs/server/resources.mdx
273
+ return f"Content at path: {filepath}"
274
+
275
+ # Mixing standard and wildcard parameters
276
+ @mcp.resource("repo://{owner}/{path*}/template.py")
277
+ def get_template_file(owner: str, path: str) -> dict:
278
+ """Retrieves a file from a specific repository and path, but
279
+ only if the resource ends with `template.py`"""
280
+ # Can match repo://jlowin/fastmcp/src/resources/template.py
281
+ return {
282
+ "owner": owner,
283
+ "path": path + "/template.py",
284
+ "content": f"File at {path}/template.py in {owner}'s repository"
285
+ }
286
+ ```
287
 
288
+ Wildcard parameters are useful when:
289
+
290
+ - Working with file paths or hierarchical data
291
+ - Creating APIs that need to capture variable-length path segments
292
+ - Building URL-like patterns similar to REST APIs
293
+
294
+ 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.
295
+
296
+ ### Default Values
297
+
298
+ <VersionBadge version="2.2.0" />
299
 
300
  When creating resource templates, FastMCP enforces two rules for the relationship between URI template parameters and function parameters:
301
 
 
373
 
374
  ### Custom Template Keys
375
 
376
+ <VersionBadge version="2.2.0" />
377
+
378
  Similar to resources, you can provide custom keys when directly adding templates:
379
 
380
  ```python
src/fastmcp/resources/template.py CHANGED
@@ -24,13 +24,16 @@ from fastmcp.utilities.types import _convert_set_defaults
24
 
25
 
26
  def build_regex(template: str) -> re.Pattern:
27
- # Escape all non-brace characters, then restore {var} placeholders
28
  parts = re.split(r"(\{[^}]+\})", template)
29
  pattern = ""
30
  for part in parts:
31
  if part.startswith("{") and part.endswith("}"):
32
  name = part[1:-1]
33
- pattern += f"(?P<{name}>[^/]+)"
 
 
 
 
34
  else:
35
  pattern += re.escape(part)
36
  return re.compile(f"^{pattern}$")
 
24
 
25
 
26
  def build_regex(template: str) -> re.Pattern:
 
27
  parts = re.split(r"(\{[^}]+\})", template)
28
  pattern = ""
29
  for part in parts:
30
  if part.startswith("{") and part.endswith("}"):
31
  name = part[1:-1]
32
+ if name.endswith("*"):
33
+ name = name[:-1]
34
+ pattern += f"(?P<{name}>.+)"
35
+ else:
36
+ pattern += f"(?P<{name}>[^/]+)"
37
  else:
38
  pattern += re.escape(part)
39
  return re.compile(f"^{pattern}$")
tests/resources/test_resource_template.py CHANGED
@@ -301,6 +301,23 @@ class TestResourceTemplate:
301
  class TestMatchUriTemplate:
302
  """Test match_uri_template function."""
303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  @pytest.mark.parametrize(
305
  "uri, expected_params",
306
  [
@@ -361,7 +378,7 @@ class TestMatchUriTemplate:
361
  ("other+prefix+test://foo/test/123", None),
362
  ],
363
  )
364
- def test_match_prefixed_uri_template(
365
  self, uri: str, expected_params: dict[str, str] | None
366
  ):
367
  """Test matching URIs against a template with a prefix."""
@@ -369,10 +386,53 @@ class TestMatchUriTemplate:
369
  result = match_uri_template(uri=uri, uri_template=uri_template)
370
  assert result == expected_params
371
 
372
- def test_quoted_params(self):
373
  uri_template = "user://{name}/{email}"
374
  quoted_name = quote("John Doe", safe="")
375
  quoted_email = quote("john@example.com", safe="")
376
  uri = f"user://{quoted_name}/{quoted_email}"
377
  result = match_uri_template(uri=uri, uri_template=uri_template)
378
  assert result == {"name": "John Doe", "email": "john@example.com"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  class TestMatchUriTemplate:
302
  """Test match_uri_template function."""
303
 
304
+ @pytest.mark.parametrize(
305
+ "uri, expected_params",
306
+ [
307
+ ("test://a/b", None),
308
+ ("test://a/b/c", None),
309
+ ("test://a/x/b", {"x": "x"}),
310
+ ("test://a/x/y/b", None),
311
+ ],
312
+ )
313
+ def test_match_uri_template_single_param(
314
+ self, uri: str, expected_params: dict[str, str]
315
+ ):
316
+ """Test that match_uri_template uses the slash delimiter."""
317
+ uri_template = "test://a/{x}/b"
318
+ result = match_uri_template(uri=uri, uri_template=uri_template)
319
+ assert result == expected_params
320
+
321
  @pytest.mark.parametrize(
322
  "uri, expected_params",
323
  [
 
378
  ("other+prefix+test://foo/test/123", None),
379
  ],
380
  )
381
+ def test_match_uri_template_with_prefix(
382
  self, uri: str, expected_params: dict[str, str] | None
383
  ):
384
  """Test matching URIs against a template with a prefix."""
 
386
  result = match_uri_template(uri=uri, uri_template=uri_template)
387
  assert result == expected_params
388
 
389
+ def test_match_uri_template_quoted_params(self):
390
  uri_template = "user://{name}/{email}"
391
  quoted_name = quote("John Doe", safe="")
392
  quoted_email = quote("john@example.com", safe="")
393
  uri = f"user://{quoted_name}/{quoted_email}"
394
  result = match_uri_template(uri=uri, uri_template=uri_template)
395
  assert result == {"name": "John Doe", "email": "john@example.com"}
396
+
397
+ @pytest.mark.parametrize(
398
+ "uri, expected_params",
399
+ [
400
+ ("test://a/b", None),
401
+ ("test://a/b/c", None),
402
+ ("test://a/x/b", {"x": "x"}),
403
+ ("test://a/x/y/b", {"x": "x/y"}),
404
+ ("bad-prefix://a/x/y/b", None),
405
+ ("test://a/x/y/z", None),
406
+ ],
407
+ )
408
+ def test_match_uri_template_wildcard_param(
409
+ self, uri: str, expected_params: dict[str, str]
410
+ ):
411
+ """Test that match_uri_template uses the slash delimiter."""
412
+ uri_template = "test://a/{x*}/b"
413
+ result = match_uri_template(uri=uri, uri_template=uri_template)
414
+ assert result == expected_params
415
+
416
+ @pytest.mark.parametrize(
417
+ "uri, expected_params",
418
+ [
419
+ ("test://a/x/y/b/c/d", {"x": "x/y", "y": "c/d"}),
420
+ ("bad-prefix://a/x/y/b/c/d", None),
421
+ ("test://a/x/y/c/d", None),
422
+ ("test://a/x/b/y", {"x": "x", "y": "y"}),
423
+ ],
424
+ )
425
+ def test_match_uri_template_multiple_wildcard_params(
426
+ self, uri: str, expected_params: dict[str, str]
427
+ ):
428
+ """Test that match_uri_template uses the slash delimiter."""
429
+ uri_template = "test://a/{x*}/b/{y*}"
430
+ result = match_uri_template(uri=uri, uri_template=uri_template)
431
+ assert result == expected_params
432
+
433
+ def test_match_uri_template_wildcard_and_literal_param(self):
434
+ """Test that match_uri_template uses the slash delimiter."""
435
+ uri = "test://a/x/y/b"
436
+ uri_template = "test://a/{x*}/{y}"
437
+ result = match_uri_template(uri=uri, uri_template=uri_template)
438
+ assert result == {"x": "x/y", "y": "b"}