Decorators return functions instead of component objects (#2856)

This commit is contained in:
Jeremiah Lowin 2026-01-12 21:58:07 -05:00 committed by GitHub
commit 1b723f302d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 2406 additions and 1606 deletions

View file

@ -0,0 +1,121 @@
"""Test that deprecated import paths for function components still work."""
import warnings
import pytest
from fastmcp.utilities.tests import temporary_settings
class TestDeprecatedFunctionToolImports:
def test_function_tool_from_tool_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning, match="Import from fastmcp.tools.function_tool"
):
from fastmcp.tools.tool import FunctionTool
# Verify it's the real class
from fastmcp.tools.function_tool import (
FunctionTool as CanonicalFunctionTool,
)
assert FunctionTool is CanonicalFunctionTool
def test_parsed_function_from_tool_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning, match="Import from fastmcp.tools.function_tool"
):
from fastmcp.tools.tool import ParsedFunction
from fastmcp.tools.function_tool import (
ParsedFunction as CanonicalParsedFunction,
)
assert ParsedFunction is CanonicalParsedFunction
def test_tool_decorator_from_tool_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning, match="Import from fastmcp.tools.function_tool"
):
from fastmcp.tools.tool import tool
from fastmcp.tools.function_tool import tool as canonical_tool
assert tool is canonical_tool
def test_no_warning_when_disabled(self):
with temporary_settings(deprecation_warnings=False):
with warnings.catch_warnings():
warnings.simplefilter("error")
from fastmcp.tools.tool import FunctionTool # noqa: F401
class TestDeprecatedFunctionResourceImports:
def test_function_resource_from_resource_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning,
match="Import from fastmcp.resources.function_resource",
):
from fastmcp.resources.resource import FunctionResource
from fastmcp.resources.function_resource import (
FunctionResource as CanonicalFunctionResource,
)
assert FunctionResource is CanonicalFunctionResource
def test_resource_decorator_from_resource_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning,
match="Import from fastmcp.resources.function_resource",
):
from fastmcp.resources.resource import resource
from fastmcp.resources.function_resource import (
resource as canonical_resource,
)
assert resource is canonical_resource
def test_no_warning_when_disabled(self):
with temporary_settings(deprecation_warnings=False):
with warnings.catch_warnings():
warnings.simplefilter("error")
from fastmcp.resources.resource import FunctionResource # noqa: F401
class TestDeprecatedFunctionPromptImports:
def test_function_prompt_from_prompt_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning, match="Import from fastmcp.prompts.function_prompt"
):
from fastmcp.prompts.prompt import FunctionPrompt
from fastmcp.prompts.function_prompt import (
FunctionPrompt as CanonicalFunctionPrompt,
)
assert FunctionPrompt is CanonicalFunctionPrompt
def test_prompt_decorator_from_prompt_module(self):
with temporary_settings(deprecation_warnings=True):
with pytest.warns(
DeprecationWarning, match="Import from fastmcp.prompts.function_prompt"
):
from fastmcp.prompts.prompt import prompt
from fastmcp.prompts.function_prompt import prompt as canonical_prompt
assert prompt is canonical_prompt
def test_no_warning_when_disabled(self):
with temporary_settings(deprecation_warnings=False):
with warnings.catch_warnings():
warnings.simplefilter("error")
from fastmcp.prompts.prompt import FunctionPrompt # noqa: F401

View file

@ -5,7 +5,8 @@ from mcp.types import TextContent, TextResourceContents
from fastmcp.client.client import Client
from fastmcp.server.server import FastMCP
from fastmcp.tools.tool import FunctionTool, Tool
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool import Tool
from tests.conftest import get_fn_name

View file

