GEMMA DEVELOPER AGENT
SAVED EVIDENCE
← 31B real rollouts · 480 (exact stack, 32 lanes, 13-Nov)
ROLLOUT 5 · modelcontextprotocol/python-sdk

modelcontextprotocol__python-sdk-1864

Solved
405 stotal time
16tool calls
19model calls
4,661tokens generated
165,672prompt tokens read

No error recorded

⚠ The request log has 18 model calls but the trace has 20 agent calls, so tokens are listed separately instead of beside each turn.

Turn by turn

Token shading: p ≥ 0.9 0.5 – 0.9 0.2 – 0.5 below 0.2. Hover a token for its id and probability.

System instructions 4,822 characters
You are a principal Python software engineer resolving a repository issue in `/workspace`.
Your sole objective is to produce a minimal, root-cause source patch that passes every hidden fail-to-pass and pass-to-pass regression test on the first grading run.

## Critical Environment & Grader Rules
1. **Air-Gapped Sandbox & Available CLI Tools**: No network access. Never run `pip`, `uv`, `conda`, or `git fetch`. Note: `rg` (`ripgrep`) is NOT installed in the container — always use `grep -rn --include="*.py"` for fast exact searches so you never waste a tool call on `command not found`.
2. **Protected Grading Paths**: Before running official verification, the grading harness **strips and reverts all changes** to `tests/`, `test/`, `testing/`, `test_*.py`, `*_test.py`, `conftest.py`, `pyproject.toml`, `setup.cfg`, `pytest.ini`, and `tox.ini`. Every part of your fix MUST reside in non-test production source files.
3. **Untracked File Trap (`git add -N .`)**: `submit_patch` stages ALL untracked files inside `/workspace`! Never leave reproduction or scratch files inside `/workspace` — run reproductions inline via `python3 - <<'PY'` or inside `/tmp/repro.py` in a single `run_command`.
4. **Stdout Truncation Rule**: `run_command` truncates stdout after the first 5,000 characters from the TOP. Always run pytest quietly with short tracebacks:
   `pytest path/to/test_file.py -k pattern -q --tb=short --no-header`
   If a test file is large, redirect output to `/tmp/pytest.log` and inspect `tail -n 80 /tmp/pytest.log` so failure tracebacks at the bottom are never cut off.
5. **Strict Pacing Discipline**: `get_status` and `submit_patch` are free and do NOT count toward `max_tool_calls`. Target **16-22 counted tool calls total**, and **begin your first `edit_file` change no later than tool call 15**. Reserve at least 4 calls for verification, `git status --short && git diff`, and submission.

## Execution Protocol

### Phase 1 — Pinpoint Code & Tests Immediately (1-2 tool calls)
- The user message already contains `Workspace Layout` — do NOT waste a turn running `ls` on top-level directories.
- Run a single compound `run_command` with `grep -rn --include="*.py"` to locate:
  (a) the exact symbols, error strings, or functions from the problem description,
  (b) **all parallel or sibling implementations** (for example sync + async variants, `Client` + `AsyncClient`, serializer + deserializer, multiple backends/dialects, base + subclass methods, `__init__.py` / `__all__` exports), and
  (c) the closest existing unit test files under `tests/`.

### Phase 2 — Deep Read & Spec Anchor (2-5 tool calls)
- Use `read_file` with generous `start_line` and `end_line` windows (180-300 lines per call so you read whole modules in fewer calls) on the target function/class, callers, type annotations, and neighboring tests.
- Before editing, verify every explicit and implicit requirement from the issue: exact parameter names, defaults, boundary/domain edge cases, exception types/messages, `None` vs empty collection vs falsy (`0`, `False`, `""`) semantics, mutability/copying, and return types.

### Phase 3 — Reproduce Bug in One Call (1 tool call)
- Run ONE concise command (`python3 - <<'PY' ... PY` or targeted `pytest ... -q --tb=short --no-header`) via `run_command` to observe the exact pre-fix failure. Do NOT spend multiple tool calls writing separate reproduction scripts.

### Phase 4 — Surgical Implementation Across All Parallel Paths (1-4 tool calls)
- Begin editing by tool call 15! Use `edit_file` with 4-6 lines of exact surrounding context copied from `read_file`.
- Update ALL parallel codepaths (sync/async, multiple classes/dialects, type hints) consistently so partial fixes do not fail sibling tests.
- **2-Strike Edit Rule**: If `edit_file` fails twice on the same block due to whitespace or escaping, do not retry `edit_file` a third time — apply an atomic Python transformation via `run_command` and immediately verify `python3 -m py_compile path/to/file.py`.

### Phase 5 — Edge-Case Verification, Diff Hygiene & Clean Exit (2-4 tool calls)
- Run a single compound `run_command` testing both your reproduction + boundary edge cases (`None`, empty input, leading/trailing delimiters, subdomain/prefix boundaries, subclass/async parity) AND the relevant existing `pytest` module(s) with `-q --tb=short --no-header`.
- Run `git status --short && git diff` via `run_command`. Confirm:
  - ZERO untracked (`??`) files in `/workspace` (`rm -f` any untracked workspace files immediately),
  - ZERO modifications to `tests/` or protected config files (`git checkout -- tests/` if touched),
  - Minimal, clean diff with valid imports and indentation.
- Call `submit_patch` once verification passes, then respond with a brief 2-sentence plain-text summary and make NO further tool calls.
Task prompt 3,632 characters
You are evaluating a software engineering task for repository modelcontextprotocol/python-sdk.

Problem Statement:
URL Parameters Not Decoded in Dynamic Resources
### Initial Checks

- [ ] I confirm that I'm using the latest version of MCP Python SDK
- [x] I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue

### Description

Dynamic URL resource parameters in MCP protocol are not automatically URL-decoded, causing encoded characters (like %20 for spaces and %C3%A9 for accented characters) to be passed as-is to resource handlers instead of their decoded equivalents.


Here is a simple log for the bug: 
- **before**
```
2025-06-17 14:43:44,385 [INFO] kn-graph: Found 0 products matching 'stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc' - (logger.py:226)
```

- **after explicit decode**
```
2025-06-17 14:55:10,173 [INFO] kn-graph: Found 10 products matching 'stick correcteur teinté anti-imperfections rechargeable au zinc' - (logger.py:226)
```

### Example Code

```Python
@mcp.resource("products://search/{product_identifier}")
    def find_similar_products(product_identifier: str):
        """
        Find similar products based on a given product identifier.
        :param product_identifier: The identifier of the product (product name or code).
        :return: Similar products or an error message if none are found.
        """
        print(product_identifier)
        """
         -- walkaround: explicit decoding
        from urllib.parse import unquote
        print(unquote(product_identifier))
        """
```

### Python & MCP Python SDK

```Text
# uv pip list | grep "mcp"
mcp                          1.9.2
# python --version
Python 3.13.4
```

## Task Budget (Session terminates when any budget is exhausted)
- Time allowance: 7.5 minutes
- Tool calls allowance: 30 calls
- Max loop iterations: 40 turns

## Execution Environment Rules
- Single command timeout: 90 seconds (commands exceeding this fail without ending the session)
- Command output limit: 5000 characters
- File view limit: 150 lines per read_file call
- File character limit: 10000 characters per read_file call
- Environment is offline (no network/PyPI access). All repository and test dependencies are ALREADY pre-installed. Do NOT attempt to run pip install or download packages.

## Instructions:
0. All source code is under `/workspace`. Do NOT search outside `/workspace` (e.g. `/usr/`, `/wheels/`, system site-packages). If imports fail, the issue is in the source code under `/workspace`, not in missing system packages.
1. Analyze the problem statement and any provided hints carefully to identify all requested script paths, CLI subcommands, or Python modules.
2. Inspect existing codebase conventions and test files before making edits.
3. Verify your implementation using targeted tests or inline assertions before submitting.
4. Call submit_patch only after your implementation is complete and verified.
5. As your final action, you must return a text-only response reporting your completion to terminate the session.
## Workspace Layout
The repository is located at `/workspace`. Here is the directory tree (up to 3 levels):
```
./CLAUDE.md
./CODE_OF_CONDUCT.md
./CONTRIBUTING.md
./LICENSE
./README.md
./RELEASE.md
./SECURITY.md
./docs
./examples
./mkdocs.yml
./pyproject.toml
./scripts
./scripts/test
./scripts/update_readme_snippets.py
./src
./src/mcp
./src/mcp/__init__.py
./src/mcp/cli
./src/mcp/client
./src/mcp/os
./src/mcp/py.typed
./src/mcp/server
./src/mcp/shared
./src/mcp/types.py
./tests
./uv.lock
```
Turn 1at 20 s180 tokens out3,031 prompt tokens
The user is reporting a bug where URL parameters in dynamic resources are not automatically URL-decoded in the MCP Python SDK.
Specifically, when a resource handler is defined with a template like `products://search/{product_identifier}`, the `product_identifier` passed to the handler is still URL-encoded (e.g., `%20` instead of space).

I need to find where the resource handler is invoked and where the template parameters are extracted.

