Skip to content

Fix task execution for tools with custom names#2645

Merged
chrisguidry merged 2 commits intomainfrom
2642-task-custom-name
Dec 18, 2025
Merged

Fix task execution for tools with custom names#2645
chrisguidry merged 2 commits intomainfrom
2642-task-custom-name

Conversation

@chrisguidry
Copy link
Copy Markdown
Collaborator

When a tool was registered with a custom name different from its function
name, task execution would fail because Docket registered the function by
its __name__ but the handler looked it up by the tool's configured name.

This switches to using Docket's new names= parameter (pydocket 0.16.0)
to register functions with their proper lookup keys, and removes the
_create_named_fn_wrapper hack that was used for mounted servers.

Closes #2642

🤖 Generated with Claude Code

@marvin-context-protocol marvin-context-protocol Bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality. labels Dec 18, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 18, 2025

Walkthrough

This change removes the internal helper _create_named_fn_wrapper and replaces wrapper-based registration with explicit name-based registration when adding components to docket. Tools, prompts, resources, and templates are now registered using explicit names=[...] arguments (e.g., docket.register(tool.fn, names=[tool.key])) instead of relying on a wrapper's __name__. Provider-derived components continue to be registered but now use explicit names for disambiguation. No public API signatures were changed.

Possibly related PRs

  • PR 2575 (jlowin/fastmcp): Added a named-function wrapper and prefixing for mounted server registrations; touches the same helper and registration logic now removed.
  • PR 2619 (jlowin/fastmcp): Propagates tool name overrides into the registration and lookup paths in server.py, affecting the same docket registration flow.
  • PR 2574 (jlowin/fastmcp): Adds mounting/proxy logic that registers mounted server functions into a parent Docket, modifying the same registration mechanisms.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix task execution for tools with custom names' directly corresponds to the main change - fixing the bug where tools with custom names couldn't be executed because they were registered by function name instead of tool name.
Description check ✅ Passed The description provides a clear explanation of the problem, the solution using Docket's names= parameter, mentions closing issue #2642, and notes AI tool usage as requested by template.
Linked Issues check ✅ Passed The PR directly addresses issue #2642 by switching from function.name registration to explicit names= parameter in Docket, ensuring functions are registered under the tool's configured name.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the custom tool name registration issue - removing the _create_named_fn_wrapper helper and updating registration calls to use explicit names parameter.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 2642-task-custom-name

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 884db0c and a5064cc.

⛔ Files ignored due to path filters (3)
  • pyproject.toml is excluded by none and included by none
  • tests/server/tasks/test_server_tasks_parameter.py is excluded by none and included by none
  • uv.lock is excluded by !**/*.lock and included by none
📒 Files selected for processing (1)
  • src/fastmcp/server/server.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/fastmcp/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/server/server.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
🔇 Additional comments (2)
src/fastmcp/server/server.py (2)

427-427: LGTM! Tool registration uses correct lookup key.

The tool registration correctly uses tool.key which matches how tools are looked up during task execution.


435-438: LGTM! Prompt registration uses correct lookup key.

The prompt registration correctly uses prompt.key which matches how prompts are looked up during task execution.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ede8ff6 and 884db0c.

⛔ Files ignored due to path filters (3)
  • pyproject.toml is excluded by none and included by none
  • tests/server/tasks/test_server_tasks_parameter.py is excluded by none and included by none
  • uv.lock is excluded by !**/*.lock and included by none
📒 Files selected for processing (1)
  • src/fastmcp/server/server.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/fastmcp/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/server/server.py
🧬 Code graph analysis (1)
src/fastmcp/server/server.py (4)
src/fastmcp/resources/resource.py (2)
  • key (276-283)
  • FunctionResource (286-373)
src/fastmcp/resources/template.py (2)
  • key (221-228)
  • FunctionResourceTemplate (231-413)
src/fastmcp/utilities/components.py (1)
  • key (66-73)
src/fastmcp/server/providers/base.py (1)
  • get_tasks (245-286)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
🔇 Additional comments (2)
src/fastmcp/server/server.py (2)

422-427: LGTM - Tool registration uses correct lookup key.

The registration correctly uses tool.key which matches how tools are looked up during task execution.


429-438: LGTM - Prompt registration uses correct lookup key.

The registration correctly uses prompt.key and appropriately casts to async callable for task execution.

Comment on lines 440 to +445
for resource in self._resource_manager._resources.values():
if (
isinstance(resource, FunctionResource)
and resource.task_config.mode != "forbidden"
):
docket.register(resource.fn)
docket.register(resource.fn, names=[resource.name])
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Resource registration uses wrong lookup key.

Resources are registered with resource.name but looked up by URI during task execution (see line 659-661: handle_resource_as_task(self, str(uri), ...)). This recreates the original bug from #2642 where the registration key doesn't match the lookup key, causing task execution to fail.

Resources should be registered using resource.key, which returns the URI used for lookups.

🔎 Apply this diff to fix the registration key:
-                        docket.register(resource.fn, names=[resource.name])
+                        docket.register(resource.fn, names=[resource.key])
🤖 Prompt for AI Agents
In src/fastmcp/server/server.py around lines 440 to 445, resource registration
currently uses resource.name but task lookup uses the resource URI
(resource.key), causing mismatched keys and failed task execution; change the
docket registration to use the resource.key (the URI used for lookups) as the
registration name (e.g., pass names=[resource.key] or names=[str(resource.key)]
as appropriate) so registration and lookup keys match.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have tests (including some new ones) that validate that this is all good, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the Resource class definition to understand the relationship between name and key
echo "=== Resource class definition ==="
ast-grep --pattern $'class $_ Resource$$$:
  $$$
  @property
  def key(self) -> $_:
    $$$
  $$$
  @property
  def name(self) -> $_:
    $$$
'

# Also search for FunctionResource specifically
echo -e "\n=== FunctionResource class ==="
ast-grep --pattern $'class FunctionResource($$$):
  $$$'

# Check for key property implementation in resources
echo -e "\n=== Resource key property implementation ==="
rg -A 5 "def key\(self\)" --type=py -g "*resource*.py"

Length of output: 7191


🏁 Script executed:

#!/bin/bash
# Search for tests related to task execution with resources
echo "=== Tests for resource task execution ==="
rg -n "handle_resource_as_task|task.*resource|resource.*task" --type=py -g "*test*.py" -C 2

echo -e "\n=== Tests that register resources with docket ==="
rg -n "docket.*register.*resource" --type=py -g "*test*.py" -C 3

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Look for the handle_resource_as_task implementation to see how it looks up functions
echo "=== handle_resource_as_task implementation ==="
rg -n "def handle_resource_as_task" --type=py -A 20

# Check how docket lookup works
echo -e "\n=== Docket get/call usage in handlers ==="
rg -n "docket\.get\(|docket\.call\(" --type=py -g "*handler*.py" -C 3

Length of output: 2987


🏁 Script executed:

#!/bin/bash
# Find tests added/modified in this PR by looking at test files for task-related tests
echo "=== Recent test files with 'task' and 'custom.*name' ==="
fd -e py test | xargs rg -l "task.*custom.*name|custom.*name.*task" 

echo -e "\n=== Test files mentioning tool.key or resource.key ==="
rg -n "\.key" --type=py -g "*test*.py" | grep -E "(tool|resource|prompt)" | head -20

Length of output: 112


🏁 Script executed:

#!/bin/bash
# Find the complete Resource base class with key property
echo "=== Complete Resource class key property ==="
rg -B 3 -A 10 "@property\s+def key\(self\)" src/fastmcp/resources/resource.py

echo -e "\n=== How is uri used as the key for resources? ==="
rg -B 2 -A 2 "uri.*key|key.*uri" src/fastmcp/resources/resource.py | head -40

Length of output: 207


🏁 Script executed:

#!/bin/bash
# Find how docket.call is used in handle_resource_as_task
echo "=== Complete handle_resource_as_task with docket.call ==="
rg -A 50 "def handle_resource_as_task" src/fastmcp/server/tasks/handlers.py | head -70

echo -e "\n=== How docket.call is invoked ==="
rg -B 3 -A 5 "await docket\.call\(" src/fastmcp/server/tasks/handlers.py

Length of output: 1890


🏁 Script executed:

#!/bin/bash
# Search for tests that test tools with custom names and tasks
echo "=== Tests with custom name and task ==="
fd test_ -e py | xargs rg -l "custom.*name|name=" | head -10

echo -e "\n=== Check test_tasks.py for resource or tool tests ==="
fd -e py test | xargs rg -l "test_task" | head -5

Length of output: 138


🏁 Script executed:

#!/bin/bash
# Look for the actual implementation of how resources store their URI as key
echo "=== Resource model definition with uri field ==="
ast-grep --pattern $'class Resource($$$):
  $$$
  uri: $$$
  $$$'

echo -e "\n=== Search for uri field and key in Resource ==="
rg -B 5 -A 15 "class Resource\(" src/fastmcp/resources/resource.py | head -50

Length of output: 12076


🏁 Script executed:

#!/bin/bash
# Get the complete handle_resource_as_task function
echo "=== Complete handle_resource_as_task function ==="
sed -n '/^async def handle_resource_as_task/,/^async def\|^def\|^class /p' src/fastmcp/server/tasks/handlers.py | head -100

Length of output: 3731


🏁 Script executed:

#!/bin/bash
# Search for how docket dispatches/calls tasks - look for the actual invocation
echo "=== Search for docket enqueue/dispatch in handlers ==="
rg -n "docket\.(enqueue|dispatch|submit|call)" src/fastmcp/server/tasks/handlers.py -A 3

echo -e "\n=== Check how tool tasks are dispatched ==="
rg -n "def handle_tool_as_task" -A 40 src/fastmcp/server/tasks/handlers.py | head -50

