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

modelcontextprotocol__python-sdk-1864

Solved
438 stotal time
19tool calls
22model calls
5,075tokens generated
187,424prompt tokens read

No error recorded

⚠ The request log has 21 model calls but the trace has 24 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 29 s300 tokens out3,031 prompt tokens
The user is reporting an issue where dynamic URL resource parameters in the MCP Python SDK are not being automatically URL-decoded.
Example: `products://search/{product_identifier}` with a value like `stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc` should be decoded to `stick correcteur teinté anti-imperfections rechargeable au zinc`.

I need to find where the resource parameter extraction happens in the `mcp` package.
Based on the provided example, there is an `@mcp.resource` decorator.
Looking at the workspace layout, `src/mcp/server` seems like a likely place for this logic.

Plan:
1. Find the definition of the `@mcp.resource` decorator.
2. Trace how the parameters are extracted from the URI and passed to the handler.
3. Identify where `urllib.parse.unquote` (or similar) should be inserted.
4. Create a reproduction test case.
5. Apply the fix.
6. Verify the fix.

Let's start by searching for `@mcp.resource` or where resources are handled.
TOOL CALL · run_command
{
  "command": "grep -rn --include=\"*.py\" \"resource(\" src/mcp | grep \"def\""
}
Result of run_command 1,339 characters
{
  "status": "ok",
  "stdout": "src/mcp/server/fastmcp/resources/templates.py:94:    async def create_resource(\nsrc/mcp/server/fastmcp/resources/resource_manager.py:31:    def add_resource(self, resource: Resource) -> Resource:\nsrc/mcp/server/fastmcp/resources/resource_manager.py:84:    async def get_resource(\nsrc/mcp/server/fastmcp/server.py:367:    async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]:\nsrc/mcp/server/fastmcp/server.py:511:    def add_resource(self, resource: Resource) -> None:\nsrc/mcp/server/fastmcp/server.py:519:    def resource(\nsrc/mcp/server/fastmcp/server.py:1119:    async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:\nsrc/mcp/server/lowlevel/server.py:337:    def read_resource(self):\nsrc/mcp/server/lowlevel/server.py:413:    def subscribe_resource(self):  # pragma: no cover\nsrc/mcp/server/lowlevel/server.py:426:    def unsubscribe_resource(self):  # pragma: no cover\nsrc/mcp/client/session.py:282:    async def read_resource(self, uri: str | AnyUrl) -> types.ReadResourceResult:\nsrc/mcp/client/session.py:289:    async def subscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult:\nsrc/mcp/client/session.py:296:    async def unsubscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult:\n",
  "stderr": "",
  "exit_code": 0
}
Turn 2at 54 s302 tokens out3,826 prompt tokens
The example uses `@mcp.resource("products://search/{product_identifier}")`. This looks like a decorator.
`src/mcp/server/fastmcp/server.py:519` has `def resource(`. This is likely the implementation of the `@mcp.resource` decorator in `FastMCP`.

Let's examine `src/mcp/server/fastmcp/server.py` around line 519 and also see how these resources are actually resolved when `read_resource` is called.

`src/mcp/server/fastmcp/server.py` also contains `read_resource` at line 367 and 1119.

Wait, the example code shows:
```python
@mcp.resource("products://search/{product_identifier}")
    def find_similar_products(product_identifier: str):
```
This suggests that `FastMCP` handles the mapping of the URI template to the function arguments.

Let's look at `src/mcp/server/fastmcp/server.py` to see how `@resource` is implemented and how it's used during `read_resource`.
TOOL CALL · read_file
{
  "end_line": 550,
  "filepath": "src/mcp/server/fastmcp/server.py",
  "start_line": 510
}
Result of read_file 1,703 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/fastmcp/server.py",
  "content": "\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:",
  "start_line": 510,
  "end_line": 550,
  "total_lines": 1298,
  "is_truncated": false
}
Turn 3at 60 s76 tokens out4,592 prompt tokens
The `resource` method is a decorator. I need to see the full implementation of `resource` and how it's stored and then invoked.
TOOL CALL · read_file
{
  "end_line": 600,
  "filepath": "src/mcp/server/fastmcp/server.py",
  "start_line": 551
}
Result of read_file 2,386 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/fastmcp/server.py",
  "content": "            @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,",
  "start_line": 551,
  "end_line": 600,
  "total_lines": 1298,
  "is_truncated": false
}
Turn 4at 69 s121 tokens out5,278 prompt tokens
It calls `self._resource_manager.add_template`. Let's find `_resource_manager`.

Looking at `src/mcp/server/fastmcp/resources/resource_manager.py`.
I want to see how templates are handled and how they are matched and called.
I'll read `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 5at 79 s199 tokens out6,571 prompt tokens
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)
                except Exception as e:  # pragma: no cover
                    raise ValueError(f\"Error creating resource from template: {e}\")
```

It calls `template.matches(uri_str)` and then `template.create_resource(uri_str, params, context=context)`.
The `params` are passed to `create_resource`.

Let's look at `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 6at 127 s659 tokens out8,194 prompt tokens
The `matches` method is where the parameters are extracted:

```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
```

`match.groupdict()` returns the extracted parameters as-is from the URI. If the URI contains percent-encoded characters, they remain encoded.

I should use `urllib.parse.unquote` on the values of the returned dict.

Wait, let's verify this with a reproduction script.

First, I need to see how `FastMCP` is instantiated and used to create a server.
I'll create a reproduction script that creates a `FastMCP` server, adds a resource template, and calls `read_resource`.

Wait, `FastMCP` is in `src/mcp/server/fastmcp/server.py`.
I can create a simple script using `FastMCP`.
TOOL CALL · run_command
{
  "command": "python3 - <<'PY'\nfrom mcp.server.fastmcp import FastMCP\nimport asyncio\nfrom urllib.parse import quote\n\nasync def reproduce():\n    mcp = FastMCP(\"repro\")\n\n    @mcp.resource(\"products://search/{product_identifier}\")\n    def find_similar_products(product_identifier: str):\n        return f\"Searching for: {product_identifier}\"\n\n    # Test with percent-encoded string\n    product_id = \"stick correcteur teinté anti-imperfections rechargeable au zinc\"\n    encoded_id = quote(product_id)\n    uri = f\"products://search/{encoded_id}\"\n    \n    print(f\"URI: {uri}\")\n    \n    try:\n        # FastMCP's read_resource returns a generator of ReadResourceContents\n        # We need to see what it returns.\n        # Looking at FastMCP.read_resource implementation...\n        \n        # In src/mcp/server/fastmcp/server.py:\n        # async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]:\n        #     # ... calls self._resource_manager.get_resource(uri, context=context)\n        #     # ...\n        \n        # Let's try calling it directly\n        results = await mcp.read_resource(uri)\n        for result in results:\n            print(f\"Result: {result}\")\n    except Exception as e:\n        print(f\"Error: {e}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(reproduce())\nPY\n"
}
Result of run_command 337 characters
{
  "status": "ok",
  "stdout": "URI: products://search/stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc\nResult: ReadResourceContents(content='Searching for: stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc', mime_type='text/plain', meta=None)\n",
  "stderr": "",
  "exit_code": 0
}
Turn 7at 148 s454 tokens out9,000 prompt tokens
The reproduction confirms the bug. The result is `Searching for: stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc` instead of `Searching for: stick correcteur teinté anti-imperfections rechargeable au zinc`.

Now I need to fix this in `src/mcp/server/fastmcp/resources/templates.py`.

I will modify the `matches` method to decode the values in the result dictionary.

```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:
            params = match.groupdict()
            return {k: unquote(v) for k, v in params.items()}
        return None
```

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

Let's check the imports in `src/mcp/server/fastmcp/resources/templates.py` again.
It doesn't have `urllib.parse`.

I'll apply the fix.
Turn 8at 148 s
<|tool_call>call edit_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>,new_string:<|"|>from __future__ import annotations

import inspect
import re
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from urllib.parse import unquote

from pydantic import BaseModel, Field, validate_call<|"|>,old_string:<|"|>from __future__ import annotations

import inspect
import re
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel, Field, validate_call<|"|>}<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 9at 163 s287 tokens out8,088 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",
  "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"
}
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 10at 169 s251 tokens out8,605 prompt tokens
TOOL CALL · run_command
{
  "command": "python3 - <<'PY'\nfrom mcp.server.fastmcp import FastMCP\nimport asyncio\nfrom urllib.parse import quote\n\nasync def reproduce():\n    mcp = FastMCP(\"repro\")\n\n    @mcp.resource(\"products://search/{product_identifier}\")\n    def find_similar_products(product_identifier: str):\n        return f\"Searching for: {product_identifier}\"\n\n    # Test with percent-encoded string\n    product_id = \"stick correcteur teinté anti-imperfections rechargeable au zinc\"\n    encoded_id = quote(product_id)\n    uri = f\"products://search/{encoded_id}\"\n    \n    print(f\"URI: {uri}\")\n    \n    try:\n        results = await mcp.read_resource(uri)\n        for result in results:\n            print(f\"Result: {result}\")\n    except Exception as e:\n        print(f\"Error: {e}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(reproduce())\nPY\n"
}
Result of run_command 203 characters
{
  "status": "ok",
  "stdout": "URI: products://search/stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc\nError: name 'unquote' is not defined\n",
  "stderr": "",
  "exit_code": 0
}
Turn 11at 193 s301 tokens out8,948 prompt tokens
Wait, I edited `src/mcp/server/fastmcp/resources/templates.py` and added `from urllib.parse import unquote` in my previous attempt, but then the user message said "Your previous response reached the token limit before the tool call finished closing".