First, I'll search for where resources are handled and how the parameters are parsed. I'll look for `mcp.resource` or similar decorators and how they are implemented.
TOOL CALL · run_command
{
  "command": "grep -rn --include=\"*.py\" \"resource\" src/mcp | grep -v \"tests\" | head -n 20"
}
Result of run_command 1,902 characters
{
  "status": "ok",
  "stdout": "src/mcp/shared/auth_utils.py:9:def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str:\nsrc/mcp/shared/auth_utils.py:10:    \"\"\"Convert server URL to canonical resource URL per RFC 8707.\nsrc/mcp/shared/auth_utils.py:12:    RFC 8707 section 2 states that resource URIs \"MUST NOT include a fragment component\".\nsrc/mcp/shared/auth_utils.py:19:        Canonical resource URL string\nsrc/mcp/shared/auth_utils.py:31:def check_resource_allowed(requested_resource: str, configured_resource: str) -> bool:\nsrc/mcp/shared/auth_utils.py:32:    \"\"\"Check if a requested resource URL matches a configured resource URL.\nsrc/mcp/shared/auth_utils.py:34:    A requested resource matches if it has the same scheme, domain, port,\nsrc/mcp/shared/auth_utils.py:35:    and its path starts with the configured resource's path. This allows\nsrc/mcp/shared/auth_utils.py:36:    hierarchical matching where a token for a parent resource can be used\nsrc/mcp/shared/auth_utils.py:37:    for child resources.\nsrc/mcp/shared/auth_utils.py:40:        requested_resource: The resource URL being requested\nsrc/mcp/shared/auth_utils.py:41:        configured_resource: The resource URL that has been configured\nsrc/mcp/shared/auth_utils.py:44:        True if the requested resource matches the configured resource\nsrc/mcp/shared/auth_utils.py:47:    requested = urlparse(requested_resource)\nsrc/mcp/shared/auth_utils.py:48:    configured = urlparse(configured_resource)\nsrc/mcp/shared/auth.py:144:    resource: AnyHttpUrl\nsrc/mcp/shared/auth.py:149:    resource_signing_alg_values_supported: list[str] | None = None\nsrc/mcp/shared/auth.py:150:    resource_name: str | None = None\nsrc/mcp/shared/auth.py:151:    resource_documentation: AnyHttpUrl | None = None\nsrc/mcp/shared/auth.py:152:    resource_policy_uri: AnyHttpUrl | None = None\n",
  "stderr": "",
  "exit_code": 0
}
Turn 2at 25 s59 tokens out3,868 prompt tokens
The previous `grep` didn't show the decorator implementation. I'll search for `resource(` to find the decorator definition.
TOOL CALL · run_command
{
  "command": "grep -rn --include=\"*.py\" \"def resource\" src/mcp"
}
Result of run_command 309 characters
{
  "status": "ok",
  "stdout": "src/mcp/shared/auth_utils.py:9:def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str:\nsrc/mcp/server/fastmcp/server.py:519:    def resource(\nsrc/mcp/client/session_group.py:184:    def resources(self) -> dict[str, types.Resource]:\n",
  "stderr": "",
  "exit_code": 0
}
Turn 3at 34 s84 tokens out4,064 prompt tokens
It seems `src/mcp/server/fastmcp/server.py` contains the `resource` decorator. Let's examine that file around line 519.
TOOL CALL · read_file
{
  "end_line": 650,
  "filepath": "src/mcp/server/fastmcp/server.py",
  "start_line": 500
}
Result of read_file 5,960 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/fastmcp/server.py",
  "content": "\n        Example:\n            @mcp.completion()\n            async def handle_completion(ref, argument, context):\n                if isinstance(ref, ResourceTemplateReference):\n                    # Return completions based on ref, argument, and context\n                    return Completion(values=[\"option1\", \"option2\"])\n                return None\n        \"\"\"\n        return self._mcp_server.completion()\n\n    def add_resource(self, resource: Resource) -> None:\n        \"\"\"Add a resource to the server.\n\n        Args:\n            resource: A Resource instance to add\n        \"\"\"\n        self._resource_manager.add_resource(resource)\n\n    def resource(\n        self,\n        uri: str,\n        *,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        mime_type: str | None = None,\n        icons: list[Icon] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n    ) -> Callable[[AnyFunction], AnyFunction]:\n        \"\"\"Decorator to register a function as a resource.\n\n        The function will be called when the resource is read to generate its content.\n        The function can return:\n        - str for text content\n        - bytes for binary content\n        - other types will be converted to JSON\n\n        If the URI contains parameters (e.g. \"resource://{param}\") or the function\n        has parameters, it will be registered as a template resource.\n\n        Args:\n            uri: URI for the resource (e.g. \"resource://my-resource\" or \"resource://{param}\")\n            name: Optional name for the resource\n            title: Optional human-readable title for the resource\n            description: Optional description of the resource\n            mime_type: Optional MIME type for the resource\n            meta: Optional metadata dictionary for the resource\n\n        Example:\n            @server.resource(\"resource://my-resource\")\n            def get_data() -> str:\n                return \"Hello, world!\"\n\n            @server.resource(\"resource://my-resource\")\n            async get_data() -> str:\n                data = await fetch_data()\n                return f\"Hello, world! {data}\"\n\n            @server.resource(\"resource://{city}/weather\")\n            def get_weather(city: str) -> str:\n                return f\"Weather for {city}\"\n\n            @server.resource(\"resource://{city}/weather\")\n            async def get_weather(city: str) -> str:\n                data = await fetch_weather(city)\n                return f\"Weather for {city}: {data}\"\n        \"\"\"\n        # Check if user passed function directly instead of calling decorator\n        if callable(uri):\n            raise TypeError(\n                \"The @resource decorator was used incorrectly. \"\n                \"Did you forget to call it? Use @resource('uri') instead of @resource\"\n            )\n\n        def decorator(fn: AnyFunction) -> AnyFunction:\n            # Check if this should be a template\n            sig = inspect.signature(fn)\n            has_uri_params = \"{\" in uri and \"}\" in uri\n            has_func_params = bool(sig.parameters)\n\n            if has_uri_params or has_func_params:\n                # Check for Context parameter to exclude from validation\n                context_param = find_context_parameter(fn)\n\n                # Validate that URI params match function params (excluding context)\n                uri_params = set(re.findall(r\"{(\\w+)}\", uri))\n                # We need to remove the context_param from the resource function if\n                # there is any.\n                func_params = {p for p in sig.parameters.keys() if p != context_param}\n\n                if uri_params != func_params:\n                    raise ValueError(\n                        f\"Mismatch between URI parameters {uri_params} and function parameters {func_params}\"\n                    )\n\n                # Register as template\n                self._resource_manager.add_template(\n                    fn=fn,\n                    uri_template=uri,\n                    name=name,\n                    title=title,\n                    description=description,\n                    mime_type=mime_type,\n                    icons=icons,\n                    annotations=annotations,\n                    meta=meta,\n                )\n            else:\n                # Register as regular resource\n                resource = FunctionResource.from_function(\n                    fn=fn,\n                    uri=uri,\n                    name=name,\n                    title=title,\n                    description=description,\n                    mime_type=mime_type,\n                    icons=icons,\n                    annotations=annotations,\n                    meta=meta,\n                )\n                self.add_resource(resource)\n            return fn\n\n        return decorator\n\n    def add_prompt(self, prompt: Prompt) -> None:\n        \"\"\"Add a prompt to the server.\n\n        Args:\n            prompt: A Prompt instance to add\n        \"\"\"\n        self._prompt_manager.add_prompt(prompt)\n\n    def prompt(\n        self,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n    ) -> Callable[[AnyFunction], AnyFunction]:\n        \"\"\"Decorator to register a prompt.\n\n        Args:\n            name: Optional name for the prompt (defaults to function name)\n            title: Optional human-readable title for the prompt\n            description: Optional description of what the prompt does\n\n        Example:",
  "start_line": 500,
  "end_line": 649,
  "total_lines": 1298,
  "is_truncated": true
}
Turn 4at 41 s83 tokens out5,583 prompt tokens
The `resource` decorator calls `self._resource_manager.add_template`. I need to find where the `ResourceManager` is defined and how it handles template resources when they are read.

Let's find the definition of `ResourceManager`.
TOOL CALL · run_command
{
  "command": "grep -rn --include=\"*.py\" \"class ResourceManager\" src/mcp"
}
Result of run_command 140 characters
{
  "status": "ok",
  "stdout": "src/mcp/server/fastmcp/resources/resource_manager.py:23:class ResourceManager:\n",
  "stderr": "",
  "exit_code": 0
}
Turn 5at 44 s58 tokens out5,730 prompt tokens
Let's examine `src/mcp/server/fastmcp/resources/resource_manager.py`.
TOOL CALL · read_file
{
  "filepath": "src/mcp/server/fastmcp/resources/resource_manager.py"
}
Result of read_file 4,364 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/fastmcp/resources/resource_manager.py",
  "content": "\"\"\"Resource manager functionality.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\n\nfrom pydantic import AnyUrl\n\nfrom mcp.server.fastmcp.resources.base import Resource\nfrom mcp.server.fastmcp.resources.templates import ResourceTemplate\nfrom mcp.server.fastmcp.utilities.logging import get_logger\nfrom mcp.types import Annotations, Icon\n\nif TYPE_CHECKING:\n    from mcp.server.fastmcp.server import Context\n    from mcp.server.session import ServerSessionT\n    from mcp.shared.context import LifespanContextT, RequestT\n\nlogger = get_logger(__name__)\n\n\nclass ResourceManager:\n    \"\"\"Manages FastMCP resources.\"\"\"\n\n    def __init__(self, warn_on_duplicate_resources: bool = True):\n        self._resources: dict[str, Resource] = {}\n        self._templates: dict[str, ResourceTemplate] = {}\n        self.warn_on_duplicate_resources = warn_on_duplicate_resources\n\n    def add_resource(self, resource: Resource) -> Resource:\n        \"\"\"Add a resource to the manager.\n\n        Args:\n            resource: A Resource instance to add\n\n        Returns:\n            The added resource. If a resource with the same URI already exists,\n            returns the existing resource.\n        \"\"\"\n        logger.debug(\n            \"Adding resource\",\n            extra={\n                \"uri\": resource.uri,\n                \"type\": type(resource).__name__,\n                \"resource_name\": resource.name,\n            },\n        )\n        existing = self._resources.get(str(resource.uri))\n        if existing:\n            if self.warn_on_duplicate_resources:\n                logger.warning(f\"Resource already exists: {resource.uri}\")\n            return existing\n        self._resources[str(resource.uri)] = resource\n        return resource\n\n    def add_template(\n        self,\n        fn: Callable[..., Any],\n        uri_template: str,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        mime_type: str | None = None,\n        icons: list[Icon] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n    ) -> ResourceTemplate:\n        \"\"\"Add a template from a function.\"\"\"\n        template = ResourceTemplate.from_function(\n            fn,\n            uri_template=uri_template,\n            name=name,\n            title=title,\n            description=description,\n            mime_type=mime_type,\n            icons=icons,\n            annotations=annotations,\n            meta=meta,\n        )\n        self._templates[template.uri_template] = template\n        return template\n\n    async def get_resource(\n        self,\n        uri: AnyUrl | str,\n        context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,\n    ) -> Resource | None:\n        \"\"\"Get resource by URI, checking concrete resources first, then templates.\"\"\"\n        uri_str = str(uri)\n        logger.debug(\"Getting resource\", extra={\"uri\": uri_str})\n\n        # First check concrete resources\n        if resource := self._resources.get(uri_str):\n            return resource\n\n        # Then check templates\n        for template in self._templates.values():\n            if params := template.matches(uri_str):\n                try:\n                    return await template.create_resource(uri_str, params, context=context)\n                except Exception as e:  # pragma: no cover\n                    raise ValueError(f\"Error creating resource from template: {e}\")\n\n        raise ValueError(f\"Unknown resource: {uri}\")\n\n    def list_resources(self) -> list[Resource]:\n        \"\"\"List all registered resources.\"\"\"\n        logger.debug(\"Listing resources\", extra={\"count\": len(self._resources)})\n        return list(self._resources.values())\n\n    def list_templates(self) -> list[ResourceTemplate]:\n        \"\"\"List all registered templates.\"\"\"\n        logger.debug(\"Listing templates\", extra={\"count\": len(self._templates)})\n        return list(self._templates.values())",
  "start_line": 1,
  "end_line": 115,
  "total_lines": 115,
  "is_truncated": false
}
Turn 6at 55 s179 tokens out6,959 prompt tokens
In `ResourceManager.get_resource`, the code does:

```python
        # Then check templates
        for template in self._templates.values():
            if params := template.matches(uri_str):
                try:
                    return await template.create_resource(uri_str, params, context=context)
                except Exception as e:  # pragma: no cover
                    raise ValueError(f\"Error creating resource from template: {e}\")
```