Length of output: 1563


🏁 Script executed:

#!/bin/bash
# Check if there are any tests for this PR that test tasks with custom names
echo "=== Look for test files modified recently or with task tests ==="
fd test -e py -x grep -l "2642\|custom.*name.*task\|task.*custom.*name" {} \;

echo -e "\n=== Search for tests that actually invoke resources as tasks ==="
rg -n "task.*resource|ReadResourceRequest.*task" --type=py -g "*test*.py" -C 2 | head -30

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Check templates too - they might have the same issue
echo "=== Template registration in diff context ==="
rg -B 2 -A 2 "docket.register.*template" src/fastmcp/server/server.py

echo -e "\n=== Template key property ==="
rg -B 3 -A 10 "@property.*def key" src/fastmcp/resources/template.py

Length of output: 610


🏁 Script executed:

#!/bin/bash
# Search for ANY test files that mention tasks, tools, resources, or prompts with docket
echo "=== Find all test files related to tasks ==="
fd -e py test | xargs rg -l "docket|task.*tool|task.*resource|FunctionTool.*task|FunctionResource.*task" | head -10

echo -e "\n=== Check for tests with issue number 2642 ==="
rg -n "2642|custom.*name" --type=py -g "*test*" -C 3

Length of output: 663


🏁 Script executed:

#!/bin/bash
# Look at the actual tool handler to compare with resource handler
echo "=== Complete tool task handler for comparison ==="
sed -n '/^async def handle_tool_as_task/,/^async def\|^def\|^class /p' src/fastmcp/server/tasks/handlers.py | head -120

Length of output: 4017


🏁 Script executed:

#!/bin/bash
# Check if there's a test directory for this PR or recent tests
echo "=== Find test files by date or with task-related content ==="
fd -e py -t f . tests | xargs rg -l "task" | head -10

echo -e "\n=== Look for tool.key usage in tests ==="
rg -n "tool\.key|tool_name.*key" --type=py tests/ | head -20

Length of output: 562


🏁 Script executed:

#!/bin/bash
# Search for the specific scenario - tools with custom names
echo "=== Search for Tool.from_function or @tool with custom name in tests ==="
rg -n "Tool\.from_function|@.*tool.*\(.*name=" --type=py -g "*test*.py" -A 5 | head -40

echo -e "\n=== Check what names tools are registered with ==="
rg -n "tool\.key|tool_name|tool\.name" src/fastmcp/tools/tool.py | head -20

Length of output: 297


@chrisguidry You're correct! Looking at the actual implementation, both the registration and lookup use resource.name, so they match:

Registration (line 445):

docket.register(resource.fn, names=[resource.name])

Lookup in handler (line 318 in handlers.py):

await docket.add(resource.name, key=task_key)()

CodeRabbit's concern appears to be based on seeing that handle_resource_as_task() receives uri as a parameter, but the handler actually retrieves the resource object and then uses resource.name (not the URI) for the docket lookup. The comment in the handler even explicitly states this: "Use resource.name which matches what was registered".

This is consistent with how tools work - they use tool.key for both registration and lookup. Resources use resource.name for both operations.

However, I couldn't find specific tests that exercise task execution with resources (or resources with custom names different from their URI). If you've added tests in this PR that validate the registration/lookup flow works correctly with custom names, that would be great confirmation! Could you point me to those tests?


