diff --git a/docs/python-sdk/fastmcp-tools-tool.mdx b/docs/python-sdk/fastmcp-tools-tool.mdx
index a5e3b8409..1fe8e4c9d 100644
--- a/docs/python-sdk/fastmcp-tools-tool.mdx
+++ b/docs/python-sdk/fastmcp-tools-tool.mdx
@@ -106,10 +106,10 @@ Schedule this tool for background execution via docket.
#### `from_tool`
```python
-from_tool(cls, tool: Tool) -> TransformedTool
+from_tool(cls, tool: Tool | Callable[..., Any]) -> TransformedTool
```
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
index 30fd08d5c..84a1b66cf 100644
--- a/docs/python-sdk/fastmcp-tools-tool_transform.mdx
+++ b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
@@ -62,7 +62,7 @@ y=2)` will call the parent tool with `x=1` and `y=2`.
- `RuntimeError`: If called outside a transformed tool context.
-### `apply_transformations_to_tools`
+### `apply_transformations_to_tools`
```python
apply_transformations_to_tools(tools: dict[str, Tool], transformations: dict[str, ToolTransformConfig]) -> dict[str, Tool]
@@ -213,7 +213,7 @@ functions.
#### `from_tool`
```python
-from_tool(cls, tool: Tool, name: str | None = None, version: str | NotSetT | None = NotSet, title: str | NotSetT | None = NotSet, description: str | NotSetT | None = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | NotSetT | None = NotSet, output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: Callable[[Any], str] | NotSetT | None = NotSet, meta: dict[str, Any] | NotSetT | None = NotSet) -> TransformedTool
+from_tool(cls, tool: Tool | Callable[..., Any], name: str | None = None, version: str | NotSetT | None = NotSet, title: str | NotSetT | None = NotSet, description: str | NotSetT | None = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | NotSetT | None = NotSet, output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: Callable[[Any], str] | NotSetT | None = NotSet, meta: dict[str, Any] | NotSetT | None = NotSet) -> TransformedTool
```
Create a transformed tool from a parent tool.
@@ -293,7 +293,7 @@ async def custom_output(**kwargs) -> ToolResult:
```
-### `ToolTransformConfig`
+### `ToolTransformConfig`
Provides a way to transform a tool.
@@ -301,7 +301,7 @@ Provides a way to transform a tool.
**Methods:**
-#### `apply`
+#### `apply`
```python
apply(self, tool: Tool) -> TransformedTool
diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py
index bdacdac58..3c99db465 100644
--- a/src/fastmcp/tools/tool.py
+++ b/src/fastmcp/tools/tool.py
@@ -363,7 +363,7 @@ class Tool(FastMCPComponent):
@classmethod
def from_tool(
cls,
- tool: Tool,
+ tool: Tool | Callable[..., Any],
*,
name: str | None = None,
title: str | NotSetT | None = NotSet,
@@ -378,6 +378,8 @@ class Tool(FastMCPComponent):
) -> TransformedTool:
from fastmcp.tools.tool_transform import TransformedTool
+ tool = cls._ensure_tool(tool)
+
return TransformedTool.from_tool(
tool=tool,
transform_fn=transform_fn,
@@ -392,6 +394,21 @@ class Tool(FastMCPComponent):
meta=meta,
)
+ @classmethod
+ def _ensure_tool(cls, tool: Tool | Callable[..., Any]) -> Tool:
+ """Coerce a callable into a Tool, respecting @tool decorator metadata."""
+ if isinstance(tool, Tool):
+ return tool
+
+ from fastmcp.decorators import get_fastmcp_meta
+ from fastmcp.tools.function_tool import FunctionTool, ToolMeta
+
+ fmeta = get_fastmcp_meta(tool)
+ if isinstance(fmeta, ToolMeta):
+ return FunctionTool.from_function(tool, metadata=fmeta)
+
+ return cls.from_function(tool)
+
def get_span_attributes(self) -> dict[str, Any]:
return super().get_span_attributes() | {
"fastmcp.component.type": "tool",
diff --git a/src/fastmcp/tools/tool_transform.py b/src/fastmcp/tools/tool_transform.py
index f22010750..9fc63e6e3 100644
--- a/src/fastmcp/tools/tool_transform.py
+++ b/src/fastmcp/tools/tool_transform.py
@@ -368,7 +368,7 @@ class TransformedTool(Tool):
@classmethod
def from_tool(
cls,
- tool: Tool,
+ tool: Tool | Callable[..., Any],
name: str | None = None,
version: str | NotSetT | None = NotSet,
title: str | NotSetT | None = NotSet,
@@ -456,6 +456,8 @@ class TransformedTool(Tool):
)
```
"""
+ tool = Tool._ensure_tool(tool)
+
if (
serializer is not NotSet
and serializer is not None
diff --git a/tests/tools/tool_transform/test_tool_transform.py b/tests/tools/tool_transform/test_tool_transform.py
index 47ab1853b..f6167a8ec 100644
--- a/tests/tools/tool_transform/test_tool_transform.py
+++ b/tests/tools/tool_transform/test_tool_transform.py
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field
from fastmcp import FastMCP
from fastmcp.client.client import Client
-from fastmcp.tools import Tool, forward, forward_raw
+from fastmcp.tools import Tool, forward, forward_raw, tool
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool import ToolResult
from fastmcp.tools.tool_transform import (
@@ -41,6 +41,60 @@ def test_tool_from_tool_no_change(add_tool):
assert new_tool.description == add_tool.description
+def test_from_tool_accepts_decorated_function():
+ @tool
+ def search(q: str, limit: int = 10) -> list[str]:
+ """Search for items."""
+ return [f"Result {i} for {q}" for i in range(limit)]
+
+ transformed = Tool.from_tool(
+ search,
+ name="find_items",
+ transform_args={"q": ArgTransform(name="query")},
+ )
+ assert isinstance(transformed, TransformedTool)
+ assert transformed.name == "find_items"
+ assert "query" in transformed.parameters["properties"]
+ assert "q" not in transformed.parameters["properties"]
+
+
+def test_from_tool_accepts_plain_function():
+ def search(q: str, limit: int = 10) -> list[str]:
+ return [f"Result {i} for {q}" for i in range(limit)]
+
+ transformed = Tool.from_tool(
+ search,
+ name="find_items",
+ transform_args={"q": ArgTransform(name="query")},
+ )
+ assert isinstance(transformed, TransformedTool)
+ assert transformed.name == "find_items"
+ assert "query" in transformed.parameters["properties"]
+
+
+def test_from_tool_decorated_function_preserves_metadata():
+ @tool(description="Custom description")
+ def search(q: str) -> list[str]:
+ """Original description."""
+ return []
+
+ transformed = Tool.from_tool(search)
+ assert transformed.parent_tool.description == "Custom description"
+
+
+async def test_from_tool_decorated_function_runs(add_tool):
+ @tool
+ def add(x: int, y: int = 10) -> int:
+ return x + y
+
+ transformed = Tool.from_tool(
+ add,
+ transform_args={"x": ArgTransform(name="a")},
+ )
+ result = await transformed.run(arguments={"a": 3, "y": 5})
+ assert result.structured_content == {"result": 8}
+
+
async def test_renamed_arg_description_is_maintained(add_tool):
new_tool = Tool.from_tool(
add_tool, transform_args={"old_x": ArgTransform(name="new_x")}
@@ -492,6 +546,29 @@ def test_function_with_kwargs_can_add_params(add_tool):
assert "new_x" in tool.parameters["properties"]
+async def test_from_tool_decorated_function_via_client():
+ @tool
+ def search(q: str, limit: int = 10) -> list[str]:
+ """Search for items."""
+ return [f"Result {i} for {q}" for i in range(limit)]
+
+ better_search = Tool.from_tool(
+ search,
+ name="find_items",
+ transform_args={
+ "q": ArgTransform(name="query", description="The search terms"),
+ },
+ )
+
+ mcp = FastMCP("Server")
+ mcp.add_tool(better_search)
+
+ async with Client(mcp) as client:
+ result = await client.call_tool("find_items", {"query": "hello", "limit": 3})
+ assert isinstance(result.content[0], TextContent)
+ assert "Result 0 for hello" in result.content[0].text
+
+
class TestProxy:
@pytest.fixture
def mcp_server(self) -> FastMCP: