Skip to content

Commit 4a9ea40

Browse files
fix: URL-decode parameters extracted from resource templates (#973)
Github-Issue: #973 Reported-by: codeonym
1 parent c017c89 commit 4a9ea40

File tree

2 files changed

+11
-8
lines changed

2 files changed

+11
-8
lines changed

src/mcp/server/fastmcp/resources/templates.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import re
77
from collections.abc import Callable
88
from typing import TYPE_CHECKING, Any
9+
from urllib.parse import unquote
910

1011
from pydantic import BaseModel, Field, validate_call
1112

@@ -83,12 +84,16 @@ def from_function(
8384
)
8485

8586
def matches(self, uri: str) -> dict[str, Any] | None:
86-
"""Check if URI matches template and extract parameters."""
87+
"""Check if URI matches template and extract parameters.
88+
89+
Extracted parameters are URL-decoded to handle percent-encoded characters.
90+
"""
8791
# Convert template to regex pattern
8892
pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
8993
match = re.match(f"^{pattern}$", uri)
9094
if match:
91-
return match.groupdict()
95+
# URL-decode all extracted parameter values
96+
return {key: unquote(value) for key, value in match.groupdict().items()}
9297
return None
9398

9499
async def create_resource(

tests/issues/test_973_url_decoding.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def search(query: str) -> str: # pragma: no cover
3939

4040
params = template.matches("search://caf%C3%A9")
4141
assert params is not None
42-
assert params["query"] == "cafe" # encoded as UTF-8
42+
assert params["query"] == "café"
4343

4444
def test_template_matches_decodes_complex_phrase(self):
4545
"""Test complex French phrase from the original issue."""
@@ -53,11 +53,9 @@ def search(query: str) -> str: # pragma: no cover
5353
name="search",
5454
)
5555

56-
params = template.matches(
57-
"search://stick%20correcteur%20teint%C3%A9%20anti-imperfections"
58-
)
56+
params = template.matches("search://stick%20correcteur%20teint%C3%A9%20anti-imperfections")
5957
assert params is not None
60-
assert params["query"] == "stick correcteur teinte anti-imperfections"
58+
assert params["query"] == "stick correcteur teinté anti-imperfections"
6159

6260
def test_template_matches_preserves_plus_sign(self):
6361
"""Test that plus sign remains as plus (not converted to space).
@@ -77,4 +75,4 @@ def search(query: str) -> str: # pragma: no cover
7775

7876
params = template.matches("search://hello+world")
7977
assert params is not None
80-
assert params["query"] == "hello+world"
78+
assert params["query"] == "hello+world"

0 commit comments

Comments
 (0)