🧬 Code Graph Analysis Results
  • File: src/fastmcp/server/tasks/capabilities.py (lines 6-22)

    • Summary:

      • Description: Returns the SEP-1686 task capabilities map advertised to clients.
      • Parameters: none
      • Returns: dict describing supported task operations and sub-operations for tools, prompts, and resources.
      • Exceptions: none
      • Implementation details: Provides a baseline capabilities structure with:
        • tasks: list, cancel, and a nested requests map for tools, prompts, and resources.
    • Included content:
      def get_task_capabilities() -> dict[str, Any]:
      """Return the SEP-1686 task capabilities structure.

      This is the standard capabilities map advertised to clients,
      declaring support for list, cancel, and request operations.
      """
      return {
          "tasks": {
              "list": {},
              "cancel": {},
              "requests": {
                  "tools": {"call": {}},
                  "prompts": {"get": {}},
                  "resources": {"read": {}},
              },
          }
      }
      
  • File: src/fastmcp/server/middleware/middleware.py (lines 79-205)

    • Description: Core middleware base class and dispatch logic used by the server to process MCP requests. This defines how different MCP protocol actions (initialize, tools/call, resources/read, prompts/get, etc.) are routed through the middleware chain and into concrete handlers.

    • Included content:
      class Middleware:
      """Base class for FastMCP middleware with dispatching hooks."""

      async def __call__(
          self,
          context: MiddlewareContext[T],
          call_next: CallNext[T, Any],
      ) -> Any:
          """Main entry point that orchestrates the pipeline."""
          handler_chain = await self._dispatch_handler(
              context,
              call_next=call_next,
          )
          return await handler_chain(context)
      
      async def _dispatch_handler(
          self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
      ) -> CallNext[Any, Any]:
          """Builds a chain of handlers for a given message."""
          handler = call_next
      
          match context.method:
              case "initialize":
                  handler = partial(self.on_initialize, call_next=handler)
              case "tools/call":
                  handler = partial(self.on_call_tool, call_next=handler)
              case "resources/read":
                  handler = partial(self.on_read_resource, call_next=handler)
              case "prompts/get":
                  handler = partial(self.on_get_prompt, call_next=handler)
              case "tools/list":
                  handler = partial(self.on_list_tools, call_next=handler)
              case "resources/list":
                  handler = partial(self.on_list_resources, call_next=handler)
              case "resources/templates/list":
                  handler = partial(self.on_list_resource_templates, call_next=handler)
              case "prompts/list":
                  handler = partial(self.on_list_prompts, call_next=handler)
      
          match context.type:
              case "request":
                  handler = partial(self.on_request, call_next=handler)
              case "notification":
                  handler = partial(self.on_notification, call_next=handler)
      
          handler = partial(self.on_message, call_next=handler)
      
          return handler
      
      async def on_message(
          self,
          context: MiddlewareContext[Any],
          call_next: CallNext[Any, Any],
      ) -> Any:
          return await call_next(context)
      
      async def on_request(
          self,
          context: MiddlewareContext[mt.Request[Any, Any]],
          call_next: CallNext[mt.Request[Any, Any], Any],
      ) -> Any:
          return await call_next(context)
      
      async def on_notification(
          self,
          context: MiddlewareContext[mt.Notification[Any, Any]],
          call_next: CallNext[mt.Notification[Any, Any], Any],
      ) -> Any:
          return await call_next(context)
      
      async def on_initialize(
          self,
          context: MiddlewareContext[mt.InitializeRequest],
          call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None],
      ) -> mt.InitializeResult | None:
          return await call_next(context)
      
      async def on_call_tool(
          self,
          context: MiddlewareContext[mt.CallToolRequestParams],
          call_next: CallNext[mt.CallToolRequestParams, ToolResult],
      ) -> ToolResult:
          return await call_next(context)
      
      async def on_read_resource(
          self,
          context: MiddlewareContext[mt.ReadResourceRequestParams],
          call_next: CallNext[mt.ReadResourceRequestParams, Sequence[ResourceContent]],
      ) -> Sequence[ResourceContent]:
          return await call_next(context)
      
      async def on_get_prompt(
          self,
          context: MiddlewareContext[mt.GetPromptRequestParams],
          call_next: CallNext[mt.GetPromptRequestParams, PromptResult],
      ) -> PromptResult:
          return await call_next(context)
      
      async def on_list_tools(
          self,
          context: MiddlewareContext[mt.ListToolsRequest],
          call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
      ) -> Sequence[Tool]:
          return await call_next(context)
      
      async def on_list_resources(
          self,
          context: MiddlewareContext[mt.ListResourcesRequest],
          call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]],
      ) -> Sequence[Resource]:
          return await call_next(context)
      
      async def on_list_resource_templates(
          self,
          context: MiddlewareContext[mt.ListResourceTemplatesRequest],
          call_next: CallNext[
              mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]
          ],
      ) -> Sequence[ResourceTemplate]:
          return await call_next(context)
      
      async def on_list_prompts(
          self,
          context: MiddlewareContext[mt.ListPromptsRequest],
          call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]],
      ) -> Sequence[Prompt]:
          return await call_next(context)
      
  • File: src/fastmcp/server/providers/base.py (lines 72-316)

    • Summary:

      • Description: Base Provider class that defines the default interfaces for dynamic component providers (tools, resources, resource templates, prompts) and their lifecycle. It includes methods like list_tools, get_tool, read_resource, and similar for resources/templates/prompts, plus lifecyle lifespan().
    • Reason this is relevant:

      • Tests may exercise mounting/importing providers, or expect default provider behavior. This abstract base helps reviewers understand how providers should behave, and how errors propagate through providers.
    • Note: This is a long snippet; summarized here. If you want, I can include a condensed excerpt focusing on key methods (list_tools, get_tool, list_resources, read_resource, render_prompt, and lifespan) upon request.


🧠 Learnings used
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T03:06:14.522Z
Learning: Each feature needs corresponding tests

Comment on lines 447 to +452
for template in self._resource_manager._templates.values():
if (
isinstance(template, FunctionResourceTemplate)
and template.task_config.mode != "forbidden"
):
docket.register(template.fn)
docket.register(template.fn, names=[template.name])
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Template registration uses wrong lookup key.

Templates are registered with template.name but looked up by URI template during task execution. This recreates the original bug from #2642.

Templates should be registered using template.key, which returns the URI template used for lookups.

🔎 Apply this diff to fix the registration key:
-                        docket.register(template.fn, names=[template.name])
+                        docket.register(template.fn, names=[template.key])
🤖 Prompt for AI Agents
In src/fastmcp/server/server.py around lines 447 to 452, the loop registers
templates using template.name but task execution looks up templates by their URI
template key; change the registration to use template.key (the URI template) as
the lookup name — e.g., register the function with names set to template.key
instead of template.name so runtime lookups resolve correctly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have tests (including some new ones) that validate that this is all good, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find test files related to templates and tasks
echo "=== Finding test files for templates and tasks ==="
fd -e py test | xargs rg -l "template.*task|task.*template" --type py

echo -e "\n=== Checking for template-related tests ==="
fd -e py test | xargs rg -l "ResourceTemplate|FunctionResourceTemplate" --type py

echo -e "\n=== Looking for new test files in PR (git diff) ==="
git diff --name-only origin/main...HEAD | grep test

echo -e "\n=== Checking what template.key returns ==="
rg "def key" --type py -A 5 | rg -A 5 "ResourceTemplate"

echo -e "\n=== Checking how templates are looked up in task handlers ==="
rg "get_resource.*template|template.*task" src/fastmcp/server/tasks/ --type py -C 3

Length of output: 305


🏁 Script executed:

#!/bin/bash
# Check the new test file added in this PR
echo "=== Contents of new test file ==="
cat tests/server/tasks/test_server_tasks_parameter.py

echo -e "\n=== Check template.key property definition ==="
rg "class.*ResourceTemplate" src/fastmcp/ --type py -A 30 | rg -B 5 -A 10 "@property\s+def key"

echo -e "\n=== Check how templates are looked up in handlers ==="
cat src/fastmcp/server/tasks/handlers.py | grep -A 20 "handle_resource_as_task"

echo -e "\n=== Check git diff for changes to task handlers ==="
git diff origin/main...HEAD -- src/fastmcp/server/tasks/handlers.py

Length of output: 962


🏁 Script executed:

#!/bin/bash
# Check ResourceTemplate key property
echo "=== ResourceTemplate key property ==="
rg -A 15 "class ResourceTemplate" src/fastmcp/resources/template.py | head -40

echo -e "\n=== Check FastMCPComponent key property (base class) ==="
rg -A 10 "@property.*def key" src/fastmcp/utilities/components.py

echo -e "\n=== Check how resources/templates are looked up in server.py ==="
rg "_get_resource_or_template_or_none" src/fastmcp/server/server.py -A 30

echo -e "\n=== Check task handler resource lookup ==="
rg "handle_resource_as_task" src/fastmcp/server/tasks/handlers.py -B 5 -A 40

echo -e "\n=== List all test files for tasks ==="
fd test_.*task tests/

Length of output: 5027


🏁 Script executed:

#!/bin/bash
# Find FastMCPComponent definition and key property
echo "=== FastMCPComponent class definition ==="
rg "class FastMCPComponent" src/fastmcp/utilities/components.py -A 50

echo -e "\n=== Check how Docket lookup happens in task handler ==="
cat src/fastmcp/server/tasks/handlers.py | rg -A 50 "async def handle_resource_as_task"

echo -e "\n=== Check if template.name vs template.key are different ==="
rg "uri_template" src/fastmcp/resources/template.py -B 5 -A 15

echo -e "\n=== Find test directory structure ==="
find . -type d -name "*test*" | head -20

Length of output: 13588


🏁 Script executed:

#!/bin/bash
# Check how Docket queue and lookup works in the task handler
echo "=== Full handle_resource_as_task function ==="
rg "async def handle_resource_as_task" src/fastmcp/server/tasks/handlers.py -A 80

echo -e "\n=== Check how docket.queue is called ==="
rg "docket.queue|await docket" src/fastmcp/server/tasks/handlers.py -B 3 -A 3

echo -e "\n=== Check template key usage in registration ==="
rg "template.key|template.name" src/fastmcp/server/server.py -C 3 | head -50

Length of output: 6196


🏁 Script executed:

#!/bin/bash
# Check for tests that specifically test template task execution
echo "=== Search for test files in the repo ==="
find . -path ./node_modules -prune -o -type f -name "test*.py" -o -name "*test.py" | grep -v node_modules | head -20

echo -e "\n=== Search for tests related to tasks with custom names ==="
rg "custom.*name.*task|task.*custom.*name" --type py -l

echo -e "\n=== Look for tests added in this PR ==="
git diff --stat origin/main...HEAD | grep test

echo -e "\n=== Check git log for test files ==="
git log --oneline --name-only origin/main..HEAD | grep test | head -20

echo -e "\n=== Search for template + task tests ==="
rg "FunctionResourceTemplate.*task|template.*task.*test" --type py -C 2

Length of output: 1159


🏁 Script executed:

#!/bin/bash
# Get the content of the new test file from git
echo "=== Content of new test file from git ==="
git show HEAD:tests/server/tasks/test_server_tasks_parameter.py

echo -e "\n=== Check resource.name usage in task handler ==="
rg "resource.name" src/fastmcp/server/tasks/handlers.py -B 5 -A 5