@ -1,39 +1,47 @@
"""Tests for the standalone @prompt decorator.
The @prompt decorator creates FunctionPrompt objects without registering them
to a server. Objects can be added explicitly via server.add_prompt() or
The @prompt decorator attaches metadata to functions without registering them
to a server. Functions can be added explicitly via server.add_prompt() or
discovered by FileSystemProvider.
"""
from typing import cast
import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.prompts import FunctionPrompt, prompt
from fastmcp.prompts import prompt
from fastmcp.prompts.function_prompt import DecoratedPrompt, PromptMeta
class TestPromptDecorator:
"""Tests for the @prompt decorator."""
def test_prompt_without_parens(self):
"""@prompt without parentheses should create a FunctionPrompt."""
"""@prompt without parentheses should attach metadata."""
@prompt
def analyze(topic: str) -> str:
return f"Analyze: {topic}"
assert isinstance(analyze, FunctionPrompt)
assert analyze.name == "analyze"
decorated = cast(DecoratedPrompt, analyze)
assert callable(analyze)
assert hasattr(analyze, "__fastmcp__")
assert isinstance(decorated.__fastmcp__, PromptMeta)
assert decorated.__fastmcp__.name is None # Uses function name by default
def test_prompt_with_empty_parens(self):
"""@prompt() with empty parentheses should create a FunctionPrompt."""
"""@prompt() with empty parentheses should attach metadata."""
@prompt()
def analyze(topic: str) -> str:
return f"Analyze: {topic}"
assert isinstance(analyze, FunctionPrompt)
assert analyze.name == "analyze"
decorated = cast(DecoratedPrompt, analyze)
assert callable(analyze)
assert hasattr(analyze, "__fastmcp__")
assert isinstance(decorated.__fastmcp__, PromptMeta)
def test_prompt_with_name_arg(self):
"""@prompt("name") with name as first arg should work."""
@ -42,8 +50,10 @@ class TestPromptDecorator:
def analyze(topic: str) -> str:
return f"Analyze: {topic}"
assert isinstance(analyze, FunctionPrompt)
assert analyze.name == "custom-analyze"
decorated = cast(DecoratedPrompt, analyze)
assert callable(analyze)
assert hasattr(analyze, "__fastmcp__")
assert decorated.__fastmcp__.name == "custom-analyze"
def test_prompt_with_name_kwarg(self):
"""@prompt(name="name") with keyword arg should work."""
@ -52,8 +62,10 @@ class TestPromptDecorator:
def analyze(topic: str) -> str:
return f"Analyze: {topic}"
assert isinstance(analyze, FunctionPrompt)
assert analyze.name == "custom-analyze"
decorated = cast(DecoratedPrompt, analyze)
assert callable(analyze)
assert hasattr(analyze, "__fastmcp__")
assert decorated.__fastmcp__.name == "custom-analyze"
def test_prompt_with_all_metadata(self):
"""@prompt with all metadata should store it all."""
@ -62,29 +74,32 @@ class TestPromptDecorator:
name="custom-analyze",
title="Analysis Prompt",
description="Analyzes topics",
tags={"analysis"},
tags={"analysis", "demo"},
meta={"custom": "value"},
)
def analyze(topic: str) -> str:
return f"Analyze: {topic}"
assert isinstance(analyze, FunctionPrompt)
assert analyze.name == "custom-analyze"
assert analyze.title == "Analysis Prompt"
assert analyze.description == "Analyzes topics"
assert analyze.tags == {"analysis"}
assert analyze.meta == {"custom": "value"}
decorated = cast(DecoratedPrompt, analyze)
assert callable(analyze)
assert hasattr(analyze, "__fastmcp__")
assert decorated.__fastmcp__.name == "custom-analyze"
assert decorated.__fastmcp__.title == "Analysis Prompt"
assert decorated.__fastmcp__.description == "Analyzes topics"
assert decorated.__fastmcp__.tags == {"analysis", "demo"}
assert decorated.__fastmcp__.meta == {"custom": "value"}
async def test_prompt_can_be_rendered(self):
"""Prompt created by @prompt should be renderable."""
async def test_prompt_function_still_callable(self):
"""Decorated function should still be directly callable."""
@prompt
def analyze(topic: str) -> str:
"""Analyze a topic."""
return f"Analyze: {topic}"
return f"Please analyze: {topic}"
result = await analyze.render({"topic": "Python"})
assert result.messages[0].content.text == "Analyze: Python" # type: ignore[union-attr]
# The function is still callable even though it has metadata
result = cast(DecoratedPrompt, analyze)("Python")
assert result == "Please analyze: Python"
def test_prompt_rejects_classmethod_decorator(self):
"""@prompt should reject classmethod-decorated functions."""
@ -93,12 +108,12 @@ class TestPromptDecorator:
class MyClass:
@prompt # type: ignore[arg-type]
@classmethod
def my_prompt(cls) -> str:
return "hello"
def my_prompt(cls, topic: str) -> str:
return f"Analyze: {topic}"
def test_prompt_with_both_name_args_raises(self):
"""@prompt should raise if both positional and keyword name are given."""
with pytest.raises(TypeError, match="Cannot specify both"):
with pytest.raises(TypeError, match="Cannot specify.*both.*argument.*keyword"):
@prompt("name1", name="name2") # type: ignore[call-overload]
def my_prompt() -> str:
@ -119,5 +134,5 @@ class TestPromptDecorator:
prompts = await client.list_prompts()
assert any(p.name == "analyze" for p in prompts)
result = await client.get_prompt("analyze", {"topic": "Python"})
assert "Python" in str(result)
result = await client.get_prompt("analyze", {"topic": "FastMCP"})
assert "FastMCP" in str(result)

View file

@ -1,7 +1,8 @@
import pytest
from pydantic import AnyUrl, BaseModel
from fastmcp.resources.resource import FunctionResource, ResourceContent
from fastmcp.resources.function_resource import FunctionResource
from fastmcp.resources.resource import ResourceContent
class TestFunctionResource:

View file

@ -6,7 +6,7 @@ from pydantic import BaseModel
from fastmcp import Context, FastMCP
from fastmcp.resources import ResourceTemplate
from fastmcp.resources.resource import FunctionResource
from fastmcp.resources.function_resource import FunctionResource
from fastmcp.resources.template import match_uri_template

View file

@ -4,7 +4,7 @@ from pydantic import AnyUrl, BaseModel
from fastmcp import Client, FastMCP
from fastmcp.resources import Resource, ResourceContent, ResourceResult
from fastmcp.resources.resource import FunctionResource
from fastmcp.resources.function_resource import FunctionResource
class TestResourceValidation:

View file

@ -1,16 +1,18 @@
"""Tests for the standalone @resource decorator.
The @resource decorator creates Resource or ResourceTemplate objects without
registering them to a server. Objects can be added explicitly via
server.add_resource() / server.add_template() or discovered by FileSystemProvider.
The @resource decorator attaches metadata to functions without registering them
to a server. Functions can be added explicitly via server.add_resource() /
server.add_template() or discovered by FileSystemProvider.
"""
from typing import cast
import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.resources import FunctionResource, resource
from fastmcp.resources.template import FunctionResourceTemplate
from fastmcp.resources import resource
from fastmcp.resources.function_resource import DecoratedResource, ResourceMeta
class TestResourceDecorator:
@ -25,34 +27,41 @@ class TestResourceDecorator:
return "{}"
def test_resource_with_uri(self):
"""@resource("uri") should create a FunctionResource."""
"""@resource("uri") should attach metadata."""
@resource("config://app")
def get_config() -> dict:
return {"setting": "value"}
assert isinstance(get_config, FunctionResource)
assert str(get_config.uri) == "config://app"
decorated = cast(DecoratedResource, get_config)
assert callable(get_config)
assert hasattr(get_config, "__fastmcp__")
assert isinstance(decorated.__fastmcp__, ResourceMeta)
assert decorated.__fastmcp__.uri == "config://app"
def test_resource_with_template_uri(self):
"""@resource with template URI should create a FunctionResourceTemplate."""
"""@resource with template URI should attach metadata."""
@resource("users://{user_id}/profile")
def get_profile(user_id: str) -> dict:
return {"id": user_id}
assert isinstance(get_profile, FunctionResourceTemplate)
assert get_profile.uri_template == "users://{user_id}/profile"
decorated = cast(DecoratedResource, get_profile)
assert callable(get_profile)
assert hasattr(get_profile, "__fastmcp__")
assert decorated.__fastmcp__.uri == "users://{user_id}/profile"
def test_resource_with_function_params_becomes_template(self):
"""@resource with function params and URI params should create a template."""
"""@resource with function params should attach metadata."""
@resource("data://items/{category}")
def get_items(category: str, limit: int = 10) -> list:
return list(range(limit))
assert isinstance(get_items, FunctionResourceTemplate)
assert get_items.uri_template == "data://items/{category}"
decorated = cast(DecoratedResource, get_items)
assert callable(get_items)
assert hasattr(get_items, "__fastmcp__")
assert decorated.__fastmcp__.uri == "data://items/{category}"
def test_resource_with_all_metadata(self):
"""@resource with all metadata should store it all."""
@ -69,36 +78,39 @@ class TestResourceDecorator:
def get_config() -> dict:
return {"setting": "value"}
assert isinstance(get_config, FunctionResource)
assert str(get_config.uri) == "config://app"
assert get_config.name == "app-config"
assert get_config.title == "Application Config"
assert get_config.description == "Gets app configuration"
assert get_config.mime_type == "application/json"
assert get_config.tags == {"config"}
assert get_config.meta == {"custom": "value"}
decorated = cast(DecoratedResource, get_config)
assert callable(get_config)
assert hasattr(get_config, "__fastmcp__")
assert decorated.__fastmcp__.uri == "config://app"
assert decorated.__fastmcp__.name == "app-config"
assert decorated.__fastmcp__.title == "Application Config"
assert decorated.__fastmcp__.description == "Gets app configuration"
assert decorated.__fastmcp__.mime_type == "application/json"
assert decorated.__fastmcp__.tags == {"config"}
assert decorated.__fastmcp__.meta == {"custom": "value"}
async def test_resource_can_be_read(self):
"""Resource created by @resource should be readable."""
async def test_resource_function_still_callable(self):
"""Decorated function should still be directly callable."""
@resource("config://app")
def get_config() -> dict:
"""Get config."""
return {"setting": "value"}
assert isinstance(get_config, FunctionResource)
result = await get_config.read()
# The function is still callable even though it has metadata
result = cast(DecoratedResource, get_config)()
assert result == {"setting": "value"}
def test_resource_rejects_classmethod_decorator(self):
"""@resource should reject classmethod-decorated functions."""
with pytest.raises(TypeError, match="classmethod"):
class MyClass:
@resource("config://app") # type: ignore[arg-type]
@classmethod
def get_config(cls) -> str:
return "{}"
# Note: This now happens when added to server, not at decoration time
@resource("config://app")
def standalone() -> str:
return "{}"
# Should not raise at decoration
assert callable(standalone)
async def test_resource_added_to_server(self):
"""Resource created by @resource should work when added to a server."""
@ -108,7 +120,7 @@ class TestResourceDecorator:
"""Get config."""
return '{"version": "1.0"}'
assert isinstance(get_config, FunctionResource)
assert callable(get_config)
mcp = FastMCP("Test")
mcp.add_resource(get_config)
@ -128,10 +140,11 @@ class TestResourceDecorator:
"""Get user profile."""
return f'{{"id": "{user_id}"}}'
assert isinstance(get_profile, FunctionResourceTemplate)
assert callable(get_profile)
mcp = FastMCP("Test")
mcp.add_template(get_profile)
# add_resource handles both resources and templates based on metadata
mcp.add_resource(get_profile)
async with Client(mcp) as client:
templates = await client.list_resource_templates()

View file

@ -21,7 +21,8 @@ from pydantic import AnyUrl, BaseModel
from fastmcp import Context, FastMCP
from fastmcp.client.client import CallToolResult, Client
from fastmcp.client.transports import FastMCPTransport
from fastmcp.prompts.prompt import FunctionPrompt, Message, Prompt
from fastmcp.prompts.function_prompt import FunctionPrompt
from fastmcp.prompts.prompt import Message, Prompt
from fastmcp.resources.resource import Resource
from fastmcp.server.middleware.caching import (
CachableToolResult,

View file

@ -16,7 +16,8 @@ from fastmcp.server.middleware.tool_injection import (
ResourceToolMiddleware,
ToolInjectionMiddleware,
)
from fastmcp.tools.tool import FunctionTool, Tool
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool import Tool
def multiply_fn(a: int, b: int) -> int:

View file

@ -10,7 +10,7 @@ from mcp.types import TextContent
from fastmcp import Client, Context, FastMCP
from fastmcp.exceptions import NotFoundError
from fastmcp.prompts.prompt import FunctionPrompt, Prompt
from fastmcp.prompts.prompt import Prompt
class TestPromptContext:
@ -247,6 +247,10 @@ class TestPromptDecorator:
async def test_prompt_direct_function_call(self):
"""Test that prompts can be registered via direct function call."""
from typing import cast
from fastmcp.prompts.function_prompt import DecoratedPrompt
mcp = FastMCP()
def standalone_function() -> str:
@ -255,11 +259,16 @@ class TestPromptDecorator:
result_fn = mcp.prompt(standalone_function, name="direct_call_prompt")
assert isinstance(result_fn, FunctionPrompt)
# In new decorator mode, returns the function with metadata
decorated = cast(DecoratedPrompt, result_fn)
assert hasattr(result_fn, "__fastmcp__")
assert decorated.__fastmcp__.name == "direct_call_prompt"
assert result_fn is standalone_function
prompts = await mcp.get_prompts()
prompt = next(p for p in prompts if p.name == "direct_call_prompt")
assert prompt is result_fn
# Prompt is registered separately, not same object as decorated function
assert prompt.name == "direct_call_prompt"
result = await mcp.render_prompt("direct_call_prompt")
assert len(result.messages) == 1

View file

@ -793,6 +793,10 @@ class TestToolOutputSchema:
assert result.structured_content is None
async def test_manual_structured_content(self):
from typing import cast
from fastmcp.tools.function_tool import DecoratedTool
mcp = FastMCP()
@mcp.tool
@ -801,7 +805,12 @@ class TestToolOutputSchema:
content="Hello, world!", structured_content={"message": "Hello, world!"}
)
assert f.output_schema is None
# In new decorator mode, check metadata instead of attributes
from fastmcp.utilities.types import NotSet
decorated = cast(DecoratedTool, f)
assert hasattr(f, "__fastmcp__")
assert decorated.__fastmcp__.output_schema is NotSet
result = await mcp.call_tool("f", {})
assert isinstance(result.content, list)
@ -1312,7 +1321,9 @@ class TestToolDecorator:
async def test_tool_direct_function_call(self):
"""Test that tools can be registered via direct function call."""
from fastmcp.tools import FunctionTool
from typing import cast
from fastmcp.tools.function_tool import DecoratedTool
mcp = FastMCP()
@ -1322,11 +1333,16 @@ class TestToolDecorator:
result_fn = mcp.tool(standalone_function, name="direct_call_tool")
assert isinstance(result_fn, FunctionTool)
# In new decorator mode, returns the function with metadata
decorated = cast(DecoratedTool, result_fn)
assert hasattr(result_fn, "__fastmcp__")
assert decorated.__fastmcp__.name == "direct_call_tool"
assert result_fn is standalone_function
tools = await mcp.get_tools()
tool = next(t for t in tools if t.name == "direct_call_tool")
assert tool is result_fn
# Tool is registered separately, not same object as decorated function
assert tool.name == "direct_call_tool"
result = await mcp.call_tool("direct_call_tool", {"x": 5, "y": 3})
assert result.structured_content == {"result": 8}

View file

@ -8,9 +8,9 @@ ValueError when task=True is used with a sync function.
import pytest
from fastmcp import FastMCP
from fastmcp.prompts.prompt import FunctionPrompt
from fastmcp.resources.resource import FunctionResource
from fastmcp.tools.tool import FunctionTool
from fastmcp.prompts.function_prompt import FunctionPrompt
from fastmcp.resources.function_resource import FunctionResource
from fastmcp.tools.function_tool import FunctionTool
async def test_sync_tool_with_explicit_task_true_raises():

View file

@ -7,8 +7,10 @@ import pytest
from mcp.types import AnyUrl, TextContent
from fastmcp import FastMCP
from fastmcp.prompts.prompt import FunctionPrompt, Prompt
from fastmcp.resources.resource import FunctionResource, Resource
from fastmcp.prompts.function_prompt import FunctionPrompt
from fastmcp.prompts.prompt import Prompt
from fastmcp.resources.function_resource import FunctionResource
from fastmcp.resources.resource import Resource
from fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate
from fastmcp.server.providers import Provider
from fastmcp.tools.tool import Tool, ToolResult

View file

@ -1,39 +1,47 @@
"""Tests for the standalone @tool decorator.
The @tool decorator creates FunctionTool objects without registering them
to a server. Objects can be added explicitly via server.add_tool() or
The @tool decorator attaches metadata to functions without registering them
to a server. Functions can be added explicitly via server.add_tool() or
discovered by FileSystemProvider.
"""
from typing import cast
import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.tools import FunctionTool, tool
from fastmcp.tools import tool
from fastmcp.tools.function_tool import DecoratedTool, ToolMeta
class TestToolDecorator:
"""Tests for the @tool decorator."""
def test_tool_without_parens(self):
"""@tool without parentheses should create a FunctionTool."""
"""@tool without parentheses should attach metadata."""
@tool
def greet(name: str) -> str:
return f"Hello, {name}!"
assert isinstance(greet, FunctionTool)
assert greet.name == "greet"
decorated = cast(DecoratedTool, greet)
assert callable(greet)
assert hasattr(greet, "__fastmcp__")
assert isinstance(decorated.__fastmcp__, ToolMeta)
assert decorated.__fastmcp__.name is None # Uses function name by default
def test_tool_with_empty_parens(self):
"""@tool() with empty parentheses should create a FunctionTool."""
"""@tool() with empty parentheses should attach metadata."""
@tool()
def greet(name: str) -> str:
return f"Hello, {name}!"
assert isinstance(greet, FunctionTool)
assert greet.name == "greet"
decorated = cast(DecoratedTool, greet)
assert callable(greet)
assert hasattr(greet, "__fastmcp__")
assert isinstance(decorated.__fastmcp__, ToolMeta)
def test_tool_with_name_arg(self):
"""@tool("name") with name as first arg should work."""
@ -42,8 +50,10 @@ class TestToolDecorator:
def greet(name: str) -> str:
return f"Hello, {name}!"
assert isinstance(greet, FunctionTool)
assert greet.name == "custom-greet"
decorated = cast(DecoratedTool, greet)
assert callable(greet)
assert hasattr(greet, "__fastmcp__")
assert decorated.__fastmcp__.name == "custom-greet"
def test_tool_with_name_kwarg(self):
"""@tool(name="name") with keyword arg should work."""
@ -52,8 +62,10 @@ class TestToolDecorator:
def greet(name: str) -> str:
return f"Hello, {name}!"
assert isinstance(greet, FunctionTool)
assert greet.name == "custom-greet"
decorated = cast(DecoratedTool, greet)
assert callable(greet)
assert hasattr(greet, "__fastmcp__")
assert decorated.__fastmcp__.name == "custom-greet"
def test_tool_with_all_metadata(self):
"""@tool with all metadata should store it all."""
@ -68,23 +80,26 @@ class TestToolDecorator:
def greet(name: str) -> str:
return f"Hello, {name}!"
assert isinstance(greet, FunctionTool)
assert greet.name == "custom-greet"
assert greet.title == "Greeting Tool"
assert greet.description == "Greets people"
assert greet.tags == {"greeting", "demo"}
assert greet.meta == {"custom": "value"}
decorated = cast(DecoratedTool, greet)
assert callable(greet)
assert hasattr(greet, "__fastmcp__")
assert decorated.__fastmcp__.name == "custom-greet"
assert decorated.__fastmcp__.title == "Greeting Tool"
assert decorated.__fastmcp__.description == "Greets people"
assert decorated.__fastmcp__.tags == {"greeting", "demo"}
assert decorated.__fastmcp__.meta == {"custom": "value"}
async def test_tool_can_be_run(self):
"""Tool created by @tool should be runnable."""
async def test_tool_function_still_callable(self):
"""Decorated function should still be directly callable."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
result = await greet.run({"name": "World"})
assert result.content[0].text == "Hello, World!" # type: ignore[union-attr]
# The function is still callable even though it has metadata
result = cast(DecoratedTool, greet)("World")
assert result == "Hello, World!"
def test_tool_rejects_classmethod_decorator(self):
"""@tool should reject classmethod-decorated functions."""
@ -98,7 +113,7 @@ class TestToolDecorator:
def test_tool_with_both_name_args_raises(self):
"""@tool should raise if both positional and keyword name are given."""
with pytest.raises(TypeError, match="Cannot specify both"):
with pytest.raises(TypeError, match="Cannot specify.*both.*argument.*keyword"):
@tool("name1", name="name2") # type: ignore[call-overload]
def my_tool() -> str:

View file

@ -13,7 +13,8 @@ from fastmcp import FastMCP
from fastmcp.client.client import Client
from fastmcp.exceptions import ToolError
from fastmcp.tools import Tool, forward, forward_raw
from fastmcp.tools.tool import FunctionTool, ToolResult
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool import ToolResult
from fastmcp.tools.tool_transform import (
ArgTransform,
ToolTransformConfig,
@ -1052,7 +1053,10 @@ class TestEnableDisable:
def add(x: int, y: int = 10) -> int:
return x + y
new_add = Tool.from_tool(add, name="new_add")
# Get the registered Tool object from the server
add_tool = await mcp._local_provider.get_component("tool:add")
assert isinstance(add_tool, Tool)
new_add = Tool.from_tool(add_tool, name="new_add")
mcp.add_tool(new_add)
# Disable original tool, but new_add should still work
@ -1076,7 +1080,10 @@ class TestEnableDisable:
def add(x: int, y: int = 10) -> int:
return x + y
new_add = Tool.from_tool(add, name="new_add")
# Get the registered Tool object from the server
add_tool = await mcp._local_provider.get_component("tool:add")
assert isinstance(add_tool, Tool)
new_add = Tool.from_tool(add_tool, name="new_add")
mcp.add_tool(new_add)
# Disable both tools via server