Let me check if the import was actually applied.

Looking at the history:
Tool call 6: `edit_file` on `src/mcp/server/fastmcp/resources/templates.py` to add `from urllib.parse import unquote`.
The output for tool call 6 was NOT shown in the history provided to me in the latest turn (the user just said "Your previous response reached the token limit").

Actually, the user said:
"Your previous response reached the token limit before the tool call finished closing (<|tool_call|> was cut off). Do NOT repeat your prior reasoning... emit your next tool call immediately".

So tool call 6 (the import) might not have been registered by the system if it was cut off.

Let me verify the contents of `src/mcp/server/fastmcp/resources/templates.py` to see if `unquote` is imported.
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 12at 200 s139 tokens out9,533 prompt tokens
Yes, `unquote` is missing. I need to add it.
TOOL CALL · edit_file
{
  "filepath": "src/mcp/server/fastmcp/resources/templates.py",
  "new_string": "import inspect\nimport re\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\nfrom urllib.parse import unquote\n\nfrom pydantic import BaseModel, Field, validate_call",
  "old_string": "import inspect\nimport re\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\n\nfrom pydantic import BaseModel, Field, validate_call"
}
Result of edit_file 461 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@@ -6,6 +6,7 @@\n import re\n from collections.abc import Callable\n from typing import TYPE_CHECKING, Any\n+from urllib.parse import unquote\n \n from pydantic import BaseModel, Field, validate_call\n \n",
  "is_truncated": false
}
Turn 13at 210 s275 tokens out9,844 prompt tokens
Now let's run the reproduction script again.
TOOL CALL · run_command
{
  "command": "python3 - <<'PY'\nfrom mcp.server.fastmcp import FastMCP\nimport asyncio\nfrom urllib.parse import quote\n\nasync def reproduce():\n    mcp = FastMCP(\"repro\")\n\n    @mcp.resource(\"products://search/{product_identifier}\")\n    def find_similar_products(product_identifier: str):\n        return f\"Searching for: {product_identifier}\"\n\n    # Test with percent-encoded string\n    product_id = \"stick correcteur teinté anti-imperfections rechargeable au zinc\"\n    encoded_id = quote(product_id)\n    uri = f\"products://search/{encoded_id}\"\n    \n    print(f\"URI: {uri}\")\n    \n    try:\n        results = await mcp.read_resource(uri)\n        for result in results:\n            print(f\"Result: {result}\")\n    except Exception as e:\n        import traceback\n        traceback.print_exc()\n        print(f\"Error: {e}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(reproduce())\nPY\n"
}
Result of run_command 325 characters
{
  "status": "ok",
  "stdout": "URI: products://search/stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc\nResult: ReadResourceContents(content='Searching for: stick correcteur teinté anti-imperfections rechargeable au zinc', mime_type='text/plain', meta=None)\n",
  "stderr": "",
  "exit_code": 0
}
Turn 14at 242 s522 tokens out10,240 prompt tokens
The reproduction now passes.

Now I should check if there are other places where resource parameters are extracted and not decoded.

I've checked `src/mcp/server/fastmcp/resources/templates.py`, which seems to be the core of how dynamic resources are handled in `FastMCP`.

Let's check `src/mcp/server/fastmcp/server.py` just in case.
I already saw `read_resource` there:

```python
    async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]:
        # ...
        resource = await self._resource_manager.get_resource(uri, context=context)
        # ...
```

And `ResourceManager.get_resource` in `src/mcp/server/fastmcp/resources/resource_manager.py`:

```python
    async def get_resource(
        self,
        uri: AnyUrl | str,
        context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
    ) -> Resource | None:
        \"\"\"Get resource by URI, checking concrete resources first, then templates.\"\"\"
        uri_str = str(uri)
        logger.debug(\"Getting resource\", extra={\"uri\": uri_str})

        # First check concrete resources
        if resource := self._resources.get(uri_str):
            return resource

        # 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}\")
```

The decoding happens in `template.matches(uri_str)`, which I've already patched.

Is there any other place?
`src/mcp/server/lowlevel/server.py` had `read_resource`, but that's probably for the low-level server which might be used differently.

Let's check `src/mcp/server/lowlevel/server.py`.
TOOL CALL · read_file
{
  "filepath": "src/mcp/server/lowlevel/server.py"
}
Result of read_file 5,367 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/lowlevel/server.py",
  "content": "\"\"\"\nMCP Server Module\n\nThis module provides a framework for creating an MCP (Model Context Protocol) server.\nIt allows you to easily define and handle various types of requests and notifications\nin an asynchronous manner.\n\nUsage:\n1. Create a Server instance:\n   server = Server(\"your_server_name\")\n\n2. Define request handlers using decorators:\n   @server.list_prompts()\n   async def handle_list_prompts(request: types.ListPromptsRequest) -> types.ListPromptsResult:\n       # Implementation\n\n   @server.get_prompt()\n   async def handle_get_prompt(\n       name: str, arguments: dict[str, str] | None\n   ) -> types.GetPromptResult:\n       # Implementation\n\n   @server.list_tools()\n   async def handle_list_tools(request: types.ListToolsRequest) -> types.ListToolsResult:\n       # Implementation\n\n   @server.call_tool()\n   async def handle_call_tool(\n       name: str, arguments: dict | None\n   ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:\n       # Implementation\n\n   @server.list_resource_templates()\n   async def handle_list_resource_templates() -> list[types.ResourceTemplate]:\n       # Implementation\n\n3. Define notification handlers if needed:\n   @server.progress_notification()\n   async def handle_progress(\n       progress_token: str | int, progress: float, total: float | None,\n       message: str | None\n   ) -> None:\n       # Implementation\n\n4. Run the server:\n   async def main():\n       async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n           await server.run(\n               read_stream,\n               write_stream,\n               InitializationOptions(\n                   server_name=\"your_server_name\",\n                   server_version=\"your_version\",\n                   capabilities=server.get_capabilities(\n                       notification_options=NotificationOptions(),\n                       experimental_capabilities={},\n                   ),\n               ),\n           )\n\n   asyncio.run(main())\n\nThe Server class provides methods to register handlers for various MCP requests and\nnotifications. It automatically manages the request context and handles incoming\nmessages from the client.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport contextvars\nimport json\nimport logging\nimport warnings\nfrom collections.abc import AsyncIterator, Awaitable, Callable, Iterable\nfrom contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager\nfrom typing import Any, Generic, TypeAlias, cast\n\nimport anyio\nimport jsonschema\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom typing_extensions import TypeVar\n\nimport mcp.types as types\nfrom mcp.server.experimental.request_context import Experimental\nfrom mcp.server.lowlevel.experimental import ExperimentalHandlers\nfrom mcp.server.lowlevel.func_inspection import create_call_wrapper\nfrom mcp.server.lowlevel.helper_types import ReadResourceContents\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.context import RequestContext\nfrom mcp.shared.exceptions import McpError, UrlElicitationRequiredError\nfrom mcp.shared.message import ServerMessageMetadata, SessionMessage\nfrom mcp.shared.session import RequestResponder\nfrom mcp.shared.tool_name_validation import validate_and_warn_tool_name\n\nlogger = logging.getLogger(__name__)\n\nLifespanResultT = TypeVar(\"LifespanResultT\", default=Any)\nRequestT = TypeVar(\"RequestT\", default=Any)\n\n# type aliases for tool call results\nStructuredContent: TypeAlias = dict[str, Any]\nUnstructuredContent: TypeAlias = Iterable[types.ContentBlock]\nCombinationContent: TypeAlias = tuple[UnstructuredContent, StructuredContent]\n\n# This will be properly typed in each Server instance's context\nrequest_ctx: contextvars.ContextVar[RequestContext[ServerSession, Any, Any]] = contextvars.ContextVar(\"request_ctx\")\n\n\nclass NotificationOptions:\n    def __init__(\n        self,\n        prompts_changed: bool = False,\n        resources_changed: bool = False,\n        tools_changed: bool = False,\n    ):\n        self.prompts_changed = prompts_changed\n        self.resources_changed = resources_changed\n        self.tools_changed = tools_changed\n\n\n@asynccontextmanager\nasync def lifespan(_: Server[LifespanResultT, RequestT]) -> AsyncIterator[dict[str, Any]]:\n    \"\"\"Default lifespan context manager that does nothing.\n\n    Args:\n        server: The server instance this lifespan is managing\n\n    Returns:\n        An empty context object\n    \"\"\"\n    yield {}\n\n\nclass Server(Generic[LifespanResultT, RequestT]):\n    def __init__(\n        self,\n        name: str,\n        version: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        instructions: str | None = None,\n        website_url: str | None = None,\n        icons: list[types.Icon] | None = None,\n        lifespan: Callable[\n            [Server[LifespanResultT, RequestT]],\n            AbstractAsyncContextManager[LifespanResultT],\n        ] = lifespan,\n    ):",
  "start_line": 1,
  "end_line": 150,
  "total_lines": 808,
  "is_truncated": true
}
Turn 15at 245 s60 tokens out12,200 prompt tokens
Searching for `read_resource` in `src/mcp/server/lowlevel/server.py`.
TOOL CALL · run_command
{
  "command": "grep -n \"read_resource\" src/mcp/server/lowlevel/server.py"
}
Result of run_command 299 characters
{
  "status": "ok",
  "stdout": "337:    def read_resource(self):\n368:                            \"Returning str or bytes from read_resource is deprecated. \"\n387:                        raise ValueError(f\"Unexpected return type from read_resource: {type(result)}\")\n",
  "stderr": "",
  "exit_code": 0
}
Turn 16at 254 s105 tokens out12,357 prompt tokens
The `read_resource` in `Server` class of `lowlevel/server.py` seems to be a concentrator/manager of registered handlers.

Wait, let's see line 337 of `src/mcp/server/lowlevel/server.py`.
TOOL CALL · read_file
{
  "end_line": 360,
  "filepath": "src/mcp/server/lowlevel/server.py",
  "start_line": 330
}
Result of read_file 1,717 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/lowlevel/server.py",
  "content": "                return types.ServerResult(types.ListResourceTemplatesResult(resourceTemplates=templates))\n\n            self.request_handlers[types.ListResourceTemplatesRequest] = handler\n            return func\n\n        return decorator\n\n    def read_resource(self):\n        def decorator(\n            func: Callable[[str], Awaitable[str | bytes | Iterable[ReadResourceContents]]],\n        ):\n            logger.debug(\"Registering handler for ReadResourceRequest\")\n\n            async def handler(req: types.ReadResourceRequest):\n                result = await func(req.params.uri)\n\n                def create_content(data: str | bytes, mime_type: str | None, meta: dict[str, Any] | None = None):\n                    # Note: ResourceContents uses Field(alias=\"_meta\"), so we must use the alias key\n                    meta_kwargs: dict[str, Any] = {\"_meta\": meta} if meta is not None else {}\n                    match data:\n                        case str() as data:\n                            return types.TextResourceContents(\n                                uri=req.params.uri,\n                                text=data,\n                                mimeType=mime_type or \"text/plain\",\n                                **meta_kwargs,\n                            )\n                        case bytes() as data:  # pragma: no cover\n                            return types.BlobResourceContents(\n                                uri=req.params.uri,\n                                blob=base64.b64encode(data).decode(),",
  "start_line": 330,
  "end_line": 360,
  "total_lines": 808,
  "is_truncated": false
}
Turn 17at 278 s301 tokens out12,882 prompt tokens
In `src/mcp/server/lowlevel/server.py`, `read_resource` is a decorator that registers a handler for `ReadResourceRequest`.
The handler calls `func(req.params.uri)`.

```python
            async def handler(req: types.ReadResourceRequest):
                result = await func(req.params.uri)
```

Unlike `FastMCP`, `Server` in `lowlevel/server.py` doesn't seem to have built-in URI template matching logic. It just passes the URI as-is to the provided function. If the user defined a handler for `read_resource` in the low-level SDK, they're responsible for parsing and decoding the URI.

The issue specifically mentioned "Dynamic URL resource parameters in MCP protocol are not automatically URL-decoded", and the example provided used the `@mcp.resource(...)` decorator, which is a `FastMCP` feature.

So the fix in `FastMCP`'s `ResourceTemplate.matches` is correct and sufficient for the reported issue.

One last check: are there any other "Dynamic URL resource parameters" in `FastMCP`?

`FastMCP` also has tools. Let's check if those are handled similarly.
TOOL CALL · run_command
{
  "command": "grep -rn \"template\" src/mcp/server/fastmcp | grep \"matches\""
}
Result of run_command 293 characters
{
  "status": "ok",
  "stdout": "src/mcp/server/fastmcp/resources/templates.py:87:        \"\"\"Check if URI matches template and extract parameters.\"\"\"\nsrc/mcp/server/fastmcp/resources/resource_manager.py:99:            if params := template.matches(uri_str):\n",
  "stderr": "",
  "exit_code": 0
}
Turn 18at 286 s101 tokens out13,293 prompt tokens
Only `ResourceTemplate` uses `.matches()`.

What about tools? Tools usually take named arguments from a JSON payload, not from a URI.

Let's verify `src/mcp/server/fastmcp/server.py` one more time to see if there are other template-like features.
TOOL CALL · run_command
{
  "command": "grep -rn \"template\" src/mcp/server/fastmcp/server.py"
}
Result of run_command 992 characters
{
  "status": "ok",
  "stdout": "298:        self._mcp_server.list_resource_templates()(self.list_resource_templates)\n351:    async def list_resource_templates(self) -> list[MCPResourceTemplate]:\n352:        templates = self._resource_manager.list_templates()\n355:                uriTemplate=template.uri_template,\n356:                name=template.name,\n357:                title=template.title,\n358:                description=template.description,\n359:                mimeType=template.mime_type,\n360:                icons=template.icons,\n361:                annotations=template.annotations,\n362:                _meta=template.meta,\n364:            for template in templates\n540:        has parameters, it will be registered as a template resource.\n577:            # Check if this should be a template\n597:                # Register as template\n598:                self._resource_manager.add_template(\n600:                    uri_template=uri,\n",
  "stderr": "",
  "exit_code": 0
}
Turn 19at 295 s82 tokens out13,687 prompt tokens
It seems they are all related to resources.

I'll double check the `FastMCP.read_resource` implementation to ensure I didn't miss anything.
TOOL CALL · read_file
{
  "end_line": 1150,
  "filepath": "src/mcp/server/fastmcp/server.py",
  "start_line": 1100
}
Result of read_file 2,228 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/fastmcp/server.py",
  "content": "        \"\"\"Report progress for the current operation.\n\n        Args:\n            progress: Current progress value e.g. 24\n            total: Optional total value e.g. 100\n            message: Optional message e.g. Starting render...\n        \"\"\"\n        progress_token = self.request_context.meta.progressToken if self.request_context.meta else None\n\n        if progress_token is None:  # pragma: no cover\n            return\n\n        await self.request_context.session.send_progress_notification(\n            progress_token=progress_token,\n            progress=progress,\n            total=total,\n            message=message,\n        )\n\n    async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:\n        \"\"\"Read a resource by URI.\n\n        Args:\n            uri: Resource URI to read\n\n        Returns:\n            The resource content as either text or bytes\n        \"\"\"\n        assert self._fastmcp is not None, \"Context is not available outside of a request\"\n        return await self._fastmcp.read_resource(uri)\n\n    async def elicit(\n        self,\n        message: str,\n        schema: type[ElicitSchemaModelT],\n    ) -> ElicitationResult[ElicitSchemaModelT]:\n        \"\"\"Elicit information from the client/user.\n\n        This method can be used to interactively ask for additional information from the\n        client within a tool's execution. The client might display the message to the\n        user and collect a response according to the provided schema. Or in case a\n        client is an agent, it might decide how to handle the elicitation -- either by asking\n        the user or automatically generating a response.\n\n        Args:\n            schema: A Pydantic model class defining the expected response structure, according to the specification,\n                    only primive types are allowed.\n            message: Optional message to present to the user. If not provided, will use\n                    a default message based on the schema\n\n        Returns:",
  "start_line": 1100,
  "end_line": 1150,
  "total_lines": 1298,
  "is_truncated": false
}
Turn 20at 304 s151 tokens out14,353 prompt tokens
Wait, there are two `read_resource` methods in `src/mcp/server/fastmcp/server.py`?

Line 367: `async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]:`
Line 1119: `async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:`

Let's look at line 367 again.
TOOL CALL · read_file
{
  "end_line": 380,
  "filepath": "src/mcp/server/fastmcp/server.py",
  "start_line": 360
}
Result of read_file 1,048 characters
{
  "status": "ok",
  "filepath": "src/mcp/server/fastmcp/server.py",
  "content": "                icons=template.icons,\n                annotations=template.annotations,\n                _meta=template.meta,\n            )\n            for template in templates\n        ]\n\n    async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]:\n        \"\"\"Read a resource by URI.\"\"\"\n\n        context = self.get_context()\n        resource = await self._resource_manager.get_resource(uri, context=context)\n        if not resource:  # pragma: no cover\n            raise ResourceError(f\"Unknown resource: {uri}\")\n\n        try:\n            content = await resource.read()\n            return [ReadResourceContents(content=content, mime_type=resource.mime_type, meta=resource.meta)]\n        except Exception as e:  # pragma: no cover\n            logger.exception(f\"Error reading resource {uri}\")\n            raise ResourceError(str(e))",
  "start_line": 360,
  "end_line": 380,
  "total_lines": 1298,
  "is_truncated": false
}
Turn 21at 399 s269 tokens out3,859 prompt tokens
Okay, and line 1119 is likely part of a `FastMCPContext` child class or something.

Actually, if I look at the `FastMCP` class, it extends something?

No, I see `FastMCP` and then `FastMCPContext` at line 1052.
Line 1119 is inside `FastMCPContext`.

```python
    async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:
        """Read a resource by URI.

        Args:
            uri: Resource URI to read

        Returns:
            The resource content as either text or bytes
        """
        assert self._fastmcp is not None, "Context is not available outside of a request"
        return await self._fastmcp.read_resource(uri)
```

This just delegates to `self._fastmcp.read_resource(uri)`, which is the method at line 367.

So everything is consistent. I've fixed the resource decoding in the only place where templates are matched.

Final check on `git status --short`.
TOOL CALL · run_command
{
  "command": "git status --short && git diff"
}
Result of run_command 991 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..efaa596 100644\n--- a/src/mcp/server/fastmcp/resources/templates.py\n+++ b/src/mcp/server/fastmcp/resources/templates.py\n@@ -6,6 +6,7 @@ import inspect\n import re\n from collections.abc import Callable\n from typing import TYPE_CHECKING, Any\n+from urllib.parse import unquote\n \n from pydantic import BaseModel, Field, validate_call\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 22at 402 s33 tokens out4,485 prompt tokens
The diff is clean. No untracked files. No changes in `tests/`.

I am confident.
TOOL CALL · submit_patch
Result of submit_patch 55 characters
{
  "status": "ok",
  "patch_size": 849,
  "files_changed": 1
}
Turn 23at 409 s87 tokens out4,558 prompt tokens
The issue is fixed. I'll provide the final summary.
Turn 24at 409 s
The bug was caused by URI template parameters in `FastMCP` being extracted via regex but not URL-decoded, leading to encoded characters (e.g., `%20`) being passed to resource handlers. I fixed this by applying `urllib.parse.unquote` to all extracted parameter values in the `ResourceTemplate.matches` method.

Model calls (not matched to turns)

Generated tokens 300 tokens · stopped: STOP
The user is reporting an issue where dynamic URL resource parameters in the MCP Python SDK are not being automatically URL-decoded.
Example: `products://search/{product_identifier}` with a value like `stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc` should be decoded to `stick correcteur teinté anti-imperfections rechargeable au zinc`.

I need to find where the resource parameter extraction happens in the `mcp` package.
Based on the provided example, there is an `@mcp.resource` decorator.
Looking at the workspace layout, `src/mcp/server` seems like a likely place for this logic.

Plan:
1. Find the definition of the `@mcp.resource` decorator.
2. Trace how the parameters are extracted from the URI and passed to the handler.
3. Identify where `urllib.parse.unquote` (or similar) should be inserted.
4. Create a reproduction test case.
5. Apply the fix.
6. Verify the fix.

Let's start by searching for `@mcp.resource` or where resources are handled.
<|channel>thought The user is reporting an issue where dynamic URL resource parameters in the MCP Python SDK are not being automatically URL-decoded. Example: `products://search/{product_identifier}` with a value like `stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc` should be decoded to `stick correcteur teinté anti-imperfections rechargeable au zinc`. I need to find where the resource parameter extraction happens in the `mcp` package. Based on the provided example, there is an `@mcp.resource` decorator. Looking at the workspace layout, `src/mcp/server` seems like a likely place for this logic. Plan: 1. Find the definition of the `@mcp.resource` decorator. 2. Trace how the parameters are extracted from the URI and passed to the handler. 3. Identify where `urllib.parse.unquote` (or similar) should be inserted. 4. Create a reproduction test case. 5. Apply the fix. 6. Verify the fix. Let's start by searching for `@mcp.resource` or where resources are handled.<channel|><|tool_call>call:run_command{command:<|"|>grep -rn --include="*.py" "resource(" src/mcp | grep "def"<|"|>}<tool_call|><|tool_response>
Generated tokens 302 tokens · stopped: STOP
The example uses `@mcp.resource("products://search/{product_identifier}")`. This looks like a decorator.
`src/mcp/server/fastmcp/server.py:519` has `def resource(`. This is likely the implementation of the `@mcp.resource` decorator in `FastMCP`.

Let's examine `src/mcp/server/fastmcp/server.py` around line 519 and also see how these resources are actually resolved when `read_resource` is called.

`src/mcp/server/fastmcp/server.py` also contains `read_resource` at line 367 and 1119.

Wait, the example code shows:
```python
@mcp.resource("products://search/{product_identifier}")
    def find_similar_products(product_identifier: str):
```
This suggests that `FastMCP` handles the mapping of the URI template to the function arguments.

Let's look at `src/mcp/server/fastmcp/server.py` to see how `@resource` is implemented and how it's used during `read_resource`.
The example uses `@mcp.resource("products://search/{product_identifier}")`. This looks like a decorator. `src/mcp/server/fastmcp/server.py:519` has `def resource(`. This is likely the implementation of the `@mcp.resource` decorator in `FastMCP`. Let's examine `src/mcp/server/fastmcp/server.py` around line 519 and also see how these resources are actually resolved when `read_resource` is called. `src/mcp/server/fastmcp/server.py` also contains `read_resource` at line 367 and 1119. Wait, the example code shows: ```python @mcp.resource("products://search/{product_identifier}") def find_similar_products(product_identifier: str): ``` This suggests that `FastMCP` handles the mapping of the URI template to the function arguments. Let's look at `src/mcp/server/fastmcp/server.py` to see how `@resource` is implemented and how it's used during `read_resource`. <channel|><|tool_call>call:read_file{filepath:<|"|>src/mcp/server/fastmcp/server.py<|"|>,start_line:510,end_line:550}<tool_call|><|tool_response>
Generated tokens 76 tokens · stopped: STOP
The `resource` method is a decorator. I need to see the full implementation of `resource` and how it's stored and then invoked.

The `resource` method is a decorator. I need to see the full implementation of `resource` and how it's stored and then invoked. <channel|><|tool_call>call:read_file{end_line:600,filepath:<|"|>src/mcp/server/fastmcp/server.py<|"|>,start_line:551}<tool_call|><|tool_response>
Generated tokens 121 tokens · stopped: STOP
It calls `self._resource_manager.add_template`. Let's find `_resource_manager`.

Looking at `src/mcp/server/fastmcp/resources/resource_manager.py`.
I want to see how templates are handled and how they are matched and called.
I'll read `src/mcp/server/fastmcp/resources/resource_manager.py`.
It calls `self._resource_manager.add_template`. Let's find `_resource_manager`. Looking at `src/mcp/server/fastmcp/resources/resource_manager.py`. I want to see how templates are handled and how they are matched and called. I'll read `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 199 tokens · stopped: STOP
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)
                except Exception as e:  # pragma: no cover
                    raise ValueError(f\"Error creating resource from template: {e}\")
```

It calls `template.matches(uri_str)` and then `template.create_resource(uri_str, params, context=context)`.
The `params` are passed to `create_resource`.

Let's look at `src/mcp/server/fastmcp/resources/templates.py`.
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) except Exception as e: # pragma: no cover raise ValueError(f\"Error creating resource from template: {e}\") ``` It calls `template.matches(uri_str)` and then `template.create_resource(uri_str, params, context=context)`. The `params` are passed to `create_resource`. Let's look at `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 659 tokens · stopped: STOP
The `matches` method is where the parameters are extracted:

```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
```

`match.groupdict()` returns the extracted parameters as-is from the URI. If the URI contains percent-encoded characters, they remain encoded.

I should use `urllib.parse.unquote` on the values of the returned dict.

Wait, let's verify this with a reproduction script.

First, I need to see how `FastMCP` is instantiated and used to create a server.
I'll create a reproduction script that creates a `FastMCP` server, adds a resource template, and calls `read_resource`.

Wait, `FastMCP` is in `src/mcp/server/fastmcp/server.py`.
I can create a simple script using `FastMCP`.

The `matches` method is where the parameters are extracted: ```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 ``` `match.groupdict()` returns the extracted parameters as-is from the URI. If the URI contains percent-encoded characters, they remain encoded. I should use `urllib.parse.unquote` on the values of the returned dict. Wait, let's verify this with a reproduction script. First, I need to see how `FastMCP` is instantiated and used to create a server. I'll create a reproduction script that creates a `FastMCP` server, adds a resource template, and calls `read_resource`. Wait, `FastMCP` is in `src/mcp/server/fastmcp/server.py`. I can create a simple script using `FastMCP`. <channel|><|tool_call>call:run_command{command:<|"|>python3 - <<'PY' from mcp.server.fastmcp import FastMCP import asyncio from urllib.parse import quote async def reproduce(): mcp = FastMCP("repro") @mcp.resource("products://search/{product_identifier}") def find_similar_products(product_identifier: str): return f"Searching for: {product_identifier}" # Test with percent-encoded string product_id = "stick correcteur teinté anti-imperfections rechargeable au zinc" encoded_id = quote(product_id) uri = f"products://search/{encoded_id}" print(f"URI: {uri}") try: # FastMCP's read_resource returns a generator of ReadResourceContents # We need to see what it returns. # Looking at FastMCP.read_resource implementation... # In src/mcp/server/fastmcp/server.py: # async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]: # # ... calls self._resource_manager.get_resource(uri, context=context) # # ... # Let's try calling it directly results = await mcp.read_resource(uri) for result in results: print(f"Result: {result}") except Exception as e: print(f"Error: {e}") if __name__ == "__main__": asyncio.run(reproduce()) PY <|"|>}<tool_call|><|tool_response>
Generated tokens 454 tokens · stopped: STOP
The reproduction confirms the bug. The result is `Searching for: stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc` instead of `Searching for: stick correcteur teinté anti-imperfections rechargeable au zinc`.

Now I need to fix this in `src/mcp/server/fastmcp/resources/templates.py`.

I will modify the `matches` method to decode the values in the result dictionary.

```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:
            params = match.groupdict()
            return {k: unquote(v) for k, v in params.items()}
        return None
```

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

Let's check the imports in `src/mcp/server/fastmcp/resources/templates.py` again.
It doesn't have `urllib.parse`.

I'll apply the fix.

The reproduction confirms the bug. The result is `Searching for: stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc` instead of `Searching for: stick correcteur teinté anti-imperfections rechargeable au zinc`. Now I need to fix this in `src/mcp/server/fastmcp/resources/templates.py`. I will modify the `matches` method to decode the values in the result dictionary. ```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: params = match.groupdict() return {k: unquote(v) for k, v in params.items()} return None ``` Wait, I need to import `unquote` from `urllib.parse`. Let's check the imports in `src/mcp/server/fastmcp/resources/templates.py` again. It doesn't have `urllib.parse`. I'll apply the fix. <channel|><|tool_call>call edit_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>,new_string:<|"|>from __future__ import annotations import inspect import re from collections.abc import Callable from typing import TYPE_CHECKING, Any from urllib.parse import unquote from pydantic import BaseModel, Field, validate_call<|"|>,old_string:<|"|>from __future__ import annotations import inspect import re from collections.abc import Callable from typing import TYPE_CHECKING, Any from pydantic import BaseModel, Field, validate_call<|"|>}<tool_call|><|tool_response>
Generated tokens 287 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 251 tokens · stopped: STOP
<channel|><|tool_call>call:run_command{command:<|"|>python3 - <<'PY' from mcp.server.fastmcp import FastMCP import asyncio from urllib.parse import quote async def reproduce(): mcp = FastMCP("repro") @mcp.resource("products://search/{product_identifier}") def find_similar_products(product_identifier: str): return f"Searching for: {product_identifier}" # Test with percent-encoded string product_id = "stick correcteur teinté anti-imperfections rechargeable au zinc" encoded_id = quote(product_id) uri = f"products://search/{encoded_id}" print(f"URI: {uri}") try: results = await mcp.read_resource(uri) for result in results: print(f"Result: {result}") except Exception as e: print(f"Error: {e}") if __name__ == "__main__": asyncio.run(reproduce()) PY <|"|>}<tool_call|><|tool_response>
Generated tokens 301 tokens · stopped: STOP
Wait, I edited `src/mcp/server/fastmcp/resources/templates.py` and added `from urllib.parse import unquote` in my previous attempt, but then the user message said "Your previous response reached the token limit before the tool call finished closing".

Let me check if the import was actually applied.

Looking at the history:
Tool call 6: `edit_file` on `src/mcp/server/fastmcp/resources/templates.py` to add `from urllib.parse import unquote`.
The output for tool call 6 was NOT shown in the history provided to me in the latest turn (the user just said "Your previous response reached the token limit").

Actually, the user said:
"Your previous response reached the token limit before the tool call finished closing (<|tool_call|> was cut off). Do NOT repeat your prior reasoning... emit your next tool call immediately".

So tool call 6 (the import) might not have been registered by the system if it was cut off.

Let me verify the contents of `src/mcp/server/fastmcp/resources/templates.py` to see if `unquote` is imported.
Wait, I edited `src/mcp/server/fastmcp/resources/templates.py` and added `from urllib.parse import unquote` in my previous attempt, but then the user message said "Your previous response reached the token limit before the tool call finished closing". Let me check if the import was actually applied. Looking at the history: Tool call 6: `edit_file` on `src/mcp/server/fastmcp/resources/templates.py` to add `from urllib.parse import unquote`. The output for tool call 6 was NOT shown in the history provided to me in the latest turn (the user just said "Your previous response reached the token limit"). Actually, the user said: "Your previous response reached the token limit before the tool call finished closing (<|tool_call|> was cut off). Do NOT repeat your prior reasoning... emit your next tool call immediately". So tool call 6 (the import) might not have been registered by the system if it was cut off. Let me verify the contents of `src/mcp/server/fastmcp/resources/templates.py` to see if `unquote` is imported.<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 139 tokens · stopped: STOP
Yes, `unquote` is missing. I need to add it.
Yes, `unquote` is missing. I need to add it. <channel|><|tool_call>call:edit_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>,new_string:<|"|>import inspect import re from collections.abc import Callable from typing import TYPE_CHECKING, Any from urllib.parse import unquote from pydantic import BaseModel, Field, validate_call<|"|>,old_string:<|"|>import inspect import re from collections.abc import Callable from typing import TYPE_CHECKING, Any from pydantic import BaseModel, Field, validate_call<|"|>}<tool_call|><|tool_response>
Generated tokens 275 tokens · stopped: STOP
Now let's run the reproduction script again.
Now let's run the reproduction script again. <channel|><|tool_call>call:run_command{command:<|"|>python3 - <<'PY' from mcp.server.fastmcp import FastMCP import asyncio from urllib.parse import quote async def reproduce(): mcp = FastMCP("repro") @mcp.resource("products://search/{product_identifier}") def find_similar_products(product_identifier: str): return f"Searching for: {product_identifier}" # Test with percent-encoded string product_id = "stick correcteur teinté anti-imperfections rechargeable au zinc" encoded_id = quote(product_id) uri = f"products://search/{encoded_id}" print(f"URI: {uri}") try: results = await mcp.read_resource(uri) for result in results: print(f"Result: {result}") except Exception as e: import traceback traceback.print_exc() print(f"Error: {e}") if __name__ == "__main__": asyncio.run(reproduce()) PY <|"|>}<tool_call|><|tool_response>
Generated tokens 522 tokens · stopped: STOP
The reproduction now passes.

Now I should check if there are other places where resource parameters are extracted and not decoded.

I've checked `src/mcp/server/fastmcp/resources/templates.py`, which seems to be the core of how dynamic resources are handled in `FastMCP`.

Let's check `src/mcp/server/fastmcp/server.py` just in case.
I already saw `read_resource` there:

```python
    async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]:
        # ...
        resource = await self._resource_manager.get_resource(uri, context=context)
        # ...
```

And `ResourceManager.get_resource` in `src/mcp/server/fastmcp/resources/resource_manager.py`:

```python
    async def get_resource(
        self,
        uri: AnyUrl | str,
        context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None,
    ) -> Resource | None:
        \"\"\"Get resource by URI, checking concrete resources first, then templates.\"\"\"
        uri_str = str(uri)
        logger.debug(\"Getting resource\", extra={\"uri\": uri_str})

        # First check concrete resources
        if resource := self._resources.get(uri_str):
            return resource

        # 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}\")
```

The decoding happens in `template.matches(uri_str)`, which I've already patched.

Is there any other place?
`src/mcp/server/lowlevel/server.py` had `read_resource`, but that's probably for the low-level server which might be used differently.

Let's check `src/mcp/server/lowlevel/server.py`.
The reproduction now passes. Now I should check if there are other places where resource parameters are extracted and not decoded. I've checked `src/mcp/server/fastmcp/resources/templates.py`, which seems to be the core of how dynamic resources are handled in `FastMCP`. Let's check `src/mcp/server/fastmcp/server.py` just in case. I already saw `read_resource` there: ```python async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]: # ... resource = await self._resource_manager.get_resource(uri, context=context) # ... ``` And `ResourceManager.get_resource` in `src/mcp/server/fastmcp/resources/resource_manager.py`: ```python async def get_resource( self, uri: AnyUrl | str, context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, ) -> Resource | None: \"\"\"Get resource by URI, checking concrete resources first, then templates.\"\"\" uri_str = str(uri) logger.debug(\"Getting resource\", extra={\"uri\": uri_str}) # First check concrete resources if resource := self._resources.get(uri_str): return resource # 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}\") ``` The decoding happens in `template.matches(uri_str)`, which I've already patched. Is there any other place? `src/mcp/server/lowlevel/server.py` had `read_resource`, but that's probably for the low-level server which might be used differently. Let's check `src/mcp/server/lowlevel/server.py`. <channel|><|tool_call>call:read_file{filepath:<|"|>src/mcp/server/lowlevel/server.py<|"|>}<tool_call|><|tool_response>
Generated tokens 60 tokens · stopped: STOP
Searching for `read_resource` in `src/mcp/server/lowlevel/server.py`.

Searching for `read_resource` in `src/mcp/server/lowlevel/server.py`. <channel|><|tool_call>call:run_command{command:<|"|>grep -n "read_resource" src/mcp/server/lowlevel/server.py<|"|>}<tool_call|><|tool_response>
Generated tokens 105 tokens · stopped: STOP
The `read_resource` in `Server` class of `lowlevel/server.py` seems to be a concentrator/manager of registered handlers.

Wait, let's see line 337 of `src/mcp/server/lowlevel/server.py`.
The `read_resource` in `Server` class of `lowlevel/server.py` seems to be a concentrator/manager of registered handlers. Wait, let's see line 337 of `src/mcp/server/lowlevel/server.py`. <channel|><|tool_call>call:read_file{end_line:360,filepath:<|"|>src/mcp/server/lowlevel/server.py<|"|>,start_line:330}<tool_call|><|tool_response>
Generated tokens 301 tokens · stopped: STOP
In `src/mcp/server/lowlevel/server.py`, `read_resource` is a decorator that registers a handler for `ReadResourceRequest`.
The handler calls `func(req.params.uri)`.

```python
            async def handler(req: types.ReadResourceRequest):
                result = await func(req.params.uri)
```

Unlike `FastMCP`, `Server` in `lowlevel/server.py` doesn't seem to have built-in URI template matching logic. It just passes the URI as-is to the provided function. If the user defined a handler for `read_resource` in the low-level SDK, they're responsible for parsing and decoding the URI.

The issue specifically mentioned "Dynamic URL resource parameters in MCP protocol are not automatically URL-decoded", and the example provided used the `@mcp.resource(...)` decorator, which is a `FastMCP` feature.

So the fix in `FastMCP`'s `ResourceTemplate.matches` is correct and sufficient for the reported issue.

One last check: are there any other "Dynamic URL resource parameters" in `FastMCP`?

`FastMCP` also has tools. Let's check if those are handled similarly.
In `src/mcp/server/lowlevel/server.py`, `read_resource` is a decorator that registers a handler for `ReadResourceRequest`. The handler calls `func(req.params.uri)`. ```python async def handler(req: types.ReadResourceRequest): result = await func(req.params.uri) ``` Unlike `FastMCP`, `Server` in `lowlevel/server.py` doesn't seem to have built-in URI template matching logic. It just passes the URI as-is to the provided function. If the user defined a handler for `read_resource` in the low-level SDK, they're responsible for parsing and decoding the URI. The issue specifically mentioned "Dynamic URL resource parameters in MCP protocol are not automatically URL-decoded", and the example provided used the `@mcp.resource(...)` decorator, which is a `FastMCP` feature. So the fix in `FastMCP`'s `ResourceTemplate.matches` is correct and sufficient for the reported issue. One last check: are there any other "Dynamic URL resource parameters" in `FastMCP`? `FastMCP` also has tools. Let's check if those are handled similarly.<channel|><|tool_call>call:run_command{command:<|"|>grep -rn "template" src/mcp/server/fastmcp | grep "matches"<|"|>}<tool_call|><|tool_response>
Generated tokens 101 tokens · stopped: STOP
Only `ResourceTemplate` uses `.matches()`.

What about tools? Tools usually take named arguments from a JSON payload, not from a URI.

Let's verify `src/mcp/server/fastmcp/server.py` one more time to see if there are other template-like features.
Only `ResourceTemplate` uses `.matches()`. What about tools? Tools usually take named arguments from a JSON payload, not from a URI. Let's verify `src/mcp/server/fastmcp/server.py` one more time to see if there are other template-like features. <channel|><|tool_call>call:run_command{command:<|"|>grep -rn "template" src/mcp/server/fastmcp/server.py<|"|>}<tool_call|><|tool_response>
Generated tokens 82 tokens · stopped: STOP
It seems they are all related to resources.

I'll double check the `FastMCP.read_resource` implementation to ensure I didn't miss anything.

It seems they are all related to resources. I'll double check the `FastMCP.read_resource` implementation to ensure I didn't miss anything. <channel|><|tool_call>call:read_file{end_line:1150,filepath:<|"|>src/mcp/server/fastmcp/server.py<|"|>,start_line:1100}<tool_call|><|tool_response>
Generated tokens 151 tokens · stopped: STOP
Wait, there are two `read_resource` methods in `src/mcp/server/fastmcp/server.py`?

Line 367: `async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]:`
Line 1119: `async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:`

Let's look at line 367 again.

Wait, there are two `read_resource` methods in `src/mcp/server/fastmcp/server.py`? Line 367: `async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContents]:` Line 1119: `async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:` Let's look at line 367 again. <channel|><|tool_call>call:read_file{end_line:380,filepath:<|"|>src/mcp/server/fastmcp/server.py<|"|>,start_line:360}<tool_call|><|tool_response>
Generated tokens 269 tokens · stopped: STOP
Okay, and line 1119 is likely part of a `FastMCPContext` child class or something.

Actually, if I look at the `FastMCP` class, it extends something?

No, I see `FastMCP` and then `FastMCPContext` at line 1052.
Line 1119 is inside `FastMCPContext`.

```python
    async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:
        """Read a resource by URI.

        Args:
            uri: Resource URI to read

        Returns:
            The resource content as either text or bytes
        """
        assert self._fastmcp is not None, "Context is not available outside of a request"
        return await self._fastmcp.read_resource(uri)
```

This just delegates to `self._fastmcp.read_resource(uri)`, which is the method at line 367.

So everything is consistent. I've fixed the resource decoding in the only place where templates are matched.

Final check on `git status --short`.

Okay, and line 1119 is likely part of a `FastMCPContext` child class or something. Actually, if I look at the `FastMCP` class, it extends something? No, I see `FastMCP` and then `FastMCPContext` at line 1052. Line 1119 is inside `FastMCPContext`. ```python async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]: """Read a resource by URI. Args: uri: Resource URI to read Returns: The resource content as either text or bytes """ assert self._fastmcp is not None, "Context is not available outside of a request" return await self._fastmcp.read_resource(uri) ``` This just delegates to `self._fastmcp.read_resource(uri)`, which is the method at line 367. So everything is consistent. I've fixed the resource decoding in the only place where templates are matched. Final check on `git status --short`. <channel|><|tool_call>call:run_command{command:<|"|>git status --short && git diff<|"|>}<tool_call|><|tool_response>
Generated tokens 33 tokens · stopped: STOP
The diff is clean. No untracked files. No changes in `tests/`.

I am confident.
The diff is clean. No untracked files. No changes in `tests/`. I am confident.<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..efaa596 100644
--- a/src/mcp/server/fastmcp/resources/templates.py
+++ b/src/mcp/server/fastmcp/resources/templates.py
@@ -6,6 +6,7 @@ import inspect
 import re
 from collections.abc import Callable
 from typing import TYPE_CHECKING, Any
+from urllib.parse import unquote
 
 from pydantic import BaseModel, Field, validate_call
 
@@ -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_complex_phrase 
tests/server/test_stateless_mode.py::test_create_message_fails_in_stateless_mode 
tests/issues/test_973_url_decoding.py::test_template_matches_preserves_plus_sign 
tests/server/test_stateless_mode.py::test_exception_has_method_attribute 
tests/server/test_stateless_mode.py::test_stateless_error_message_is_actionable 
tests/issues/test_973_url_decoding.py::test_template_matches_decodes_space 
tests/server/test_stateless_mode.py::test_list_roots_fails_in_stateless_mode 
tests/server/test_stateless_mode.py::test_elicit_deprecated_fails_in_stateless_mode 
tests/server/test_stateless_mode.py::test_stateful_mode_does_not_raise_stateless_error 
[gw12] [  8%] PASSED tests/server/test_stateless_mode.py::test_stateful_mode_does_not_raise_stateless_error 
tests/issues/test_973_url_decoding.py::test_template_matches_decodes_accented_characters 
tests/server/test_stateless_mode.py::test_elicit_form_fails_in_stateless_mode 
[gw6] [ 16%] PASSED tests/server/test_stateless_mode.py::test_elicit_form_fails_in_stateless_mode 
[gw9] [ 25%] PASSED tests/server/test_stateless_mode.py::test_elicit_deprecated_fails_in_stateless_mode 
[gw1] [ 33%] PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_accented_characters 
[gw0] [ 41%] PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_space 
tests/server/test_stateless_mode.py::test_elicit_url_fails_in_stateless_mode 
[gw7] [ 50%] PASSED tests/server/test_stateless_mode.py::test_elicit_url_fails_in_stateless_mode 
[gw10] [ 58%] PASSED tests/server/test_stateless_mode.py::test_exception_has_method_attribute 
[gw2] [ 66%] PASSED tests/server/test_stateless_mode.py::test_list_roots_fails_in_stateless_mode 
[gw5] [ 75%] PASSED tests/server/test_stateless_mode.py::test_create_message_fails_in_stateless_mode 
[gw11] [ 83%] PASSED tests/server/test_stateless_mode.py::test_stateless_error_message_is_actionable 
[gw3] [ 91%] PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_complex_phrase 
[gw4] [100%] PASSED tests/issues/test_973_url_decoding.py::test_template_matches_preserves_plus_sign 

═══════════════════════════════ 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-72eb8d919f4f47e4939677576fbdb558.xml --
=========================== short test summary info ============================
PASSED tests/server/test_stateless_mode.py::test_stateful_mode_does_not_raise_stateless_error
PASSED tests/server/test_stateless_mode.py::test_elicit_form_fails_in_stateless_mode
PASSED tests/server/test_stateless_mode.py::test_elicit_deprecated_fails_in_stateless_mode
PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_accented_characters
PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_space
PASSED tests/server/test_stateless_mode.py::test_elicit_url_fails_in_stateless_mode
PASSED tests/server/test_stateless_mode.py::test_exception_has_method_attribute
PASSED tests/server/test_stateless_mode.py::test_list_roots_fails_in_stateless_mode
PASSED tests/server/test_stateless_mode.py::test_create_message_fails_in_stateless_mode
PASSED tests/server/test_stateless_mode.py::test_stateless_error_message_is_actionable
PASSED tests/issues/test_973_url_decoding.py::test_template_matches_decodes_complex_phrase
PASSED tests/issues/test_973_url_decoding.py::test_template_matches_preserves_plus_sign
============================= 12 passed in 25.10s ==============================
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.100" timestamp="2026-11-14T03:41:06.303671+00:00" hostname="7d3ed7330333"><testcase classname="tests.issues.test_973_url_decoding" name="test_template_matches_decodes_complex_phrase" time="0.003" /><testcase classname="tests.server.test_stateless_mode" name="test_list_roots_fails_in_stateless_mode" time="0.002" /><testcase classname="tests.server.test_stateless_mode" name="test_elicit_deprecated_fails_in_stateless_mode" time="0.002" /><testcase classname="tests.server.test_stateless_mode" name="test_stateless_error_message_is_actionable" time="0.002" /><testcase classname="tests.issues.test_973_url_decoding" name="test_template_matches_preserves_plus_sign" time="0.003" /><testcase classname="tests.server.test_stateless_mode" name="test_create_message_fails_in_stateless_mode" time="0.004" /><testcase classname="tests.server.test_stateless_mode" name="test_stateful_mode_does_not_raise_stateless_error" time="0.002" /><testcase classname="tests.issues.test_973_url_decoding" name="test_template_matches_decodes_accented_characters" time="0.003" /><testcase classname="tests.server.test_stateless_mode" name="test_elicit_form_fails_in_stateless_mode" time="0.002" /><testcase classname="tests.issues.test_973_url_decoding" name="test_template_matches_decodes_space" time="0.002" /><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_exception_has_method_attribute" time="0.002" /></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.
  [  22.1s] 💭 (989 chars) "The user is reporting an issue where dynamic URL resource parameters in the MCP Python SDK 
are not being automatically U..."
  [  22.1s] 🔧 run_command grep -rn --include="*.py" "resource(" src/mcp | grep "def"
  [  22.1s]   → src/mcp/server/fastmcp/resources/templates.py:94:    async def create_resource(
  [  47.2s] 💭 (869 chars) "The example uses `@mcp.resource("products://search/{product_identifier}")`. This looks like 
a decorator.
`src/mcp/server..."
  [  47.2s] 📖 read_file (src/mcp/server/fastmcp/server.py)
  [  47.3s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/server.py', 'content': '\n    def 
add_resource(self, resource: Reso…
  [  53.0s] 💭 (127 chars) "The `resource` method is a decorator. I need to see the full implementation of `resource` 
and how it's stored and then i..."
  [  53.0s] 📖 read_file (src/mcp/server/fastmcp/server.py)
  [  53.0s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/server.py', 'content': '            
@server.resource("resource://my…
  [  62.0s] 💭 (290 chars) "It calls `self._resource_manager.add_template`. Let's find `_resource_manager`.

Looking at `src/mcp/server/fastmcp/reso..."
  [  62.0s] 📖 read_file (src/mcp/server/fastmcp/resources/resource_manager.py)
  [  62.0s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/resources/resource_manager.py', 'content': 
'"""Resource manager fun…
  [  72.1s] 💭 (658 chars) "In `ResourceManager.get_resource`:
```python
        # Then check templates
        for template in self._templates.valu..."
  [  72.1s] 📖 read_file (src/mcp/server/fastmcp/resources/templates.py)
  [  72.1s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/resources/templates.py', 'content': '"""Resource 
template functiona…
  [ 119.6s] 💭 (1037 chars) "The `matches` method is where the parameters are extracted:

```python
    def matches(self, uri: str) -> dict[str, Any]..."
  [ 119.6s] 🔧 run_command (39 lines) python3 - <<'PY'
  [ 120.6s]   → URI: products://search/stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc
  [ 141.0s] 💭 (1056 chars) "The reproduction confirms the bug. The result is `Searching for: 
stick%20correcteur%20teint%C3%A9%20anti-imperfections%2..."
  [ 141.0s] ✅ Agent finished: <|tool_call>call 
edit_file{filepath:<|"|>src/mcp/server/fastmcp/resources/templates.py<|"|>,new_string:<|"|>from __future__ import 
annotations

import inspect
import re
from collections.abc import Cal...
  [ 141.0s] 🎯 Task Prompt: Your previous response reached the token limit before the tool call finished closing 
(<|tool_call|> was cut off). Do NOT...
  [ 155.6s] ✏️ edit_file (src/mcp/server/fastmcp/resources/templates.py)
  [ 155.8s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/resources/templates.py', 'occurrences': 1, 
'strategy': 'unescaped',…
  [ 162.2s] 🔧 run_command (29 lines) python3 - <<'PY'
  [ 162.5s]   → URI: products://search/stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc
  [ 185.8s] 💭 (1028 chars) "Wait, I edited `src/mcp/server/fastmcp/resources/templates.py` and added `from urllib.parse
import unquote` in my previo..."
  [ 185.8s] 📖 read_file (src/mcp/server/fastmcp/resources/templates.py)
  [ 185.8s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/resources/templates.py', 'content': '"""Resource 
template functiona…
  [ 192.5s] 💭 (44 chars) "Yes, `unquote` is missing. I need to add it."
  [ 192.5s] ✏️ edit_file (src/mcp/server/fastmcp/resources/templates.py)
  [ 192.6s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/resources/templates.py', 'occurrences': 1, 
'strategy': 'exact', 'di…
  [ 203.1s] 💭 (44 chars) "Now let's run the reproduction script again."
  [ 203.1s] 🔧 run_command (31 lines) python3 - <<'PY'
  [ 203.5s]   → URI: products://search/stick%20correcteur%20teint%C3%A9%20anti-imperfections%20rechargeable%20au%20zinc
  [ 235.2s] 💭 (1871 chars) "The reproduction now passes.

Now I should check if there are other places where resource parameters are extracted and n..."
  [ 235.2s] 📖 read_file (src/mcp/server/lowlevel/server.py)
  [ 235.2s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/lowlevel/server.py', 'content': '"""\nMCP Server 
Module\n\nThis module prov…
  [ 238.1s] 💭 (69 chars) "Searching for `read_resource` in `src/mcp/server/lowlevel/server.py`."
  [ 238.1s] 🔧 run_command grep -n "read_resource" src/mcp/server/lowlevel/server.py
  [ 238.1s]   → 337:    def read_resource(self):
  [ 247.2s] 💭 (186 chars) "The `read_resource` in `Server` class of `lowlevel/server.py` seems to be a 
concentrator/manager of registered handlers...."
  [ 247.2s] 📖 read_file (src/mcp/server/lowlevel/server.py)
  [ 247.2s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/lowlevel/server.py', 'content': '                return 
types.ServerResult(…
  [ 271.1s] 💭 (1061 chars) "In `src/mcp/server/lowlevel/server.py`, `read_resource` is a decorator that registers a 
handler for `ReadResourceRequest..."
  [ 271.1s] 🔧 run_command grep -rn "template" src/mcp/server/fastmcp | grep "matches"
  [ 271.2s]   → src/mcp/server/fastmcp/resources/templates.py:87:        """Check if URI matches template and extract 
parameters."""
  [ 278.6s] 💭 (246 chars) "Only `ResourceTemplate` uses `.matches()`.

What about tools? Tools usually take named arguments from a JSON payload, no..."
  [ 278.6s] 🔧 run_command grep -rn "template" src/mcp/server/fastmcp/server.py
  [ 278.6s]   → 298:        self._mcp_server.list_resource_templates()(self.list_resource_templates)
  [ 288.3s] 💭 (139 chars) "It seems they are all related to resources.

I'll double check the `FastMCP.read_resource` implementation to ensure I di..."
  [ 288.3s] 📖 read_file (src/mcp/server/fastmcp/server.py)
  [ 288.3s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/server.py', 'content': '        """Report progress 
for the current …
  [ 296.7s] 💭 (307 chars) "Wait, there are two `read_resource` methods in `src/mcp/server/fastmcp/server.py`?

Line 367: `async def read_resource(s..."
  [ 296.7s] 📖 read_file (src/mcp/server/fastmcp/server.py)
  [ 296.8s]   ✓ {'status': 'ok', 'filepath': 'src/mcp/server/fastmcp/server.py', 'content': '                
icons=template.icons,\n    …
  [ 392.2s] 💭 (911 chars) "Okay, and line 1119 is likely part of a `FastMCPContext` child class or something.

Actually, if I look at the `FastMCP`..."
  [ 392.2s] 🔧 run_command git status --short && git diff
  [ 392.3s]   →  M src/mcp/server/fastmcp/resources/templates.py
  [ 394.9s] 💭 (79 chars) "The diff is clean. No untracked files. No changes in `tests/`.

I am confident."
  [ 394.9s] 🤖 submit_patch
  [ 395.0s]   → submit_patch: {"status": "ok", "patch_size": 849, "files_changed": 1}
  [ 401.5s] 💭 (51 chars) "The issue is fixed. I'll provide the final summary."
  [ 401.5s] ✅ Agent finished: The bug was caused by URI template parameters in `FastMCP` being extracted via regex but 
not URL-decoded, leading to encoded characters (e.g., `%20`) being passed to resource handlers. I fixed this by...
Native result row
{
  "agent_error": null,
  "duration_seconds": 438.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": 849,
  "phase1_seconds": 411.03,
  "repo": "modelcontextprotocol/python-sdk",
  "request_log_path": "/kaggle-root/working/r31/rollouts_X/requests/modelcontextprotocol__python-sdk-1864__p0__r4.jsonl",
  "resolved": true,
  "rollout_idx": 4,
  "run_key": "modelcontextprotocol__python-sdk-1864__p0__r4",
  "strict_clean_patch": true,
  "tool_calls": 19,
  "total_llm_calls": 22,
  "trace_path": "/kaggle-root/working/r31/rollouts_X/traces/trace_modelcontextprotocol__python-sdk-1864__p0__r4.json"
}