From 59a126a0b60c2fa010042efbbca936dd0dcc5092 Mon Sep 17 00:00:00 2001 From: Miguel Miranda Dias <7780875+pandego@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:15:33 +0100 Subject: [PATCH] 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> --- .../server/providers/fastmcp_provider.py | 1 + src/fastmcp/tools/base.py | 11 ++++++- src/fastmcp/tools/function_tool.py | 21 +------------- tests/server/tasks/test_task_mount.py | 29 +++++++++++++++++++ 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/fastmcp/server/providers/fastmcp_provider.py b/src/fastmcp/server/providers/fastmcp_provider.py index 6606fb8e4..953daf2b8 100644 --- a/src/fastmcp/server/providers/fastmcp_provider.py +++ b/src/fastmcp/server/providers/fastmcp_provider.py @@ -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, diff --git a/src/fastmcp/tools/base.py b/src/fastmcp/tools/base.py index 821936f88..04c3b066e 100644 --- a/src/fastmcp/tools/base.py +++ b/src/fastmcp/tools/base.py @@ -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, diff --git a/src/fastmcp/tools/function_tool.py b/src/fastmcp/tools/function_tool.py index e74401b8e..c79747f2c 100644 --- a/src/fastmcp/tools/function_tool.py +++ b/src/fastmcp/tools/function_tool.py @@ -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, diff --git a/tests/server/tasks/test_task_mount.py b/tests/server/tasks/test_task_mount.py index d9a83ce2a..875612ada 100644 --- a/tests/server/tasks/test_task_mount.py +++ b/tests/server/tasks/test_task_mount.py @@ -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."""