Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies = [
"mcp>=1.24.0",
"openapi-pydantic>=0.5.1",
"platformdirs>=4.0.0",
"pydocket>=0.15.5",
"pydocket>=0.16.0",
"rich>=13.9.4",
"cyclopts>=4.0.0",
"authlib>=1.6.5",
Expand Down
47 changes: 14 additions & 33 deletions src/fastmcp/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,23 +112,6 @@
logger = get_logger(__name__)


def _create_named_fn_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
"""Create a wrapper function with a custom __name__ for Docket registration.

Docket uses fn.__name__ as the key for function registration and lookup.
When mounting servers, we need unique names to avoid collisions between
mounted servers that have identically-named functions.
"""
import functools

@functools.wraps(fn)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
return await fn(*args, **kwargs)

wrapper.__name__ = name
return wrapper


DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
Transport = Literal["stdio", "http", "sse", "streamable-http"]

Expand Down Expand Up @@ -441,50 +424,48 @@ async def _docket_lifespan(self) -> AsyncIterator[None]:
isinstance(tool, FunctionTool)
and tool.task_config.mode != "forbidden"
):
docket.register(tool.fn)
docket.register(tool.fn, names=[tool.key])

for prompt in self._prompt_manager._prompts.values():
if (
isinstance(prompt, FunctionPrompt)
and prompt.task_config.mode != "forbidden"
):
# task execution requires async fn (validated at creation time)
docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
docket.register(
cast(Callable[..., Awaitable[Any]], prompt.fn),
names=[prompt.key],
)

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])
Comment on lines 440 to +445
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


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])
Comment on lines 447 to +452
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


# Register provider components
for provider in self._providers:
try:
tasks = await provider.get_tasks()
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],
)
Comment on lines 458 to +468
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

except Exception as e:
provider_name = getattr(
provider, "server", provider
Expand Down
76 changes: 76 additions & 0 deletions tests/server/tasks/test_server_tasks_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,79 @@ async def shared_name_prompt() -> str:
# Prompt inheriting False (mode="forbidden") raises McpError
with pytest.raises(McpError):
await client.get_prompt("shared_name_prompt", task=True)


async def test_task_with_custom_tool_name():
"""Tools with custom names work correctly as tasks (issue #2642).

When a tool is registered with a custom name different from the function
name, task execution should use the custom name for Docket lookup.
"""
mcp = FastMCP("test", tasks=True)

async def my_function() -> str:
return "result from custom-named tool"

mcp.tool(my_function, name="custom-tool-name")

async with Client(mcp) as client:
# Verify the tool is registered with its custom name in Docket
docket = mcp.docket
assert docket is not None
assert "custom-tool-name" in docket.tasks

# Call the tool as a task using its custom name
task = await client.call_tool("custom-tool-name", task=True)
assert not task.returned_immediately
result = await task
assert result.data == "result from custom-named tool"


async def test_task_with_custom_resource_name():
"""Resources with custom names work correctly as tasks.

When a resource is registered with a custom name different from the function
name, task execution should use the custom name for Docket lookup.
"""
mcp = FastMCP("test", tasks=True)

@mcp.resource("test://resource", name="custom-resource-name")
async def my_resource_func() -> str:
return "result from custom-named resource"

async with Client(mcp) as client:
# Verify the resource is registered with its custom name in Docket
docket = mcp.docket
assert docket is not None
assert "custom-resource-name" in docket.tasks

# Call the resource as a task
task = await client.read_resource("test://resource", task=True)
assert not task.returned_immediately
result = await task.result()
assert result[0].text == "result from custom-named resource"


async def test_task_with_custom_template_name():
"""Resource templates with custom names work correctly as tasks.

When a template is registered with a custom name different from the function
name, task execution should use the custom name for Docket lookup.
"""
mcp = FastMCP("test", tasks=True)

@mcp.resource("test://{item_id}", name="custom-template-name")
async def my_template_func(item_id: str) -> str:
return f"result for {item_id}"

async with Client(mcp) as client:
# Verify the template is registered with its custom name in Docket
docket = mcp.docket
assert docket is not None
assert "custom-template-name" in docket.tasks

# Call the template as a task
task = await client.read_resource("test://123", task=True)
assert not task.returned_immediately
result = await task.result()
assert result[0].text == "result for 123"
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.