I need to see how `ResourceTemplate.matches` and `ResourceTemplate.create_resource` are implemented. They are in `src/mcp/server/fastmcp/resources/templates.py`.
TOOL CALL · read_file
{
  "filepath": "src/mcp/server/fastmcp/resources/templates.py"
}
Result of read_file 5,308 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/fastmcp/resources/templates.py",
  "content": "\"\"\"Resource template functionality.\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nimport re\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\n\nfrom pydantic import BaseModel, Field, validate_call\n\nfrom mcp.server.fastmcp.resources.types import FunctionResource, Resource\nfrom mcp.server.fastmcp.utilities.context_injection import find_context_parameter, inject_context\nfrom mcp.server.fastmcp.utilities.func_metadata import func_metadata\nfrom mcp.types import Annotations, Icon\n\nif TYPE_CHECKING:\n    from mcp.server.fastmcp.server import Context\n    from mcp.server.session import ServerSessionT\n    from mcp.shared.context import LifespanContextT, RequestT\n\n\nclass ResourceTemplate(BaseModel):\n    \"\"\"A template for dynamically creating resources.\"\"\"\n\n    uri_template: str = Field(description=\"URI template with parameters (e.g. weather://{city}/current)\")\n    name: str = Field(description=\"Name of the resource\")\n    title: str | None = Field(description=\"Human-readable title of the resource\", default=None)\n    description: str | None = Field(description=\"Description of what the resource does\")\n    mime_type: str = Field(default=\"text/plain\", description=\"MIME type of the resource content\")\n    icons: list[Icon] | None = Field(default=None, description=\"Optional list of icons for the resource template\")\n    annotations: Annotations | None = Field(default=None, description=\"Optional annotations for the resource template\")\n    meta: dict[str, Any] | None = Field(default=None, description=\"Optional metadata for this resource template\")\n    fn: Callable[..., Any] = Field(exclude=True)\n    parameters: dict[str, Any] = Field(description=\"JSON schema for function parameters\")\n    context_kwarg: str | None = Field(None, description=\"Name of the kwarg that should receive context\")\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        uri_template: str,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        mime_type: str | None = None,\n        icons: list[Icon] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n        context_kwarg: str | None = None,\n    ) -> ResourceTemplate:\n        \"\"\"Create a template from a function.\"\"\"\n        func_name = name or fn.__name__\n        if func_name == \"<lambda>\":\n            raise ValueError(\"You must provide a name for lambda functions\")  # pragma: no cover\n\n        # Find context parameter if it exists\n        if context_kwarg is None:  # pragma: no branch\n            context_kwarg = find_context_parameter(fn)\n\n        # Get schema from func_metadata, excluding context parameter\n        func_arg_metadata = func_metadata(\n            fn,\n            skip_names=[context_kwarg] if context_kwarg is not None else [],\n        )\n        parameters = func_arg_metadata.arg_model.model_json_schema()\n\n        # ensure the arguments are properly cast\n        fn = validate_call(fn)\n\n        return cls(\n            uri_template=uri_template,\n            name=func_name,\n            title=title,\n            description=description or fn.__doc__ or \"\",\n            mime_type=mime_type or \"text/plain\",\n            icons=icons,\n            annotations=annotations,\n            meta=meta,\n            fn=fn,\n            parameters=parameters,\n            context_kwarg=context_kwarg,\n        )\n\n    def matches(self, uri: str) -> dict[str, Any] | None:\n        \"\"\"Check if URI matches template and extract parameters.\"\"\"\n        # Convert template to regex pattern\n        pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")\n        match = re.match(f\"^{pattern}$\", uri)\n        if match:\n            return match.groupdict()\n        return None\n\n    async def create_resource(\n        self,\n        uri: str,\n        params: dict[str, Any],\n        context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,\n    ) -> Resource:\n        \"\"\"Create a resource from the template with the given parameters.\"\"\"\n        try:\n            # Add context to params if needed\n            params = inject_context(self.fn, params, context, self.context_kwarg)\n\n            # Call function and check if result is a coroutine\n            result = self.fn(**params)\n            if inspect.iscoroutine(result):\n                result = await result\n\n            return FunctionResource(\n                uri=uri,  # type: ignore\n                name=self.name,\n                title=self.title,\n                description=self.description,\n                mime_type=self.mime_type,\n                icons=self.icons,\n                annotations=self.annotations,\n                meta=self.meta,\n                fn=lambda: result,  # Capture result in closure\n            )\n        except Exception as e:\n            raise ValueError(f\"Error creating resource from template: {e}\")",
  "start_line": 1,
  "end_line": 122,
  "total_lines": 122,
  "is_truncated": false
}
Turn 7at 111 s771 tokens out8,562 prompt tokens
The issue is in `ResourceTemplate.matches`.

```python
    def matches(self, uri: str) -> dict[str, Any] | None:
        \"\"\"Check if URI matches template and extract parameters.\"\"\"
        # Convert template to regex pattern
        pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")
        match = re.match(f\"^{pattern}$\", uri)
        if match:
            return match.groupdict()
        return None
```

The `match.groupdict()` returns the raw extracted strings from the URI, which are still URL-encoded. These are then passed to `self.fn(**params)` in `create_resource`.

I should URL-decode the parameters extracted in `matches`.

Wait, if I decode them in `matches`, then `params` will contain decoded values.
`create_resource` does:
```python
    async def create_resource(
        self,
        uri: str,
        params: dict[str, Any],
        context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
    ) -> Resource:
        \"\"\"Create a resource from the template with the given parameters.\"\"\"
        try:
            # Add context to params if needed
            params = inject_context(self.fn, params, context, self.context_kwarg)

            # Call function and check if result is a coroutine
            result = self.fn(**params)
```

So if I decode in `matches`, the decoded values will be passed to the function.

Let's create a reproduction script to confirm this.
TOOL CALL · run_command
{
  "command": "python3 - <<'PY'\nimport asyncio\nfrom unittest.mock import MagicMock\nfrom urllib.parse import quote\nfrom mcp.server.fastmcp.resources.templates import ResourceTemplate\n\nasync def test_resource_template_decoding():\n    async def handle_resource(product_identifier: str):\n        return f\"Product: {product_identifier}\"\n\n    template = ResourceTemplate.from_function(\n        fn=handle_resource,\n        uri_template=\"products://search/{product_identifier}\"\n    )\n\n    # Test case: encoded space and accented character\n    product_id = \"stick correcteur teinté\"\n    encoded_id = quote(product_id)\n    uri = f\"products://search/{encoded_id}\"\n\n    params = template.matches(uri)\n    print(f\"Params extracted: {params}\")\n    \n    # We need to simulate the call from create_resource\n    # Normally ResourceTemplate.create_resource is async and calls the fn\n    # For simplicity, let's just see what matches returns\n    if params:\n        print(f\"Extracted id: {params['product_identifier']}\")\n        assert params['product_identifier'] == product_id, f\"Expected {product_id}, got {params['product_identifier']}\"\n    else:\n        print(\"URI did not match template\")\n        assert False, \"URI should have matched template\"\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(test_resource_template_decoding())\n        print(\"Test passed!\")\n    except Exception as e:\n        print(f\"Test failed: {e}\")\n        exit(1)\nPY\n"
}
Result of run_command 559 characters
{
  "status": "error",
  "error_type": "CommandError",
  "error_message": "Params extracted: {'product_identifier': 'stick%20correcteur%20teint%C3%A9'}\nExtracted id: stick%20correcteur%20teint%C3%A9\nTest failed: Expected stick correcteur teinté, got stick%20correcteur%20teint%C3%A9\n",
  "details": {
    "stdout": "Params extracted: {'product_identifier': 'stick%20correcteur%20teint%C3%A9'}\nExtracted id: stick%20correcteur%20teint%C3%A9\nTest failed: Expected stick correcteur teinté, got stick%20correcteur%20teint%C3%A9\n",
    "stderr": "",
    "exit_code": 1
  }
}
Turn 8at 161 s832 tokens out9,554 prompt tokens
The reproduction script confirms the bug. `template.matches(uri)` returns the URL-encoded string.

I should use `urllib.parse.unquote` to decode the extracted parameters.

I'll modify `src/mcp/server/fastmcp/resources/templates.py` to decode the values in `matches`.

Wait, should I decode in `matches` or in `create_resource`?
If I decode in `matches`, `matches` return value changes from encoded to decoded.
`matches` is used in `ResourceManager.get_resource`:

```python
        # Then check templates
        for template in self._templates.values():
            if params := template.matches(uri_str):
                try:
                    return await template.create_resource(uri_str, params, context=context)
```

If I decode in `matches`, `params` are already decoded when passed to `create_resource`.

Is there any other place `matches` is used?
Let's check `src/mcp/server/fastmcp/resources/templates.py` for other usages of `matches` or other methods.
`matches` is only used in `ResourceManager.get_resource`.

However, let's see if any other part of the system relies on the raw encoded values.
The `create_resource` method uses `uri` (raw) and `params`.

```python
    async def create_resource(
        self,
        uri: str,
        params: dict[str, Any],
        context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
    ) -> Resource:
        \"\"\"Create a resource from the template with the given parameters.\"\"\"
        try:
            # Add context to params if needed
            params = inject_context(self.fn, params, context, self.context_kwarg)

            # Call function and check if result is a coroutine
            result = self.fn(**params)
```

It seems safe to decode in `matches` as long as the user doesn't need the encoded version of the parameter inside their handler. But the problem statement says that the user *wants* the decoded version.

Wait, if I decode in `matches`, then `params` will contain decoded strings.
If I decode in `create_resource` just before calling `self.fn(**params)`, it might be cleaner.

But `matches`'s purpose is to extract the parameters from the URI. Typically, extracting parameters from a URI implies decoding them.

Let's check if `params` is used anywhere else in `create_resource`. It is passed to `inject_context` and then to `self.fn(**params)`.

If I decode in `matches`:
```python
    def matches(self, uri: str) -> dict[str, Any] | None:
        \"\"\"Check if URI matches template and extract parameters.\"\"\"
        # Convert template to regex pattern
        pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")
        match = re.match(f\"^{pattern}$\", uri)
        if match:
            return {k: unquote(v) for k, v in match.groupdict().items()}
        return None
```

This looks correct.

I need to import `unquote` from `urllib.parse`.

Let's verify if there are any other places where resources are parsed.
`ResourceManager.get_resource` seems to be the central point for this in FastMCP.

