fix(server): preserve mounted tool task metadata (#3632)

* fix(server): preserve mounted tool task metadata

* fix(server): move task execution metadata to base tool

* cleanup: remove stale import, tighten execution metadata guard

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
This commit is contained in:
Miguel Miranda Dias 2026-03-27 14:15:33 +01:00 committed by GitHub
commit 59a126a0b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 41 additions and 21 deletions

View file

@ -104,6 +104,7 @@ class FastMCPProviderTool(Tool):
tags=tool.tags,
annotations=tool.annotations,
task_config=tool.task_config,
execution=tool.execution,
meta=tool.get_meta(),
title=tool.title,
icons=tool.icons,

View file

@ -190,7 +190,7 @@ class Tool(FastMCPComponent):
elif self.annotations and self.annotations.title:
title = self.annotations.title
return MCPTool(
mcp_tool = MCPTool(
name=overrides.get("name", self.name),
title=overrides.get("title", title),
description=overrides.get("description", self.description),
@ -204,6 +204,15 @@ class Tool(FastMCPComponent):
), # ty:ignore[unknown-argument]
)
if (
self.task_config.supports_tasks()
and "execution" not in overrides
and not self.execution
):
mcp_tool.execution = ToolExecution(taskSupport=self.task_config.mode)
return mcp_tool
@classmethod
def from_function(
cls,

View file

@ -18,9 +18,8 @@ from typing import (
)
import anyio
import mcp.types
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData, Icon, ToolAnnotations, ToolExecution
from mcp.types import ErrorData, Icon, ToolAnnotations
from pydantic import Field
from pydantic.json_schema import SkipJsonSchema
@ -91,24 +90,6 @@ class FunctionTool(Tool):
fn: SkipJsonSchema[Callable[..., Any]]
return_type: Annotated[SkipJsonSchema[Any], Field(exclude=True)] = None
def to_mcp_tool(
self,
**overrides: Any,
) -> mcp.types.Tool:
"""Convert the FastMCP tool to an MCP tool.
Extends the base implementation to add task execution mode if enabled.
"""
# Get base MCP tool from parent
mcp_tool = super().to_mcp_tool(**overrides)
# Add task execution mode per SEP-1686
# Only set execution if not overridden and task execution is supported
if self.task_config.supports_tasks() and "execution" not in overrides:
mcp_tool.execution = ToolExecution(taskSupport=self.task_config.mode)
return mcp_tool
@classmethod
def from_function(
cls,

View file

@ -473,6 +473,35 @@ class TestMountedTaskList:
assert child_task.task_id in task_ids
class TestMountedTaskMetadata:
"""Test task metadata exposure for mounted tools."""
async def test_mounted_tool_list_preserves_task_support_metadata(self):
"""Mounted tools should preserve execution.taskSupport in tools/list."""
child = FastMCP("child")
@child.tool(task=True)
async def foo() -> dict[str, bool]:
return {"ok": True}
parent = FastMCP("parent")
parent.mount(child)
child_tools = await child.list_tools()
parent_tools = await parent.list_tools()
child_tool = next(t for t in child_tools if t.name == "foo")
parent_tool = next(t for t in parent_tools if t.name == "foo")
child_mcp_tool = child_tool.to_mcp_tool(name=child_tool.name)
parent_mcp_tool = parent_tool.to_mcp_tool(name=parent_tool.name)
assert child_mcp_tool.execution is not None
assert parent_mcp_tool.execution is not None
assert child_mcp_tool.execution.taskSupport == "optional"
assert parent_mcp_tool.execution.taskSupport == "optional"
class TestMountedTaskConfigModes:
"""Test TaskConfig mode enforcement for mounted tools."""