1+ """Test that URL-encoded parameters are decoded in resource templates.
2+
3+ Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/973
4+ """
5+
6+ from mcp .server .fastmcp .resources import ResourceTemplate
7+
8+
9+ class TestUrlParameterDecoding :
10+ """Test URL parameter decoding in resource templates."""
11+
12+ def test_template_matches_decodes_space (self ):
13+ """Test that %20 is decoded to space."""
14+
15+ def search (query : str ) -> str : # pragma: no cover
16+ return f"Results for: { query } "
17+
18+ template = ResourceTemplate .from_function (
19+ fn = search ,
20+ uri_template = "search://{query}" ,
21+ name = "search" ,
22+ )
23+
24+ params = template .matches ("search://hello%20world" )
25+ assert params is not None
26+ assert params ["query" ] == "hello world"
27+
28+ def test_template_matches_decodes_accented_characters (self ):
29+ """Test that %C3%A9 is decoded to e with accent."""
30+
31+ def search (query : str ) -> str : # pragma: no cover
32+ return f"Results for: { query } "
33+
34+ template = ResourceTemplate .from_function (
35+ fn = search ,
36+ uri_template = "search://{query}" ,
37+ name = "search" ,
38+ )
39+
40+ params = template .matches ("search://caf%C3%A9" )
41+ assert params is not None
42+ assert params ["query" ] == "cafe" # encoded as UTF-8
43+
44+ def test_template_matches_decodes_complex_phrase (self ):
45+ """Test complex French phrase from the original issue."""
46+
47+ def search (query : str ) -> str : # pragma: no cover
48+ return f"Results for: { query } "
49+
50+ template = ResourceTemplate .from_function (
51+ fn = search ,
52+ uri_template = "search://{query}" ,
53+ name = "search" ,
54+ )
55+
56+ params = template .matches (
57+ "search://stick%20correcteur%20teint%C3%A9%20anti-imperfections"
58+ )
59+ assert params is not None
60+ assert params ["query" ] == "stick correcteur teinte anti-imperfections"
61+
62+ def test_template_matches_preserves_plus_sign (self ):
63+ """Test that plus sign remains as plus (not converted to space).
64+
65+ In URI encoding, %20 is space. Plus-as-space is only for
66+ application/x-www-form-urlencoded (HTML forms).
67+ """
68+
69+ def search (query : str ) -> str : # pragma: no cover
70+ return f"Results for: { query } "
71+
72+ template = ResourceTemplate .from_function (
73+ fn = search ,
74+ uri_template = "search://{query}" ,
75+ name = "search" ,
76+ )
77+
78+ params = template .matches ("search://hello+world" )
79+ assert params is not None
80+ assert params ["query" ] == "hello+world"
0 commit comments