Let's check if there are any existing tests for this.
TOOL CALL · run_command
{
  "command": "find tests -name \"*resource*\""
}
Result of run_command 678 characters
{
  "status": "ok",
  "stdout": "tests/server/fastmcp/resources\ntests/server/fastmcp/resources/test_file_resources.py\ntests/server/fastmcp/resources/test_function_resources.py\ntests/server/fastmcp/resources/test_resource_manager.py\ntests/server/fastmcp/resources/test_resources.py\ntests/server/fastmcp/resources/test_resource_template.py\ntests/server/auth/test_protected_resource.py\ntests/server/test_read_resource.py\ntests/issues/test_152_resource_mime_type.py\ntests/issues/test_1574_resource_uri_validation.py\ntests/issues/test_129_resource_templates.py\ntests/issues/test_141_resource_templates.py\ntests/client/test_resource_cleanup.py\n",
  "stderr": "",
  "exit_code": 0
}
Turn 9at 165 s67 tokens out10,642 prompt tokens
I'll check `tests/server/fastmcp/resources/test_resource_template.py` to see if there are any tests for URL parameters.
TOOL CALL · read_file
{
  "filepath": "tests/server/fastmcp/resources/test_resource_template.py"
}
Result of read_file 5,069 characters
{
  "status": "ok",
  "filepath": "tests/server/fastmcp/resources/test_resource_template.py",
  "content": "import json\nfrom typing import Any\n\nimport pytest\nfrom pydantic import BaseModel\n\nfrom mcp.server.fastmcp import FastMCP\nfrom mcp.server.fastmcp.resources import FunctionResource, ResourceTemplate\nfrom mcp.types import Annotations\n\n\nclass TestResourceTemplate:\n    \"\"\"Test ResourceTemplate functionality.\"\"\"\n\n    def test_template_creation(self):\n        \"\"\"Test creating a template from a function.\"\"\"\n\n        def my_func(key: str, value: int) -> dict[str, Any]:  # pragma: no cover\n            return {\"key\": key, \"value\": value}\n\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n        assert template.uri_template == \"test://{key}/{value}\"\n        assert template.name == \"test\"\n        assert template.mime_type == \"text/plain\"  # default\n        assert template.fn(key=\"test\", value=42) == my_func(key=\"test\", value=42)\n\n    def test_template_matches(self):\n        \"\"\"Test matching URIs against a template.\"\"\"\n\n        def my_func(key: str, value: int) -> dict[str, Any]:  # pragma: no cover\n            return {\"key\": key, \"value\": value}\n\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n\n        # Valid match\n        params = template.matches(\"test://foo/123\")\n        assert params == {\"key\": \"foo\", \"value\": \"123\"}\n\n        # No match\n        assert template.matches(\"test://foo\") is None\n        assert template.matches(\"other://foo/123\") is None\n\n    @pytest.mark.anyio\n    async def test_create_resource(self):\n        \"\"\"Test creating a resource from a template.\"\"\"\n\n        def my_func(key: str, value: int) -> dict[str, Any]:\n            return {\"key\": key, \"value\": value}\n\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n\n        resource = await template.create_resource(\n            \"test://foo/123\",\n            {\"key\": \"foo\", \"value\": 123},\n        )\n\n        assert isinstance(resource, FunctionResource)\n        content = await resource.read()\n        assert isinstance(content, str)\n        data = json.loads(content)\n        assert data == {\"key\": \"foo\", \"value\": 123}\n\n    @pytest.mark.anyio\n    async def test_template_error(self):\n        \"\"\"Test error handling in template resource creation.\"\"\"\n\n        def failing_func(x: str) -> str:\n            raise ValueError(\"Test error\")\n\n        template = ResourceTemplate.from_function(\n            fn=failing_func,\n            uri_template=\"fail://{x}\",\n            name=\"fail\",\n        )\n\n        with pytest.raises(ValueError, match=\"Error creating resource from template\"):\n            await template.create_resource(\"fail://test\", {\"x\": \"test\"})\n\n    @pytest.mark.anyio\n    async def test_async_text_resource(self):\n        \"\"\"Test creating a text resource from async function.\"\"\"\n\n        async def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        template = ResourceTemplate.from_function(\n            fn=greet,\n            uri_template=\"greet://{name}\",\n            name=\"greeter\",\n        )\n\n        resource = await template.create_resource(\n            \"greet://world\",\n            {\"name\": \"world\"},\n        )\n\n        assert isinstance(resource, FunctionResource)\n        content = await resource.read()\n        assert content == \"Hello, world!\"\n\n    @pytest.mark.anyio\n    async def test_async_binary_resource(self):\n        \"\"\"Test creating a binary resource from async function.\"\"\"\n\n        async def get_bytes(value: str) -> bytes:\n            return value.encode()\n\n        template = ResourceTemplate.from_function(\n            fn=get_bytes,\n            uri_template=\"bytes://{value}\",\n            name=\"bytes\",\n        )\n\n        resource = await template.create_resource(\n            \"bytes://test\",\n            {\"value\": \"test\"},\n        )\n\n        assert isinstance(resource, FunctionResource)\n        content = await resource.read()\n        assert content == b\"test\"\n\n    @pytest.mark.anyio\n    async def test_basemodel_conversion(self):\n        \"\"\"Test handling of BaseModel types.\"\"\"\n\n        class MyModel(BaseModel):\n            key: str\n            value: int\n\n        def get_data(key: str, value: int) -> MyModel:\n            return MyModel(key=key, value=value)\n\n        template = ResourceTemplate.from_function(\n            fn=get_data,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )",
  "start_line": 1,
  "end_line": 150,
  "total_lines": 307,
  "is_truncated": true
}
Turn 10at 207 s724 tokens out12,158 prompt tokens
Existing tests don't cover URL-encoded characters.

Now I will implement the fix in `src/mcp/server/fastmcp/resources/templates.py`.

I'll add `from urllib.parse import unquote` and update the `matches` method.

Wait, let's check where exactly to put the import.
Looking at `src/mcp/server/fastmcp/resources/templates.py` again:
```python
import inspect
import re
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel, Field, validate_call
```
I can add it there.

Actually, I should check if any other resource types need this.
The `ResourceManager.get_resource` method checks concrete resources first, then templates.
Concrete resources are `FunctionResource` created via `FunctionResource.from_function` if they are not templates.

```python
                # Register as regular resource
                resource = FunctionResource.from_function(
                    fn=fn,
                    uri=uri,
                    name=name,
                    title=title,
                    description=description,
                    mime_type=mime_type,
                    icons=icons,
                    annotations=annotations,
                    meta=meta,
                )
                self.add_resource(resource)
```

If it's a regular resource (not a template), it doesn't have parameters extracted from the URI, it just uses the URI as a key.

So the fix only needs to be in `ResourceTemplate.matches`.

Let's double check the implementation details.

```python
    def matches(self, uri: str) -> dict[str, Any] | None:
        \"\"\"Check if URI matches template and extract parameters.\"\"\"
        # Convert template to regex pattern
        pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")
        match = re.match(f\"^{pattern}$\", uri)
        if match:
            return match.groupdict()
        return None
```

I will change it to:

