Skip to content

Commit 515e8e8

Browse files
authored
Merge pull request #2576 from jlowin/tool-execution-field
Add execution field to base Tool class
2 parents 659ec38 + 17520ec commit 515e8e8

File tree

2 files changed

+87
-1
lines changed

2 files changed

+87
-1
lines changed

src/fastmcp/tools/tool.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ class Tool(FastMCPComponent):
133133
ToolAnnotations | None,
134134
Field(description="Additional annotations about the tool"),
135135
] = None
136+
execution: Annotated[
137+
ToolExecution | None,
138+
Field(description="Task execution configuration (SEP-1686)"),
139+
] = None
136140
serializer: Annotated[
137141
ToolResultSerializerType | None,
138142
Field(description="Optional custom serializer for tool results"),
@@ -182,7 +186,7 @@ def to_mcp_tool(
182186
outputSchema=overrides.get("outputSchema", self.output_schema),
183187
icons=overrides.get("icons", self.icons),
184188
annotations=overrides.get("annotations", self.annotations),
185-
execution=overrides.get("execution"),
189+
execution=overrides.get("execution", self.execution),
186190
_meta=overrides.get(
187191
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
188192
),

tests/tools/test_tool.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
ResourceLink,
1313
TextContent,
1414
TextResourceContents,
15+
ToolExecution,
1516
)
1617
from pydantic import AnyUrl, BaseModel, Field, TypeAdapter
1718
from typing_extensions import TypedDict
@@ -1813,3 +1814,84 @@ def add(a: int, b: int) -> int:
18131814
assert tool.parameters is not None
18141815
assert "a" in tool.parameters["properties"]
18151816
assert "b" in tool.parameters["properties"]
1817+
1818+
1819+
class TestToolExecutionField:
1820+
"""Tests for the execution field on the base Tool class."""
1821+
1822+
def test_tool_with_execution_field(self):
1823+
"""Test that Tool can store and return execution metadata."""
1824+
tool = Tool(
1825+
name="my_tool",
1826+
description="A tool with execution",
1827+
parameters={"type": "object", "properties": {}},
1828+
execution=ToolExecution(taskSupport="optional"),
1829+
)
1830+
1831+
mcp_tool = tool.to_mcp_tool()
1832+
assert mcp_tool.execution is not None
1833+
assert mcp_tool.execution.taskSupport == "optional"
1834+
1835+
def test_tool_without_execution_field(self):
1836+
"""Test that Tool without execution returns None."""
1837+
tool = Tool(
1838+
name="my_tool",
1839+
description="A tool without execution",
1840+
parameters={"type": "object", "properties": {}},
1841+
)
1842+
1843+
mcp_tool = tool.to_mcp_tool()
1844+
assert mcp_tool.execution is None
1845+
1846+
def test_execution_override_takes_precedence(self):
1847+
"""Test that explicit override takes precedence over field value."""
1848+
tool = Tool(
1849+
name="my_tool",
1850+
description="A tool",
1851+
parameters={"type": "object", "properties": {}},
1852+
execution=ToolExecution(taskSupport="optional"),
1853+
)
1854+
1855+
override_execution = ToolExecution(taskSupport="required")
1856+
mcp_tool = tool.to_mcp_tool(execution=override_execution)
1857+
assert mcp_tool.execution is not None
1858+
assert mcp_tool.execution.taskSupport == "required"
1859+
1860+
async def test_function_tool_task_config_still_works(self):
1861+
"""FunctionTool should still derive execution from task_config."""
1862+
1863+
async def my_fn() -> str:
1864+
return "hello"
1865+
1866+
tool = Tool.from_function(my_fn, task=True)
1867+
mcp_tool = tool.to_mcp_tool()
1868+
1869+
# FunctionTool sets execution from task_config
1870+
assert mcp_tool.execution is not None
1871+
assert mcp_tool.execution.taskSupport == "optional"
1872+
1873+
def test_tool_execution_required_mode(self):
1874+
"""Test that Tool can store required execution mode."""
1875+
tool = Tool(
1876+
name="my_tool",
1877+
description="A tool with required execution",
1878+
parameters={"type": "object", "properties": {}},
1879+
execution=ToolExecution(taskSupport="required"),
1880+
)
1881+
1882+
mcp_tool = tool.to_mcp_tool()
1883+
assert mcp_tool.execution is not None
1884+
assert mcp_tool.execution.taskSupport == "required"
1885+
1886+
def test_tool_execution_forbidden_mode(self):
1887+
"""Test that Tool can store forbidden execution mode."""
1888+
tool = Tool(
1889+
name="my_tool",
1890+
description="A tool with forbidden execution",
1891+
parameters={"type": "object", "properties": {}},
1892+
execution=ToolExecution(taskSupport="forbidden"),
1893+
)
1894+
1895+
mcp_tool = tool.to_mcp_tool()
1896+
assert mcp_tool.execution is not None
1897+
assert mcp_tool.execution.taskSupport == "forbidden"

0 commit comments

Comments
 (0)