From 2ffe68cfa196a09cfe877bf00f4de4644fc96dd8 Mon Sep 17 00:00:00 2001 From: chaoliang yan Date: Tue, 5 May 2026 02:22:31 +1000 Subject: [PATCH] fix: preserve tool decorator metadata (#4072) Generated with Codex. Co-authored-by: lawrence3699 --- src/fastmcp/tools/function_tool.py | 7 +++++- tests/tools/test_standalone_decorator.py | 30 +++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/tools/function_tool.py b/src/fastmcp/tools/function_tool.py index ffac25c98..5efafee57 100644 --- a/src/fastmcp/tools/function_tool.py +++ b/src/fastmcp/tools/function_tool.py @@ -24,7 +24,7 @@ from pydantic import Field from pydantic.json_schema import SkipJsonSchema import fastmcp -from fastmcp.decorators import resolve_task_config +from fastmcp.decorators import get_fastmcp_meta, resolve_task_config from fastmcp.exceptions import FastMCPDeprecationWarning from fastmcp.server.auth.authorization import AuthCheck from fastmcp.server.dependencies import without_injected_parameters @@ -169,6 +169,11 @@ class FunctionTool(Tool): "Use metadata alone or individual parameters alone." ) + if metadata is None and not individual_params_provided: + fmeta = get_fastmcp_meta(fn) + if isinstance(fmeta, ToolMeta): + metadata = fmeta + # Build metadata from kwargs if not provided if metadata is None: metadata = ToolMeta( diff --git a/tests/tools/test_standalone_decorator.py b/tests/tools/test_standalone_decorator.py index b9cc4a67b..e204a0a9c 100644 --- a/tests/tools/test_standalone_decorator.py +++ b/tests/tools/test_standalone_decorator.py @@ -12,7 +12,8 @@ import pytest from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.tools import tool -from fastmcp.tools.function_tool import DecoratedTool, ToolMeta +from fastmcp.tools.base import Tool +from fastmcp.tools.function_tool import DecoratedTool, FunctionTool, ToolMeta class TestToolDecorator: @@ -89,6 +90,33 @@ class TestToolDecorator: assert decorated.__fastmcp__.tags == {"greeting", "demo"} assert decorated.__fastmcp__.meta == {"custom": "value"} + @pytest.mark.parametrize( + "factory", [Tool.from_function, FunctionTool.from_function] + ) + def test_from_function_preserves_decorator_metadata(self, factory): + """Direct from_function calls should respect @tool metadata.""" + + @tool( + name="custom-greet", + version="v1", + title="Greeting Tool", + description="Greets people", + tags={"greeting", "demo"}, + meta={"custom": "value"}, + ) + def greet(name: str) -> str: + """Fallback description.""" + return f"Hello, {name}!" + + created_tool = factory(greet) + + assert created_tool.name == "custom-greet" + assert created_tool.version == "v1" + assert created_tool.title == "Greeting Tool" + assert created_tool.description == "Greets people" + assert created_tool.tags == {"greeting", "demo"} + assert created_tool.meta == {"custom": "value"} + async def test_tool_function_still_callable(self): """Decorated function should still be directly callable."""