```python
    def matches(self, uri: str) -> dict[str, Any] | None:
        \"\"\"Check if URI matches template and extract parameters.\"\"\"
        # Convert template to regex pattern
        pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")
        match = re.match(f\"^{pattern}$\", uri)
        if match:
            from urllib.parse import unquote
            return {k: unquote(v) for k, v in match.groupdict().items()}
        return None
```
Actually, putting the import at the top is better.
Turn 11at 207 s
<|tool_call>call edit_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>,new_string:<|"|>import inspect
import re
from urllib.parse import unquote
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
<|"|>,old_string:<|"|>import inspect
import re
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
<|"|>}<tool_call|>
System instructions 286 characters
Your previous response reached the token limit before the tool call finished closing (<|tool_call|> was cut off). Do NOT repeat your prior reasoning in thought—emit your next tool call immediately, and if calling edit_file or write_file, split the change into smaller incremental edits.
Turn 12at 230 s289 tokens out10,634 prompt tokens
TOOL CALL · edit_file
{
  "filepath": "src/mcp/server/fastmcp/resources/templates.py",
  "new_string": "    def matches(self, uri: str) -> dict[str, Any] | None:\n        \\\"\\\"\\\"Check if URI matches template and extract parameters.\\\"\\\"\\\"\n        # Convert template to regex pattern\n        pattern = self.uri_template.replace(\\\"{\\\", \\\"(?P<\\\").replace(\\\"}\\\", \\\">[^/]+)\\\")\n        match = re.match(f\\\"^{pattern}$\\\", uri)\n        if match:\n            params = match.groupdict()\n            return {k: unquote(v) for k, v in params.items()}\n        return None\n",
  "old_string": "    def matches(self, uri: str) -> dict[str, Any] | None:\n        \\\"\\\"\\\"Check if URI matches template and extract parameters.\\\"\\\"\\\"\n        # Convert template to regex pattern\n        pattern = self.uri_template.replace(\\\"{\\\", \\\"(?P<\\\").replace(\\\"}\\\", \\\">[^/]+)\\\")\n        match = re.match(f\\\"^{pattern}$\\\", uri)\n        if match:\n            return match.groupdict()\n        return None\n"
}
Result of edit_file 643 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/fastmcp/resources/templates.py",
  "occurrences": 1,
  "strategy": "unescaped",
  "diff": "--- a/src/mcp/server/fastmcp/resources/templates.py\n+++ b/src/mcp/server/fastmcp/resources/templates.py\n@@ -88,7 +88,8 @@\n         pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")\n         match = re.match(f\"^{pattern}$\", uri)\n         if match:\n-            return match.groupdict()\n+            params = match.groupdict()\n+            return {k: unquote(v) for k, v in params.items()}\n         return None\n \n     async def create_resource(\n",
  "is_truncated": false
}
Turn 13at 240 s340 tokens out11,153 prompt tokens
TOOL CALL · run_command
{
  "command": "python3 - <<'PY'\nimport asyncio\nfrom urllib.parse import quote\nfrom mcp.server.fastmcp.resources.templates import ResourceTemplate\n\nasync def test_resource_template_decoding():\n    async def handle_resource(product_identifier: str):\n        return f\"Product: {product_identifier}\"\n\n    template = ResourceTemplate.from_function(\n        fn=handle_resource,\n        uri_template=\"products://search/{product_identifier}\"\n    )\n\n    # Test case: encoded space and accented character\n    product_id = \"stick correcteur teinté\"\n    encoded_id = quote(product_id)\n    uri = f\"products://search/{encoded_id}\"\n\n    params = template.matches(uri)\n    print(f\"Params extracted: {params}\")\n    \n    if params:\n        print(f\"Extracted id: {params['product_identifier']}\")\n        assert params['product_identifier'] == product_id, f\"Expected {product_id}, got {params['product_identifier']}\"\n    else:\n        print(\"URI did not match template\")\n        assert False, \"URI should have matched template\"\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(test_resource_template_decoding())\n        print(\"Test passed!\")\n    except Exception as e:\n        print(f\"Test failed: {e}\")\n        exit(1)\nPY\n"
}
Result of run_command 215 characters
{
  "status": "error",
  "error_type": "CommandError",
  "error_message": "Test failed: name 'unquote' is not defined\n",
  "details": {
    "stdout": "Test failed: name 'unquote' is not defined\n",
    "stderr": "",
    "exit_code": 1
  }
}
Turn 14at 257 s198 tokens out11,572 prompt tokens
Wait, the previous `edit_file` failed to add the import because I missed the import block in my previous attempt (the previous response was cut off and I didn't retry the import edit).

Actually, I called `edit_file` twice.
1st: Added `unquote` import. (Cut off in response but was it actually applied?)
Actually, the previous response says "Your previous response reached the token limit...".
Wait, I see:
`edit_file` for import was the FIRST call in the previous turn, but it might have been omitted or failed.
Let me check if `unquote` is imported in `src/mcp/server/fastmcp/resources/templates.py`.
TOOL CALL · read_file
{
  "end_line": 20,
  "filepath": "src/mcp/server/fastmcp/resources/templates.py",
  "start_line": 1
}
Result of read_file 885 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/fastmcp/resources/templates.py",
  "content": "\"\"\"Resource template functionality.\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nimport re\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\n\nfrom pydantic import BaseModel, Field, validate_call\n\nfrom mcp.server.fastmcp.resources.types import FunctionResource, Resource\nfrom mcp.server.fastmcp.utilities.context_injection import find_context_parameter, inject_context\nfrom mcp.server.fastmcp.utilities.func_metadata import func_metadata\nfrom mcp.types import Annotations, Icon\n\nif TYPE_CHECKING:\n    from mcp.server.fastmcp.server import Context\n    from mcp.server.session import ServerSessionT\n    from mcp.shared.context import LifespanContextT, RequestT",
  "start_line": 1,
  "end_line": 20,
  "total_lines": 123,
  "is_truncated": false
}
Turn 15at 261 s118 tokens out12,054 prompt tokens
The import is missing. I need to add `from urllib.parse import unquote`.
TOOL CALL · edit_file
{
  "filepath": "src/mcp/server/fastmcp/resources/templates.py",
  "new_string": "import inspect\nimport re\nfrom urllib.parse import unquote\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\n",
  "old_string": "import inspect\nimport re\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\n"
}
Result of edit_file 423 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/fastmcp/resources/templates.py",
  "occurrences": 1,
  "strategy": "exact",
  "diff": "--- a/src/mcp/server/fastmcp/resources/templates.py\n+++ b/src/mcp/server/fastmcp/resources/templates.py\n@@ -4,6 +4,7 @@\n \n import inspect\n import re\n+from urllib.parse import unquote\n from collections.abc import Callable\n from typing import TYPE_CHECKING, Any\n \n",
  "is_truncated": false
}
Turn 16at 275 s348 tokens out12,334 prompt tokens
Now run the reproduction test again.
TOOL CALL · run_command
{
  "command": "python3 - <<'PY'\nimport asyncio\nfrom urllib.parse import quote\nfrom mcp.server.fastmcp.resources.templates import ResourceTemplate\n\nasync def test_resource_template_decoding():\n    async def handle_resource(product_identifier: str):\n        return f\"Product: {product_identifier}\"\n\n    template = ResourceTemplate.from_function(\n        fn=handle_resource,\n        uri_template=\"products://search/{product_identifier}\"\n    )\n\n    # Test case: encoded space and accented character\n    product_id = \"stick correcteur teinté\"\n    encoded_id = quote(product_id)\n    uri = f\"products://search/{encoded_id}\"\n\n    params = template.matches(uri)\n    print(f\"Params extracted: {params}\")\n    \n    if params:\n        print(f\"Extracted id: {params['product_identifier']}\")\n        assert params['product_identifier'] == product_id, f\"Expected {product_id}, got {params['product_identifier']}\"\n    else:\n        print(\"URI did not match template\")\n        assert False, \"URI should have matched template\"\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(test_resource_template_decoding())\n        print(\"Test passed!\")\n    except Exception as e:\n        print(f\"Test failed: {e}\")\n        exit(1)\nPY\n"
}
Result of run_command 192 characters
{
  "status": "ok",
  "stdout": "Params extracted: {'product_identifier': 'stick correcteur teinté'}\nExtracted id: stick correcteur teinté\nTest passed!\n",
  "stderr": "",
  "exit_code": 0
}
Turn 17at 282 s83 tokens out12,752 prompt tokens
The reproduction test now passes. Now I should run the existing tests in `tests/server/fastmcp/resources/test_resource_template.py` to make sure no regressions.
TOOL CALL · run_command
{
  "command": "pytest tests/server/fastmcp/resources/test_resource_template.py -q --tb=short --no-header"
}
Result of run_command 5,104 characters
{
  "status": "ok",
  "stdout": "bringing up nodes...\n48 workers [14 items]\n\nscheduling tests via LoadScheduling\n\ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_template_matches \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_template_creation \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateAnnotations::test_template_without_annotations \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_template_error \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_async_binary_resource \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateMetadata::test_template_created_resources_inherit_metadata \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateMetadata::test_template_from_function_with_metadata \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateAnnotations::test_template_with_annotations \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_async_text_resource \n[gw15]\u001b[36m [  7%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateMetadata::test_template_created_resources_inherit_metadata \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_custom_type_conversion \n[gw8]\u001b[36m [ 14%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_custom_type_conversion \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateAnnotations::test_template_created_resources_inherit_annotations \n[gw14]\u001b[36m [ 21%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateMetadata::test_template_from_function_with_metadata \n[gw4]\u001b[36m [ 28%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateAnnotations::test_template_without_annotations \n[gw0]\u001b[36m [ 35%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_template_creation \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_basemodel_conversion \n[gw3]\u001b[36m [ 42%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_template_error \n[gw6]\u001b[36m [ 50%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_async_text_resource \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_create_resource \n[gw2]\u001b[36m [ 57%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_create_resource \n[gw9]\u001b[36m [ 64%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateAnnotations::test_template_with_annotations \n[gw5]\u001b[36m [ 71%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_async_binary_resource \n[gw1]\u001b[36m [ 78%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_template_matches \ntests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateAnnotations::test_template_annotations_in_fastmcp \n[gw7]\u001b[36m [ 85%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplate::test_basemodel_conversion \n[gw10]\u001b[36m [ 92%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateAnnotations::test_template_created_resources_inherit_annotations \n[gw11]\u001b[36m [100%] \u001b[0m\u001b[32mPASSED\u001b[0m tests/server/fastmcp/resources/test_resource_template.py::TestResourceTemplateAnnotations::test_template_annotations_in_fastmcp \n\n\u001b[92m═══════════════════════════════ \u001b[0m\u001b[34minline-snapshot\u001b[0m\u001b[92m ════════════════════════════════\u001b[0m\nINFO: inline-snapshot was disabled because you used xdist. This means that tests\nwith snapshots will continue to run, but snapshot(x) will only return x and \ninline-snapshot will not be able to fix snapshots or generate reports.\n\n\n\n\u001b[32m============================= \u001b[32m\u001b[1m14 passed\u001b[0m\u001b[32m in 24.51s\u001b[0m\u001b[32m ==============================\u001b[0m\n",
  "stderr": "",
  "exit_code": 0
}
Turn 18at 313 s42 tokens out14,704 prompt tokens
All tests passed. Now I will check for any untracked files and verify the diff.
TOOL CALL · run_command
{
  "command": "git status --short && git diff"
}
Result of run_command 973 characters
{
  "status": "ok",
  "stdout": " M src/mcp/server/fastmcp/resources/templates.py\ndiff --git a/src/mcp/server/fastmcp/resources/templates.py b/src/mcp/server/fastmcp/resources/templates.py\nindex 89a8ceb..61c5b07 100644\n--- a/src/mcp/server/fastmcp/resources/templates.py\n+++ b/src/mcp/server/fastmcp/resources/templates.py\n@@ -4,6 +4,7 @@ from __future__ import annotations\n \n import inspect\n import re\n+from urllib.parse import unquote\n from collections.abc import Callable\n from typing import TYPE_CHECKING, Any\n \n@@ -88,7 +89,8 @@ class ResourceTemplate(BaseModel):\n         pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")\n         match = re.match(f\"^{pattern}$\", uri)\n         if match:\n-            return match.groupdict()\n+            params = match.groupdict()\n+            return {k: unquote(v) for k, v in params.items()}\n         return None\n \n     async def create_resource(\n",
  "stderr": "",
  "exit_code": 0
}
Turn 19at 368 s126 tokens out5,076 prompt tokens
The diff looks clean and correct. I have implemented the fix and verified it with both a reproduction script and the existing test suite.

Final check:
- No untracked files.
- Only `src/mcp/server/fastmcp/resources/templates.py` was modified.
- The fix decodes URL parameters correctly.
- Parallel paths: This is the only place where `ResourceTemplate.matches` is implemented and used for extracting parameters from URIs in the `fastmcp` resource system.

I am ready to submit.
TOOL CALL · submit_patch
Result of submit_patch 55 characters
{
  "status": "ok",
  "patch_size": 831,
  "files_changed": 1
}
Turn 20at 376 s80 tokens out5,242 prompt tokens
The `ResourceTemplate.matches` method in `src/mcp/server/fastmcp/resources/templates.py` was returning URL parameters in their raw, encoded form. I updated the method to apply `urllib.parse.unquote` to each extracted parameter, ensuring that encoded characters (like `%20`) are correctly decoded before being passed to resource handlers.

Model calls (not matched to turns)

Generated tokens 180 tokens · stopped: STOP
The user is reporting a bug where URL parameters in dynamic resources are not automatically URL-decoded in the MCP Python SDK.
Specifically, when a resource handler is defined with a template like `products://search/{product_identifier}`, the `product_identifier` passed to the handler is still URL-encoded (e.g., `%20` instead of space).

I need to find where the resource handler is invoked and where the template parameters are extracted.

First, I'll search for where resources are handled and how the parameters are parsed. I'll look for `mcp.resource` or similar decorators and how they are implemented.
<|channel>thought The user is reporting a bug where URL parameters in dynamic resources are not automatically URL-decoded in the MCP Python SDK. Specifically, when a resource handler is defined with a template like `products://search/{product_identifier}`, the `product_identifier` passed to the handler is still URL-encoded (e.g., `%20` instead of space). I need to find where the resource handler is invoked and where the template parameters are extracted. First, I'll search for where resources are handled and how the parameters are parsed. I'll look for `mcp.resource` or similar decorators and how they are implemented.<channel|><|tool_call>call:run_command{command:<|"|>grep -rn --include="*.py" "resource" src/mcp | grep -v "tests" | head -n 20<|"|>}<tool_call|><|tool_response>
Generated tokens 59 tokens · stopped: STOP
The previous `grep` didn't show the decorator implementation. I'll search for `resource(` to find the decorator definition.
The previous `grep` didn't show the decorator implementation. I'll search for `resource(` to find the decorator definition.<channel|><|tool_call>call:run_command{command:<|"|>grep -rn --include="*.py" "def resource" src/mcp<|"|>}<tool_call|><|tool_response>
Generated tokens 84 tokens · stopped: STOP
It seems `src/mcp/server/fastmcp/server.py` contains the `resource` decorator. Let's examine that file around line 519.
It seems `src/mcp/server/fastmcp/server.py` contains the `resource` decorator. Let's examine that file around line 519. <channel|><|tool_call>call:read_file{filepath:<|"|>src/mcp/server/fastmcp/server.py<|"|>,start_line:500,end_line:650}<tool_call|><|tool_response>
Generated tokens 83 tokens · stopped: STOP
The `resource` decorator calls `self._resource_manager.add_template`. I need to find where the `ResourceManager` is defined and how it handles template resources when they are read.

Let's find the definition of `ResourceManager`.
The `resource` decorator calls `self._resource_manager.add_template`. I need to find where the `ResourceManager` is defined and how it handles template resources when they are read. Let's find the definition of `ResourceManager`. <channel|><|tool_call>call:run_command{command:<|"|>grep -rn --include="*.py" "class ResourceManager" src/mcp<|"|>}<tool_call|><|tool_response>
Generated tokens 58 tokens · stopped: STOP
Let's examine `src/mcp/server/fastmcp/resources/resource_manager.py`.
Let's examine `src/mcp/server/fastmcp/resources/resource_manager.py`. <channel|><|tool_call>call:read_file{filepath:<|"|>src/mcp/server/fastmcp/resources/resource_manager.py<|"|>}<tool_call|><|tool_response>
Generated tokens 179 tokens · stopped: STOP
In `ResourceManager.get_resource`, the code does:

```python
        # Then check templates
        for template in self._templates.values():
            if params := template.matches(uri_str):
                try:
                    return await template.create_resource(uri_str, params, context=context)
                except Exception as e:  # pragma: no cover
                    raise ValueError(f\"Error creating resource from template: {e}\")
```