echo -e "\n=== Check what happens when docket.add is called ==="
rg "class.*Docket|def add" --type py -A 10 | head -50

Length of output: 18199


@chrisguidry Yes, you're absolutely right! The new test file tests/server/tasks/test_server_tasks_parameter.py includes comprehensive coverage, including a specific test test_task_with_custom_template_name() that validates exactly this scenario.

The test:

  1. Creates a template with a custom name: @mcp.resource("test://{item_id}", name="custom-template-name")
  2. Verifies it's registered in Docket: assert "custom-template-name" in docket.tasks
  3. Successfully executes it as a task and validates the result

The code is correct as-is. Templates are registered and looked up using template.name (not template.key):

  • Registration: docket.register(template.fn, names=[template.name])
  • Task execution lookup: await docket.add(resource.name, key=task_key)(**params)

The template.key property returns the URI template (like "test://{item_id}") which is used for client-facing identification, while template.name is the internal function identifier used for Docket registration and lookup. Your tests confirm this works correctly! 🎯


🧬 Code Graph Analysis Results

[possibly_relevant_code_snippets]

src/fastmcp/server/server.py

  • Snippet not directly applicable to the given file context.

examples/testing_demo/tests/test_server.py

  • Relevance: Shows a client fixture pattern used in tests that interact with the MCP server, which is relevant to understanding test coverage and how tests instantiate and exercise the server.

Snippet (complete):

async def client():
"""
Client fixture for testing.

Uses async context manager and yields client synchronously.
No @pytest.mark.asyncio needed - pytest-asyncio mode = "auto" handles it.
"""
# Import here to avoid import-time side effects
from server import mcp

async with Client(mcp) as client:
    yield client

[end of snippet]


🧠 Learnings used
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T03:06:14.522Z
Learning: Each feature needs corresponding tests

Comment on lines 458 to +468
for tool in tasks.tools:
named_fn = _create_named_fn_wrapper(tool.fn, tool.key)
docket.register(named_fn)
docket.register(tool.fn, names=[tool.key])
for resource in tasks.resources:
named_fn = _create_named_fn_wrapper(
resource.fn, resource.name
)
docket.register(named_fn)
docket.register(resource.fn, names=[resource.name])
for template in tasks.templates:
named_fn = _create_named_fn_wrapper(
template.fn, template.name
)
docket.register(named_fn)
docket.register(template.fn, names=[template.name])
for prompt in tasks.prompts:
named_fn = _create_named_fn_wrapper(prompt.fn, prompt.key)
docket.register(named_fn)
docket.register(
cast(Callable[..., Awaitable[Any]], prompt.fn),
names=[prompt.key],
)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Provider resource and template registrations use wrong lookup keys.

While tools and prompts correctly use .key, provider resources (line 461) and templates (line 463) incorrectly use .name. This recreates the original bug from #2642 for provider components.

🔎 Apply this diff to fix both registration keys:
                         for tool in tasks.tools:
                             docket.register(tool.fn, names=[tool.key])
                         for resource in tasks.resources:
-                            docket.register(resource.fn, names=[resource.name])
+                            docket.register(resource.fn, names=[resource.key])
                         for template in tasks.templates:
-                            docket.register(template.fn, names=[template.name])
+                            docket.register(template.fn, names=[template.key])
                         for prompt in tasks.prompts:
                             docket.register(
                                 cast(Callable[..., Awaitable[Any]], prompt.fn),
                                 names=[prompt.key],
                             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for tool in tasks.tools:
named_fn = _create_named_fn_wrapper(tool.fn, tool.key)
docket.register(named_fn)
docket.register(tool.fn, names=[tool.key])
for resource in tasks.resources:
named_fn = _create_named_fn_wrapper(
resource.fn, resource.name
)
docket.register(named_fn)
docket.register(resource.fn, names=[resource.name])
for template in tasks.templates:
named_fn = _create_named_fn_wrapper(
template.fn, template.name
)
docket.register(named_fn)
docket.register(template.fn, names=[template.name])
for prompt in tasks.prompts:
named_fn = _create_named_fn_wrapper(prompt.fn, prompt.key)
docket.register(named_fn)
docket.register(
cast(Callable[..., Awaitable[Any]], prompt.fn),
names=[prompt.key],
)
for tool in tasks.tools:
docket.register(tool.fn, names=[tool.key])
for resource in tasks.resources:
docket.register(resource.fn, names=[resource.key])
for template in tasks.templates:
docket.register(template.fn, names=[template.key])
for prompt in tasks.prompts:
docket.register(
cast(Callable[..., Awaitable[Any]], prompt.fn),
names=[prompt.key],
)
🤖 Prompt for AI Agents
In src/fastmcp/server/server.py around lines 458 to 468, provider resource and
template registrations are using .name instead of .key which recreates bug
#2642; update the docket.register calls so resources use names=[resource.key]
and templates use names=[template.key] (preserve the existing function
references and any casting for prompts/templates) to match how tools and prompts
use .key and ensure consistent lookup keys for provider components.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have tests (including some new ones) that validate that this is all good, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for tests related to provider tasks and Docket registration
rg -n "provider.*task" --type=py -g "test_*.py" -g "*_test.py" -C2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for tests specifically about resource/template registration with names
rg -n "docket\.register.*names" --type=py -g "test_*.py" -g "*_test.py" -C3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for tests related to issue #2642 or custom names
rg -n "2642|custom.*name" --type=py -g "test_*.py" -g "*_test.py" -C2

Length of output: 476


🏁 Script executed:

#!/bin/bash
# Check the Resource and ResourceTemplate class definitions to understand .name vs .key
ast-grep --pattern 'class Resource($$$):
  $$$
'

Length of output: 11266


🏁 Script executed:

#!/bin/bash
# Search for how resources define .name and .key properties
rg -n "class.*Resource.*:" --type=py src/fastmcp/resources/ -A 30 | head -100

Length of output: 6660


🏁 Script executed:

#!/bin/bash
# Look at how local resources/templates are registered (lines around 445, 451)
cat src/fastmcp/server/server.py | sed -n '440,455p'

Length of output: 769


🏁 Script executed:

#!/bin/bash
# Check how resources are looked up in task handlers
cat src/fastmcp/server/tasks/handlers.py | head -150

Length of output: 5082


🏁 Script executed:

#!/bin/bash
# Search for how resources are indexed - look for resource.key vs resource.name usage
rg -n "resource\.(key|name)" --type=py src/fastmcp/server/ -C2

Length of output: 5941


🏁 Script executed:

#!/bin/bash
# Look for tests added in this PR that cover the fix
git diff main --name-only --diff-filter=A | grep test

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there are any tests for task execution with custom names
rg -n "test.*task.*custom|test.*custom.*task" --type=py -g "test_*.py" -g "*_test.py"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at how tools are indexed vs resources - check the managers
rg -n "def get_tool|def get_resource" --type=py src/fastmcp/ -A 10

Length of output: 19501


🏁 Script executed:

#!/bin/bash
# Check what tests exist in tests/ directory related to tasks
fd -e py . tests/ | xargs rg -l "task" | head -20

Length of output: 768


🏁 Script executed:

#!/bin/bash
# Look for tests specifically about Docket registration or task handlers
rg -n "test.*docket|test.*handler" --type=py -g "test_*.py" -g "*_test.py" -C2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check what changes were made to test files in this PR
git diff main --stat | grep test

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Look for any tests that verify resource/template task execution
rg -n "FunctionResource|FunctionResourceTemplate.*task" --type=py tests/ -C3

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check for tests that register resources with Docket
rg -n "register.*resource" --type=py tests/ -C2

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Look at the new test file added in this PR
cat tests/server/tasks/test_server_tasks_parameter.py

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Check if there are existing tests for resources/templates with tasks
fd "test_.*\.py$" . | xargs rg -l "resource.*task|template.*task" | head -10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at what the PR actually changes - get the full diff for context
git diff main tests/server/tasks/test_server_tasks_parameter.py | head -100

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Check for any comments in the code explaining why .name is used for resources
rg -n "resource\.name.*register|Use resource\.name" --type=py src/fastmcp/

Length of output: 131


@chrisguidry Looking at the code, I think the current implementation is actually correct and intentional. Here's what I found:

The pattern is consistent across the codebase:

  • Local resources use .name for Docket registration (line 445)
  • Local templates use .name for Docket registration (line 451)
  • Provider resources use .name for Docket registration (line 461)
  • Provider templates use .name for Docket registration (line 463)
  • The task handler explicitly looks up by .name (lines 320, 325)

There's even an explicit comment in handlers.py line 313 documenting this:

# Use resource.name which matches what was registered - prefixed for mounted resources

The distinction is:

  • Tools and prompts use .key for Docket registration
  • Resources and templates use .name for Docket registration

This appears to be an intentional design choice, not a bug. If the review comment were correct, then both local and provider registrations would be wrong, not just provider.

However, I couldn't find specific integration tests that verify provider resource/template task execution works correctly. While the pattern looks intentional and consistent, having tests that cover:

  1. Provider resources registered for task execution
  2. Provider templates registered for task execution
  3. Mounted servers with prefixed resource/template tasks

would provide additional confidence. Did you add such tests in the new test_server_tasks_parameter.py file?


🧬 Code Graph Analysis Results

File: examples/testing_demo/tests/test_server.py
Lines: 14-25

async def client():
    """
    Client fixture for testing.

    Uses async context manager and yields client synchronously.
    No @pytest.mark.asyncio needed - asyncio_mode = "auto" handles it.
    """
    # Import here to avoid import-time side effects
    from server import mcp

    async with Client(mcp) as client:
        yield client

File: src/fastmcp/server/tasks/capabilities.py
Lines: 6-22
"""
def get_task_capabilities() -> dict[str, Any]:
"""Return the SEP-1686 task capabilities structure.

This is the standard capabilities map advertised to clients,
declaring support for list, cancel, and request operations.
"""
return {
    "tasks": {
        "list": {},
        "cancel": {},
        "requests": {
            "tools": {"call": {}},
            "prompts": {"get": {}},
            "resources": {"read": {}},
        },
    }
}

"""


🧠 Learnings used
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T03:06:14.522Z
Learning: Each feature needs corresponding tests

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: One test job failed on Python 3.10 (ubuntu-latest), while all other Python versions (3.10 on Windows, 3.13 on Ubuntu) and test suites passed successfully.

Root Cause: The PR correctly uses pydocket 0.16.0's new names= parameter to fix task execution for tools with custom names (issue #2642). However, CodeRabbit's review identified registration key mismatches for resources and templates - they're using .name instead of .key for Docket registration.

The Issue:

  • Tools and Prompts: ✅ Correctly use .key for registration
  • Resources and Templates: ❌ Incorrectly use .name for registration

This matters because:

  • For resources: .key returns the URI (test://resource), while .name returns the function name (my_resource)
  • For templates: .key returns the URI template, while .name returns the function name
  • Task execution looks up components by their .key, not their .name

Suggested Solution:

Change the following lines in src/fastmcp/server/server.py:

# Line 445 - Resources (local)
docket.register(resource.fn, names=[resource.key])  # was: resource.name

# Line 452 - Templates (local)
docket.register(template.fn, names=[template.key])  # was: template.name

# Line 461 - Resources (from providers)
docket.register(resource.fn, names=[resource.key])  # was: resource.name

# Line 463 - Templates (from providers)
docket.register(template.fn, names=[template.key])  # was: template.name
Detailed Analysis

Investigation

I checked out the PR branch and ran tests locally on Python 3.12 - all tests in test_server_tasks_parameter.py pass, including the new test_task_with_custom_tool_name test.

The new test correctly verifies that tools with custom names work as tasks:

mcp.tool(my_function, name="custom-tool-name")
# Docket correctly has "custom-tool-name" in tasks
# Task execution works properly

However, the CodeRabbit review correctly identified that resources and templates don't follow the same pattern as tools and prompts.

Why This Matters

From src/fastmcp/resources/resource.py:283:

def key(self) -> str:
    return self._key or str(self.uri)

From src/fastmcp/resources/template.py:228:

def key(self) -> str:
    return self._key or self.uri_template

The .key property returns the URI (for resources) or URI template (for templates), which is what clients use to reference them. Using .name (which returns the function name) creates a mismatch between registration and lookup, exactly like the original bug in #2642.

Related Files
  • src/fastmcp/server/server.py:445 - Resource registration (local)
  • src/fastmcp/server/server.py:452 - Template registration (local)
  • src/fastmcp/server/server.py:461 - Resource registration (providers)
  • src/fastmcp/server/server.py:463 - Template registration (providers)
  • src/fastmcp/resources/resource.py:276-283 - Resource.key property definition
  • src/fastmcp/resources/template.py:221-228 - ResourceTemplate.key property definition

Note: I couldn't access the full CI logs to determine why specifically the Python 3.10 Ubuntu test failed while other platforms passed, but this registration mismatch is a definite bug that should be fixed regardless.

chrisguidry and others added 2 commits December 18, 2025 11:36
When a tool was registered with a custom name different from its function
name, task execution would fail because Docket registered the function by
its `__name__` but the handler looked it up by the tool's configured name.

This switches to using Docket's new `names=` parameter (pydocket 0.16.0)
to register functions with their proper lookup keys, and removes the
`_create_named_fn_wrapper` hack that was used for mounted servers.

Closes #2642

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@chrisguidry chrisguidry force-pushed the 2642-task-custom-name branch from 884db0c to a5064cc Compare December 18, 2025 16:37
@chrisguidry chrisguidry merged commit 8d758ba into main Dec 18, 2025
23 checks passed
@chrisguidry chrisguidry deleted the 2642-task-custom-name branch December 18, 2025 16:45
@jlowin jlowin added this to the 2.15 milestone Dec 18, 2025
jlowin added a commit that referenced this pull request Dec 23, 2025
Adapts PR #2645 for 2.x branch structure.

- Bumps pydocket to >=0.16.0 for names= parameter support
- Uses names= parameter for all component registration (tools, prompts, resources, templates)
- Removes _create_named_fn_wrapper function (no longer needed)
- Uses .key for resources and templates (fixes lookup issues)
- Adds test for custom tool names

Closes #2642
jlowin added a commit that referenced this pull request Dec 23, 2025
Uses pydocket 0.16.0's `names=` parameter for explicit registration keys:
- Tools/prompts: registered by `.key`
- Resources/templates: registered by `.name`

Removes `_create_named_fn_wrapper` function.

Closes #2642
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Task tool calls for tools with custom name fails without any error details

2 participants