diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index 2106b0f74..4b6ce6026 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -133,6 +133,10 @@ class Tool(FastMCPComponent): ToolAnnotations | None, Field(description="Additional annotations about the tool"), ] = None + execution: Annotated[ + ToolExecution | None, + Field(description="Task execution configuration (SEP-1686)"), + ] = None serializer: Annotated[ ToolResultSerializerType | None, Field(description="Optional custom serializer for tool results"), @@ -182,7 +186,7 @@ class Tool(FastMCPComponent): outputSchema=overrides.get("outputSchema", self.output_schema), icons=overrides.get("icons", self.icons), annotations=overrides.get("annotations", self.annotations), - execution=overrides.get("execution"), + execution=overrides.get("execution", self.execution), _meta=overrides.get( "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta) ), diff --git a/tests/tools/test_tool.py b/tests/tools/test_tool.py index 180f1bdc2..1f21ffc77 100644 --- a/tests/tools/test_tool.py +++ b/tests/tools/test_tool.py @@ -12,6 +12,7 @@ from mcp.types import ( ResourceLink, TextContent, TextResourceContents, + ToolExecution, ) from pydantic import AnyUrl, BaseModel, Field, TypeAdapter from typing_extensions import TypedDict @@ -1813,3 +1814,84 @@ class TestToolNameValidation: assert tool.parameters is not None assert "a" in tool.parameters["properties"] assert "b" in tool.parameters["properties"] + + +class TestToolExecutionField: + """Tests for the execution field on the base Tool class.""" + + def test_tool_with_execution_field(self): + """Test that Tool can store and return execution metadata.""" + tool = Tool( + name="my_tool", + description="A tool with execution", + parameters={"type": "object", "properties": {}}, + execution=ToolExecution(taskSupport="optional"), + ) + + mcp_tool = tool.to_mcp_tool() + assert mcp_tool.execution is not None + assert mcp_tool.execution.taskSupport == "optional" + + def test_tool_without_execution_field(self): + """Test that Tool without execution returns None.""" + tool = Tool( + name="my_tool", + description="A tool without execution", + parameters={"type": "object", "properties": {}}, + ) + + mcp_tool = tool.to_mcp_tool() + assert mcp_tool.execution is None + + def test_execution_override_takes_precedence(self): + """Test that explicit override takes precedence over field value.""" + tool = Tool( + name="my_tool", + description="A tool", + parameters={"type": "object", "properties": {}}, + execution=ToolExecution(taskSupport="optional"), + ) + + override_execution = ToolExecution(taskSupport="required") + mcp_tool = tool.to_mcp_tool(execution=override_execution) + assert mcp_tool.execution is not None + assert mcp_tool.execution.taskSupport == "required" + + async def test_function_tool_task_config_still_works(self): + """FunctionTool should still derive execution from task_config.""" + + async def my_fn() -> str: + return "hello" + + tool = Tool.from_function(my_fn, task=True) + mcp_tool = tool.to_mcp_tool() + + # FunctionTool sets execution from task_config + assert mcp_tool.execution is not None + assert mcp_tool.execution.taskSupport == "optional" + + def test_tool_execution_required_mode(self): + """Test that Tool can store required execution mode.""" + tool = Tool( + name="my_tool", + description="A tool with required execution", + parameters={"type": "object", "properties": {}}, + execution=ToolExecution(taskSupport="required"), + ) + + mcp_tool = tool.to_mcp_tool() + assert mcp_tool.execution is not None + assert mcp_tool.execution.taskSupport == "required" + + def test_tool_execution_forbidden_mode(self): + """Test that Tool can store forbidden execution mode.""" + tool = Tool( + name="my_tool", + description="A tool with forbidden execution", + parameters={"type": "object", "properties": {}}, + execution=ToolExecution(taskSupport="forbidden"), + ) + + mcp_tool = tool.to_mcp_tool() + assert mcp_tool.execution is not None + assert mcp_tool.execution.taskSupport == "forbidden"