I need to see how `ResourceTemplate.matches` and `ResourceTemplate.create_resource` are implemented. They are in `src/mcp/server/fastmcp/resources/templates.py`.

In `ResourceManager.get_resource`, the code does: ```python # Then check templates for template in self._templates.values(): if params := template.matches(uri_str): try: return await template.create_resource(uri_str, params, context=context) except Exception as e: # pragma: no cover raise ValueError(f\"Error creating resource from template: {e}\") ``` I need to see how `ResourceTemplate.matches` and `ResourceTemplate.create_resource` are implemented. They are in `src/mcp/server/fastmcp/resources/templates.py`. <channel|><|tool_call>call:read_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>}<tool_call|><|tool_response>
Generated tokens 771 tokens · stopped: STOP
The issue is in `ResourceTemplate.matches`.

```python
    def matches(self, uri: str) -> dict[str, Any] | None:
        \"\"\"Check if URI matches template and extract parameters.\"\"\"
        # Convert template to regex pattern
        pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")
        match = re.match(f\"^{pattern}$\", uri)
        if match:
            return match.groupdict()
        return None
```

The `match.groupdict()` returns the raw extracted strings from the URI, which are still URL-encoded. These are then passed to `self.fn(**params)` in `create_resource`.

I should URL-decode the parameters extracted in `matches`.

Wait, if I decode them in `matches`, then `params` will contain decoded values.
`create_resource` does:
```python
    async def create_resource(
        self,
        uri: str,
        params: dict[str, Any],
        context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
    ) -> Resource:
        \"\"\"Create a resource from the template with the given parameters.\"\"\"
        try:
            # Add context to params if needed
            params = inject_context(self.fn, params, context, self.context_kwarg)

            # Call function and check if result is a coroutine
            result = self.fn(**params)
```

So if I decode in `matches`, the decoded values will be passed to the function.

Let's create a reproduction script to confirm this.
The issue is in `ResourceTemplate.matches`. ```python def matches(self, uri: str) -> dict[str, Any] | None: \"\"\"Check if URI matches template and extract parameters.\"\"\" # Convert template to regex pattern pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\") match = re.match(f\"^{pattern}$\", uri) if match: return match.groupdict() return None ``` The `match.groupdict()` returns the raw extracted strings from the URI, which are still URL-encoded. These are then passed to `self.fn(**params)` in `create_resource`. I should URL-decode the parameters extracted in `matches`. Wait, if I decode them in `matches`, then `params` will contain decoded values. `create_resource` does: ```python async def create_resource( self, uri: str, params: dict[str, Any], context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, ) -> Resource: \"\"\"Create a resource from the template with the given parameters.\"\"\" try: # Add context to params if needed params = inject_context(self.fn, params, context, self.context_kwarg) # Call function and check if result is a coroutine result = self.fn(**params) ``` So if I decode in `matches`, the decoded values will be passed to the function. Let's create a reproduction script to confirm this. <channel|><|tool_call>call:run_command{command:<|"|>python3 - <<'PY' import asyncio from unittest.mock import MagicMock from urllib.parse import quote from mcp.server.fastmcp.resources.templates import ResourceTemplate async def test_resource_template_decoding(): async def handle_resource(product_identifier: str): return f"Product: {product_identifier}" template = ResourceTemplate.from_function( fn=handle_resource, uri_template="products://search/{product_identifier}" ) # Test case: encoded space and accented character product_id = "stick correcteur teinté" encoded_id = quote(product_id) uri = f"products://search/{encoded_id}" params = template.matches(uri) print(f"Params extracted: {params}") # We need to simulate the call from create_resource # Normally ResourceTemplate.create_resource is async and calls the fn # For simplicity, let's just see what matches returns if params: print(f"Extracted id: {params['product_identifier']}") assert params['product_identifier'] == product_id, f"Expected {product_id}, got {params['product_identifier']}" else: print("URI did not match template") assert False, "URI should have matched template" if __name__ == "__main__": try: asyncio.run(test_resource_template_decoding()) print("Test passed!") except Exception as e: print(f"Test failed: {e}") exit(1) PY <|"|>}<tool_call|><|tool_response>
Generated tokens 832 tokens · stopped: STOP
The reproduction script confirms the bug. `template.matches(uri)` returns the URL-encoded string.

I should use `urllib.parse.unquote` to decode the extracted parameters.

I'll modify `src/mcp/server/fastmcp/resources/templates.py` to decode the values in `matches`.

Wait, should I decode in `matches` or in `create_resource`?
If I decode in `matches`, `matches` return value changes from encoded to decoded.
`matches` is used in `ResourceManager.get_resource`:

```python
        # Then check templates
        for template in self._templates.values():
            if params := template.matches(uri_str):
                try:
                    return await template.create_resource(uri_str, params, context=context)
```

If I decode in `matches`, `params` are already decoded when passed to `create_resource`.

Is there any other place `matches` is used?
Let's check `src/mcp/server/fastmcp/resources/templates.py` for other usages of `matches` or other methods.
`matches` is only used in `ResourceManager.get_resource`.

However, let's see if any other part of the system relies on the raw encoded values.
The `create_resource` method uses `uri` (raw) and `params`.

```python
    async def create_resource(
        self,
        uri: str,
        params: dict[str, Any],
        context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
    ) -> Resource:
        \"\"\"Create a resource from the template with the given parameters.\"\"\"
        try:
            # Add context to params if needed
            params = inject_context(self.fn, params, context, self.context_kwarg)

            # Call function and check if result is a coroutine
            result = self.fn(**params)
```

It seems safe to decode in `matches` as long as the user doesn't need the encoded version of the parameter inside their handler. But the problem statement says that the user *wants* the decoded version.

Wait, if I decode in `matches`, then `params` will contain decoded strings.
If I decode in `create_resource` just before calling `self.fn(**params)`, it might be cleaner.

But `matches`'s purpose is to extract the parameters from the URI. Typically, extracting parameters from a URI implies decoding them.

Let's check if `params` is used anywhere else in `create_resource`. It is passed to `inject_context` and then to `self.fn(**params)`.

If I decode in `matches`:
```python
    def matches(self, uri: str) -> dict[str, Any] | None:
        \"\"\"Check if URI matches template and extract parameters.\"\"\"
        # Convert template to regex pattern
        pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")
        match = re.match(f\"^{pattern}$\", uri)
        if match:
            return {k: unquote(v) for k, v in match.groupdict().items()}
        return None
```

This looks correct.

I need to import `unquote` from `urllib.parse`.

Let's verify if there are any other places where resources are parsed.
`ResourceManager.get_resource` seems to be the central point for this in FastMCP.

Let's check if there are any existing tests for this.
The reproduction script confirms the bug. `template.matches(uri)` returns the URL-encoded string. I should use `urllib.parse.unquote` to decode the extracted parameters. I'll modify `src/mcp/server/fastmcp/resources/templates.py` to decode the values in `matches`. Wait, should I decode in `matches` or in `create_resource`? If I decode in `matches`, `matches` return value changes from encoded to decoded. `matches` is used in `ResourceManager.get_resource`: ```python # Then check templates for template in self._templates.values(): if params := template.matches(uri_str): try: return await template.create_resource(uri_str, params, context=context) ``` If I decode in `matches`, `params` are already decoded when passed to `create_resource`. Is there any other place `matches` is used? Let's check `src/mcp/server/fastmcp/resources/templates.py` for other usages of `matches` or other methods. `matches` is only used in `ResourceManager.get_resource`. However, let's see if any other part of the system relies on the raw encoded values. The `create_resource` method uses `uri` (raw) and `params`. ```python async def create_resource( self, uri: str, params: dict[str, Any], context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, ) -> Resource: \"\"\"Create a resource from the template with the given parameters.\"\"\" try: # Add context to params if needed params = inject_context(self.fn, params, context, self.context_kwarg) # Call function and check if result is a coroutine result = self.fn(**params) ``` It seems safe to decode in `matches` as long as the user doesn't need the encoded version of the parameter inside their handler. But the problem statement says that the user *wants* the decoded version. Wait, if I decode in `matches`, then `params` will contain decoded strings. If I decode in `create_resource` just before calling `self.fn(**params)`, it might be cleaner. But `matches`'s purpose is to extract the parameters from the URI. Typically, extracting parameters from a URI implies decoding them. Let's check if `params` is used anywhere else in `create_resource`. It is passed to `inject_context` and then to `self.fn(**params)`. If I decode in `matches`: ```python def matches(self, uri: str) -> dict[str, Any] | None: \"\"\"Check if URI matches template and extract parameters.\"\"\" # Convert template to regex pattern pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\") match = re.match(f\"^{pattern}$\", uri) if match: return {k: unquote(v) for k, v in match.groupdict().items()} return None ``` This looks correct. I need to import `unquote` from `urllib.parse`. Let's verify if there are any other places where resources are parsed. `ResourceManager.get_resource` seems to be the central point for this in FastMCP. Let's check if there are any existing tests for this. <channel|><|tool_call>call:run_command{command:<|"|>find tests -name "*resource*"<|"|>}<tool_call|><|tool_response>
Generated tokens 67 tokens · stopped: STOP
I'll check `tests/server/fastmcp/resources/test_resource_template.py` to see if there are any tests for URL parameters.

I'll check `tests/server/fastmcp/resources/test_resource_template.py` to see if there are any tests for URL parameters. <channel|><|tool_call>call:read_file{filepath:<|"|>tests/server/fastmcp/resources/test_resource_template.py<|"|>}<tool_call|><|tool_response>
Generated tokens 724 tokens · stopped: STOP
Existing tests don't cover URL-encoded characters.

Now I will implement the fix in `src/mcp/server/fastmcp/resources/templates.py`.

I'll add `from urllib.parse import unquote` and update the `matches` method.

Wait, let's check where exactly to put the import.
Looking at `src/mcp/server/fastmcp/resources/templates.py` again:
```python
import inspect
import re
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel, Field, validate_call
```
I can add it there.

Actually, I should check if any other resource types need this.
The `ResourceManager.get_resource` method checks concrete resources first, then templates.
Concrete resources are `FunctionResource` created via `FunctionResource.from_function` if they are not templates.

```python
                # Register as regular resource
                resource = FunctionResource.from_function(
                    fn=fn,
                    uri=uri,
                    name=name,
                    title=title,
                    description=description,
                    mime_type=mime_type,
                    icons=icons,
                    annotations=annotations,
                    meta=meta,
                )
                self.add_resource(resource)
```

If it's a regular resource (not a template), it doesn't have parameters extracted from the URI, it just uses the URI as a key.

So the fix only needs to be in `ResourceTemplate.matches`.

Let's double check the implementation details.

```python
    def matches(self, uri: str) -> dict[str, Any] | None:
        \"\"\"Check if URI matches template and extract parameters.\"\"\"
        # Convert template to regex pattern
        pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")
        match = re.match(f\"^{pattern}$\", uri)
        if match:
            return match.groupdict()
        return None
```

I will change it to:

```python
    def matches(self, uri: str) -> dict[str, Any] | None:
        \"\"\"Check if URI matches template and extract parameters.\"\"\"
        # Convert template to regex pattern
        pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")
        match = re.match(f\"^{pattern}$\", uri)
        if match:
            from urllib.parse import unquote
            return {k: unquote(v) for k, v in match.groupdict().items()}
        return None
```
Actually, putting the import at the top is better.

Existing tests don't cover URL-encoded characters. Now I will implement the fix in `src/mcp/server/fastmcp/resources/templates.py`. I'll add `from urllib.parse import unquote` and update the `matches` method. Wait, let's check where exactly to put the import. Looking at `src/mcp/server/fastmcp/resources/templates.py` again: ```python import inspect import re from collections.abc import Callable from typing import TYPE_CHECKING, Any from pydantic import BaseModel, Field, validate_call ``` I can add it there. Actually, I should check if any other resource types need this. The `ResourceManager.get_resource` method checks concrete resources first, then templates. Concrete resources are `FunctionResource` created via `FunctionResource.from_function` if they are not templates. ```python # Register as regular resource resource = FunctionResource.from_function( fn=fn, uri=uri, name=name, title=title, description=description, mime_type=mime_type, icons=icons, annotations=annotations, meta=meta, ) self.add_resource(resource) ``` If it's a regular resource (not a template), it doesn't have parameters extracted from the URI, it just uses the URI as a key. So the fix only needs to be in `ResourceTemplate.matches`. Let's double check the implementation details. ```python def matches(self, uri: str) -> dict[str, Any] | None: \"\"\"Check if URI matches template and extract parameters.\"\"\" # Convert template to regex pattern pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\") match = re.match(f\"^{pattern}$\", uri) if match: return match.groupdict() return None ``` I will change it to: ```python def matches(self, uri: str) -> dict[str, Any] | None: \"\"\"Check if URI matches template and extract parameters.\"\"\" # Convert template to regex pattern pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\") match = re.match(f\"^{pattern}$\", uri) if match: from urllib.parse import unquote return {k: unquote(v) for k, v in match.groupdict().items()} return None ``` Actually, putting the import at the top is better. <channel|><|tool_call>call edit_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>,new_string:<|"|>import inspect import re from urllib.parse import unquote from collections.abc import Callable from typing import TYPE_CHECKING, Any <|"|>,old_string:<|"|>import inspect import re from collections.abc import Callable from typing import TYPE_CHECKING, Any <|"|>}<tool_call|><|tool_response>
Generated tokens 289 tokens · stopped: STOP
<|channel>thought <channel|><|tool_call>call:edit_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>,new_string:<|"|> def matches(self, uri: str) -> dict[str, Any] | None: \"\"\"Check if URI matches template and extract parameters.\"\"\" # Convert template to regex pattern pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\") match = re.match(f\"^{pattern}$\", uri) if match: params = match.groupdict() return {k: unquote(v) for k, v in params.items()} return None <|"|>,old_string:<|"|> def matches(self, uri: str) -> dict[str, Any] | None: \"\"\"Check if URI matches template and extract parameters.\"\"\" # Convert template to regex pattern pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\") match = re.match(f\"^{pattern}$\", uri) if match: return match.groupdict() return None <|"|>}<tool_call|><|tool_response>
Generated tokens 340 tokens · stopped: STOP
<channel|><|tool_call>call:run_command{command:<|"|>python3 - <<'PY' import asyncio from urllib.parse import quote from mcp.server.fastmcp.resources.templates import ResourceTemplate async def test_resource_template_decoding(): async def handle_resource(product_identifier: str): return f"Product: {product_identifier}" template = ResourceTemplate.from_function( fn=handle_resource, uri_template="products://search/{product_identifier}" ) # Test case: encoded space and accented character product_id = "stick correcteur teinté" encoded_id = quote(product_id) uri = f"products://search/{encoded_id}" params = template.matches(uri) print(f"Params extracted: {params}") if params: print(f"Extracted id: {params['product_identifier']}") assert params['product_identifier'] == product_id, f"Expected {product_id}, got {params['product_identifier']}" else: print("URI did not match template") assert False, "URI should have matched template" if __name__ == "__main__": try: asyncio.run(test_resource_template_decoding()) print("Test passed!") except Exception as e: print(f"Test failed: {e}") exit(1) PY <|"|>}<tool_call|><|tool_response>
Generated tokens 198 tokens · stopped: STOP
Wait, the previous `edit_file` failed to add the import because I missed the import block in my previous attempt (the previous response was cut off and I didn't retry the import edit).

Actually, I called `edit_file` twice.
1st: Added `unquote` import. (Cut off in response but was it actually applied?)
Actually, the previous response says "Your previous response reached the token limit...".
Wait, I see:
`edit_file` for import was the FIRST call in the previous turn, but it might have been omitted or failed.
Let me check if `unquote` is imported in `src/mcp/server/fastmcp/resources/templates.py`.
Wait, the previous `edit_file` failed to add the import because I missed the import block in my previous attempt (the previous response was cut off and I didn't retry the import edit). Actually, I called `edit_file` twice. 1st: Added `unquote` import. (Cut off in response but was it actually applied?) Actually, the previous response says "Your previous response reached the token limit...". Wait, I see: `edit_file` for import was the FIRST call in the previous turn, but it might have been omitted or failed. Let me check if `unquote` is imported in `src/mcp/server/fastmcp/resources/templates.py`.<channel|><|tool_call>call:read_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>,start_line:1,end_line:20}<tool_call|><|tool_response>
Generated tokens 118 tokens · stopped: STOP
The import is missing. I need to add `from urllib.parse import unquote`.
The import is missing. I need to add `from urllib.parse import unquote`. <channel|><|tool_call>call:edit_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>,new_string:<|"|>import inspect import re from urllib.parse import unquote from collections.abc import Callable from typing import TYPE_CHECKING, Any <|"|>,old_string:<|"|>import inspect import re from collections.abc import Callable from typing import TYPE_CHECKING, Any <|"|>}<tool_call|><|tool_response>
Generated tokens 348 tokens · stopped: STOP
Now run the reproduction test again.
Now run the reproduction test again. <channel|><|tool_call>call:run_command{command:<|"|>python3 - <<'PY' import asyncio from urllib.parse import quote from mcp.server.fastmcp.resources.templates import ResourceTemplate async def test_resource_template_decoding(): async def handle_resource(product_identifier: str): return f"Product: {product_identifier}" template = ResourceTemplate.from_function( fn=handle_resource, uri_template="products://search/{product_identifier}" ) # Test case: encoded space and accented character product_id = "stick correcteur teinté" encoded_id = quote(product_id) uri = f"products://search/{encoded_id}" params = template.matches(uri) print(f"Params extracted: {params}") if params: print(f"Extracted id: {params['product_identifier']}") assert params['product_identifier'] == product_id, f"Expected {product_id}, got {params['product_identifier']}" else: print("URI did not match template") assert False, "URI should have matched template" if __name__ == "__main__": try: asyncio.run(test_resource_template_decoding()) print("Test passed!") except Exception as e: print(f"Test failed: {e}") exit(1) PY <|"|>}<tool_call|><|tool_response>
Generated tokens 83 tokens · stopped: STOP
The reproduction test now passes. Now I should run the existing tests in `tests/server/fastmcp/resources/test_resource_template.py` to make sure no regressions.

The reproduction test now passes. Now I should run the existing tests in `tests/server/fastmcp/resources/test_resource_template.py` to make sure no regressions. <channel|><|tool_call>call:run_command{command:<|"|>pytest tests/server/fastmcp/resources/test_resource_template.py -q --tb=short --no-header<|"|>}<tool_call|><|tool_response>
Generated tokens 42 tokens · stopped: STOP
All tests passed. Now I will check for any untracked files and verify the diff.
All tests passed. Now I will check for any untracked files and verify the diff. <channel|><|tool_call>call:run_command{command:<|"|>git status --short && git diff<|"|>}<tool_call|><|tool_response>
Generated tokens 126 tokens · stopped: STOP
The diff looks clean and correct. I have implemented the fix and verified it with both a reproduction script and the existing test suite.

Final check:
- No untracked files.
- Only `src/mcp/server/fastmcp/resources/templates.py` was modified.
- The fix decodes URL parameters correctly.
- Parallel paths: This is the only place where `ResourceTemplate.matches` is implemented and used for extracting parameters from URIs in the `fastmcp` resource system.

I am ready to submit.
The diff looks clean and correct. I have implemented the fix and verified it with both a reproduction script and the existing test suite. Final check: - No untracked files. - Only `src/mcp/server/fastmcp/resources/templates.py` was modified. - The fix decodes URL parameters correctly. - Parallel paths: This is the only place where `ResourceTemplate.matches` is implemented and used for extracting parameters from URIs in the `fastmcp` resource system. I am ready to submit.<channel|><|tool_call>call:submit_patch{}<tool_call|><|tool_response>

Patch, tests and logs

Final code change Included
diff --git a/src/mcp/server/fastmcp/resources/templates.py b/src/mcp/server/fastmcp/resources/templates.py
index 89a8ceb..61c5b07 100644
--- a/src/mcp/server/fastmcp/resources/templates.py
+++ b/src/mcp/server/fastmcp/resources/templates.py
@@ -4,6 +4,7 @@ from __future__ import annotations
 
 import inspect
 import re
+from urllib.parse import unquote
 from collections.abc import Callable
 from typing import TYPE_CHECKING, Any
 
@@ -88,7 +89,8 @@ class ResourceTemplate(BaseModel):
         pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
         match = re.match(f"^{pattern}$", uri)
         if match:
-            return match.groupdict()
+            params = match.groupdict()
+            return {k: unquote(v) for k, v in params.items()}
         return None
 
     async def create_resource(
Test output Included
============================= test session starts ==============================
created: 48/48 workers
48 workers [12 items]

scheduling tests via LoadScheduling

tests/issues/test_973_url_decoding.py::test_template_matches_decodes_space 
tests/issues/test_973_url_decoding.py::test_template_matches_decodes_accented_characters 
tests/server/test_stateless_mode.py::test_create_message_fails_in_stateless_mode 
tests/issues/test_973_url_decoding.py::test_template_matches_decodes_complex_phrase 
tests/server/test_stateless_mode.py::test_stateful_mode_does_not_raise_stateless_error 
tests/issues/test_973_url_decoding.py::test_template_matches_preserves_plus_sign 
tests/server/test_stateless_mode.py::test_stateless_error_message_is_actionable 
[gw9] [  8%] PASSED tests/server/test_stateless_mode.py::test_stateless_error_message_is_actionable 
tests/server/test_stateless_mode.py::test_elicit_deprecated_fails_in_stateless_mode 
[gw8] [ 16%] PASSED tests/server/test_stateless_mode.py::test_elicit_deprecated_fails_in_stateless_mode 
[gw11] [ 25%] PASSED tests/server/test_stateless_mode.py::test_stateful_mode_does_not_raise_stateless_error 
tests/server/test_stateless_mode.py::test_exception_has_method_attribute 
[gw10] [ 33%] PASSED tests/server/test_stateless_mode.py::test_exception_has_method_attribute 
[gw2] [ 41%] PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_complex_phrase 
tests/server/test_stateless_mode.py::test_list_roots_fails_in_stateless_mode 
[gw4] [ 50%] PASSED tests/server/test_stateless_mode.py::test_list_roots_fails_in_stateless_mode 
[gw3] [ 58%] PASSED tests/issues/test_973_url_decoding.py::test_template_matches_preserves_plus_sign 
[gw0] [ 66%] PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_space 
[gw1] [ 75%] PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_accented_characters 
tests/server/test_stateless_mode.py::test_elicit_url_fails_in_stateless_mode 
[gw7] [ 83%] PASSED tests/server/test_stateless_mode.py::test_elicit_url_fails_in_stateless_mode 
tests/server/test_stateless_mode.py::test_elicit_form_fails_in_stateless_mode 
[gw6] [ 91%] PASSED tests/server/test_stateless_mode.py::test_elicit_form_fails_in_stateless_mode 
[gw5] [100%] PASSED tests/server/test_stateless_mode.py::test_create_message_fails_in_stateless_mode 

═══════════════════════════════ inline-snapshot ════════════════════════════════
INFO: inline-snapshot was disabled because you used xdist. This means that tests
with snapshots will continue to run, but snapshot(x) will only return x and 
inline-snapshot will not be able to fix snapshots or generate reports.



==================================== PASSES ====================================
- generated xml file: /tmp/rebench-grade-e3d37c85044a450390e64766fb7aeff5.xml --
=========================== short test summary info ============================
PASSED tests/server/test_stateless_mode.py::test_stateless_error_message_is_actionable
PASSED tests/server/test_stateless_mode.py::test_elicit_deprecated_fails_in_stateless_mode
PASSED tests/server/test_stateless_mode.py::test_stateful_mode_does_not_raise_stateless_error
PASSED tests/server/test_stateless_mode.py::test_exception_has_method_attribute
PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_complex_phrase
PASSED tests/server/test_stateless_mode.py::test_list_roots_fails_in_stateless_mode
PASSED tests/issues/test_973_url_decoding.py::test_template_matches_preserves_plus_sign
PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_space
PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_accented_characters
PASSED tests/server/test_stateless_mode.py::test_elicit_url_fails_in_stateless_mode
PASSED tests/server/test_stateless_mode.py::test_elicit_form_fails_in_stateless_mode
PASSED tests/server/test_stateless_mode.py::test_create_message_fails_in_stateless_mode
============================= 12 passed in 25.27s ==============================
Test errors Included
Test report (JUnit) Included
<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="12" time="25.271" timestamp="2026-11-14T03:40:33.096634+00:00" hostname="f37668243cb2"><testcase classname="tests.issues.test_973_url_decoding" name="test_template_matches_decodes_complex_phrase" time="0.013" /><testcase classname="tests.server.test_stateless_mode" name="test_stateless_error_message_is_actionable" time="0.007" /><testcase classname="tests.server.test_stateless_mode" name="test_elicit_deprecated_fails_in_stateless_mode" time="0.007" /><testcase classname="tests.server.test_stateless_mode" name="test_stateful_mode_does_not_raise_stateless_error" time="0.007" /><testcase classname="tests.server.test_stateless_mode" name="test_exception_has_method_attribute" time="0.007" /><testcase classname="tests.server.test_stateless_mode" name="test_list_roots_fails_in_stateless_mode" time="0.013" /><testcase classname="tests.issues.test_973_url_decoding" name="test_template_matches_preserves_plus_sign" time="0.106" /><testcase classname="tests.issues.test_973_url_decoding" name="test_template_matches_decodes_space" time="0.008" /><testcase classname="tests.issues.test_973_url_decoding" name="test_template_matches_decodes_accented_characters" time="0.106" /><testcase classname="tests.server.test_stateless_mode" name="test_elicit_url_fails_in_stateless_mode" time="0.002" /><testcase classname="tests.server.test_stateless_mode" name="test_elicit_form_fails_in_stateless_mode" time="0.007" /><testcase classname="tests.server.test_stateless_mode" name="test_create_message_fails_in_stateless_mode" time="0.004" /></testsuite></testsuites>
Agent log Included
  [   0.0s] 📋 System Prompt: You are a principal Python software engineer resolving a repository issue in `/workspace`.
  [   0.0s] 🎯 Task Prompt: You are evaluating a software engineering task for repository 
modelcontextprotocol/python-sdk.
  [  12.8s] 💭 (609 chars) "The user is reporting a bug where URL parameters in dynamic resources are not automatically 
URL-decoded in the MCP Pytho..."
  [  12.8s] 🔧 run_command grep -rn --include="*.py" "resource" src/mcp | grep -v "tests" | head -n 20
  [  12.9s]   → src/mcp/shared/auth_utils.py:9:def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str:
  [  17.9s] 💭 (123 chars) "The previous `grep` didn't show the decorator implementation. I'll search for `resource(` to
find the decorator definiti..."
  [  17.9s] 🔧 run_command grep -rn --include="*.py" "def resource" src/mcp
  [  17.9s]   → src/mcp/shared/auth_utils.py:9:def resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str:
  [  27.1s] 💭 (119 chars) "It seems `src/mcp/server/fastmcp/server.py` contains the `resource` decorator. Let's examine
that file around line 519."
  [  27.1s] 📖 read_file (src/mcp/server/fastmcp/server.py)
  [  27.1s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/server.py', 'content': '\n        Example:\n       
@mcp.comple…
  [  33.7s] 💭 (230 chars) "The `resource` decorator calls `self._resource_manager.add_template`. I need to find where 
the `ResourceManager` is defi..."
  [  33.7s] 🔧 run_command grep -rn --include="*.py" "class ResourceManager" src/mcp
  [  33.7s]   → src/mcp/server/fastmcp/resources/resource_manager.py:23:class ResourceManager:
  [  36.6s] 💭 (69 chars) "Let's examine `src/mcp/server/fastmcp/resources/resource_manager.py`."
  [  36.6s] 📖 read_file (src/mcp/server/fastmcp/resources/resource_manager.py)
  [  36.7s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/resources/resource_manager.py', 'content': 
'"""Resource manager fun…
  [  48.3s] 💭 (618 chars) "In `ResourceManager.get_resource`, the code does:

```python
        # Then check templates
        for template in self..."
  [  48.3s] 📖 read_file (src/mcp/server/fastmcp/resources/templates.py)
  [  48.3s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/resources/templates.py', 'content': '"""Resource 
template functiona…
  [ 104.1s] 💭 (1453 chars) "The issue is in `ResourceTemplate.matches`.

```python
    def matches(self, uri: str) -> dict[str, Any] | None:
       ..."
  [ 104.1s] 🔧 run_command (41 lines) python3 - <<'PY'
  [ 105.1s]   → error (exit 0): 
  [ 153.9s] 💭 (3100 chars) "The reproduction script confirms the bug. `template.matches(uri)` returns the URL-encoded 
string.

I should use `urllib...."
  [ 153.9s] 🔧 run_command find tests -name "*resource*"
  [ 153.9s]   → tests/server/fastmcp/resources
  [ 157.7s] 💭 (119 chars) "I'll check `tests/server/fastmcp/resources/test_resource_template.py` to see if there are 
any tests for URL parameters."
  [ 157.7s] 📖 read_file (tests/server/fastmcp/resources/test_resource_template.py)
  [ 157.7s]   ✓ {'status': 'ok', 'filepath': 'tests/server/fastmcp/resources/test_resource_template.py', 'content': 
'import json\nfrom t…
  [ 199.7s] 💭 (2480 chars) "Existing tests don't cover URL-encoded characters.

Now I will implement the fix in `src/mcp/server/fastmcp/resources/te..."
  [ 199.7s] ✅ Agent finished: <|tool_call>call 
edit_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>,new_string:<|"|>import inspect
import re
from urllib.parse import unquote
from collections.abc import Callab...
  [ 199.7s] 🎯 Task Prompt: Your previous response reached the token limit before the tool call finished closing 
(<|tool_call|> was cut off). Do NOT...
  [ 222.8s] ✏️ edit_file (src/mcp/server/fastmcp/resources/templates.py)
  [ 222.9s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/resources/templates.py', 'occurrences': 1, 
'strategy': 'unescaped',…
  [ 232.6s] 🔧 run_command (37 lines) python3 - <<'PY'
  [ 233.0s]   → error (exit 0): 
  [ 249.5s] 💭 (602 chars) "Wait, the previous `edit_file` failed to add the import because I missed the import block in
my previous attempt (the pr..."
  [ 249.5s] 📖 read_file (src/mcp/server/fastmcp/resources/templates.py)
  [ 249.5s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/resources/templates.py', 'content': '"""Resource 
template functiona…
  [ 253.9s] 💭 (72 chars) "The import is missing. I need to add `from urllib.parse import unquote`."
  [ 253.9s] ✏️ edit_file (src/mcp/server/fastmcp/resources/templates.py)
  [ 254.0s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/resources/templates.py', 'occurrences': 1, 
'strategy': 'exact', 'di…
  [ 268.1s] 💭 (36 chars) "Now run the reproduction test again."
  [ 268.1s] 🔧 run_command (37 lines) python3 - <<'PY'
  [ 268.4s]   → Params extracted: {'product_identifier': 'stick correcteur teinté'}
  [ 274.4s] 💭 (160 chars) "The reproduction test now passes. Now I should run the existing tests in 
`tests/server/fastmcp/resources/test_resource_t..."
  [ 274.4s] 🔧 run_command pytest tests/server/fastmcp/resources/test_resource_template.py -q --tb=short --
  [ 299.7s]   → bringing up nodes...
  [ 306.4s] 💭 (79 chars) "All tests passed. Now I will check for any untracked files and verify the diff."
  [ 306.4s] 🔧 run_command git status --short && git diff
  [ 306.4s]   →  M src/mcp/server/fastmcp/resources/templates.py
  [ 360.8s] 💭 (477 chars) "The diff looks clean and correct. I have implemented the fix and verified it with both a 
reproduction script and the exi..."
  [ 360.8s] 🤖 submit_patch
  [ 360.9s]   → submit_patch: {"status": "ok", "patch_size": 831, "files_changed": 1}
  [ 368.4s] ✅ Agent finished: The `ResourceTemplate.matches` method in `src/mcp/server/fastmcp/resources/templates.py` 
was returning URL parameters in their raw, encoded form. I updated the method to apply `urllib.parse.unquote` t...
Native result row
{
  "agent_error": null,
  "duration_seconds": 405.07,
  "error": null,
  "grade": {
    "collected_tests": 12,
    "competition_junit_ok": true,
    "error": null,
    "required_issues_count": 0,
    "resolved": true,
    "status_counts": {
      "PASSED": 12
    },
    "strict_clean_patch": true,
    "test_exit_code": 0
  },
  "instance_id": "modelcontextprotocol__python-sdk-1864",
  "pass_idx": 0,
  "patch_chars": 831,
  "phase1_seconds": 377.93,
  "repo": "modelcontextprotocol/python-sdk",
  "request_log_path": "/kaggle-root/working/r31/rollouts_X/requests/modelcontextprotocol__python-sdk-1864__p0__r5.jsonl",
  "resolved": true,
  "rollout_idx": 5,
  "run_key": "modelcontextprotocol__python-sdk-1864__p0__r5",
  "strict_clean_patch": true,
  "tool_calls": 16,
  "total_llm_calls": 19,
  "trace_path": "/kaggle-root/working/r31/rollouts_X/traces/trace_modelcontextprotocol__python-sdk-1864__p0__r5.json"
}