Ensure tool serializer is applied

This commit is contained in:
Jeremiah Lowin 2025-06-04 12:47:12 -04:00
commit cc541d6eee
5 changed files with 146 additions and 84 deletions

View file

@ -154,7 +154,6 @@ class FastMCP(Generic[LifespanResultT]):
self._additional_http_routes: list[BaseRoute] = []
self._tool_manager = ToolManager(
duplicate_behavior=on_duplicate_tools,
serializer=tool_serializer,
mask_error_details=self.settings.mask_error_details,
)
self._resource_manager = ResourceManager(
@ -165,6 +164,7 @@ class FastMCP(Generic[LifespanResultT]):
duplicate_behavior=on_duplicate_prompts,
mask_error_details=self.settings.mask_error_details,
)
self._tool_serializer = tool_serializer
if lifespan is None:
self._has_lifespan = False
@ -184,7 +184,7 @@ class FastMCP(Generic[LifespanResultT]):
if tools:
for tool in tools:
if not isinstance(tool, Tool):
tool = Tool.from_function(tool)
tool = Tool.from_function(tool, serializer=self._tool_serializer)
self.add_tool(tool)
# Set up MCP protocol handlers
@ -564,6 +564,7 @@ class FastMCP(Generic[LifespanResultT]):
tags=tags,
annotations=annotations,
exclude_args=exclude_args,
serializer=self._tool_serializer,
)
self.add_tool(tool)
return fn

View file

@ -23,11 +23,9 @@ class ToolManager:
def __init__(
self,
duplicate_behavior: DuplicateBehavior | None = None,
serializer: Callable[[Any], str] | None = None,
mask_error_details: bool = False,
):
self._tools: dict[str, Tool] = {}
self._serializer = serializer
self.mask_error_details = mask_error_details
# Default to "warn" if None is provided
@ -67,6 +65,7 @@ class ToolManager:
description: str | None = None,
tags: set[str] | None = None,
annotations: ToolAnnotations | None = None,
serializer: Callable[[Any], str] | None = None,
exclude_args: list[str] | None = None,
) -> Tool:
"""Add a tool to the server."""
@ -81,8 +80,8 @@ class ToolManager:
description=description,
tags=tags,
annotations=annotations,
serializer=self._serializer,
exclude_args=exclude_args,
serializer=serializer,
)
return self.add_tool(tool)

View file

@ -7,6 +7,7 @@ from pydantic import Field
from fastmcp import Client, FastMCP
from fastmcp.exceptions import NotFoundError
from fastmcp.prompts.prompt import Prompt
from fastmcp.resources import Resource, ResourceTemplate
from fastmcp.server.server import (
MountedServer,
add_resource_prefix,
@ -389,8 +390,10 @@ class TestResourceDecorator:
obj = MyClass("My prefix:")
mcp.add_resource_fn(
obj.get_data, uri="resource://data", name="instance-resource"
mcp.add_resource(
Resource.from_function(
obj.get_data, uri="resource://data", name="instance-resource"
)
)
async with Client(mcp) as client:
@ -407,8 +410,10 @@ class TestResourceDecorator:
def get_data(cls) -> str:
return f"{cls.prefix} Hello, world!"
mcp.add_resource_fn(
MyClass.get_data, uri="resource://data", name="class-resource"
mcp.add_resource(
Resource.from_function(
MyClass.get_data, uri="resource://data", name="class-resource"
)
)
async with Client(mcp) as client:
@ -508,9 +513,12 @@ class TestTemplateDecorator:
return f"{self.prefix} Data for {name}"
obj = MyClass("My prefix:")
mcp.add_resource_fn(
obj.get_data, uri="resource://{name}/data", name="instance-template"
template = ResourceTemplate.from_function(
obj.get_data,
uri_template="resource://{name}/data",
name="instance-template",
)
mcp.add_template(template)
async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")
@ -526,11 +534,12 @@ class TestTemplateDecorator:
def get_data(cls, name: str) -> str:
return f"{cls.prefix} Data for {name}"
mcp.add_resource_fn(
template = ResourceTemplate.from_function(
MyClass.get_data,
uri="resource://{name}/data",
uri_template="resource://{name}/data",
name="class-template",
)
mcp.add_template(template)
async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")

View file

@ -20,7 +20,7 @@ from fastmcp import Client, Context, FastMCP
from fastmcp.client.transports import FastMCPTransport
from fastmcp.exceptions import ToolError
from fastmcp.prompts.prompt import EmbeddedResource, Prompt, PromptMessage
from fastmcp.resources import FileResource
from fastmcp.resources import FileResource, ResourceTemplate
from fastmcp.resources.resource import FunctionResource
from fastmcp.tools.tool import Tool
from fastmcp.utilities.types import Image
@ -1074,7 +1074,10 @@ class TestResourceTemplateContext:
def __call__(self, param: str, ctx: Context) -> str:
return f"Resource template: {param} {ctx.request_id}"
mcp.add_resource_fn(MyResource(), uri="resource://{param}")
template = ResourceTemplate.from_function(
MyResource(), uri_template="resource://{param}"
)
mcp.add_template(template)
async with Client(mcp) as client:
result = await client.read_resource(AnyUrl("resource://test"))

View file

@ -11,6 +11,7 @@ from pydantic import BaseModel
from fastmcp import Context, FastMCP, Image
from fastmcp.exceptions import NotFoundError, ToolError
from fastmcp.tools import FunctionTool, ToolManager
from fastmcp.tools.tool import Tool
from fastmcp.utilities.tests import temporary_settings
@ -23,7 +24,8 @@ class TestAddTools:
return a + b
manager = ToolManager()
manager.add_tool_from_fn(add)
tool = Tool.from_function(add)
manager.add_tool(tool)
tool = manager.get_tool("add")
assert tool is not None
@ -40,7 +42,8 @@ class TestAddTools:
return f"Data from {url}"
manager = ToolManager()
manager.add_tool_from_fn(fetch_data)
tool = Tool.from_function(fetch_data)
manager.add_tool(tool)
tool = manager.get_tool("fetch_data")
assert tool is not None
@ -60,7 +63,8 @@ class TestAddTools:
return {"id": 1, **user.model_dump()}
manager = ToolManager()
manager.add_tool_from_fn(create_user)
tool = Tool.from_function(create_user)
manager.add_tool(tool)
tool = manager.get_tool("create_user")
assert tool is not None
@ -79,7 +83,8 @@ class TestAddTools:
return x + y
manager = ToolManager()
manager.add_tool_from_fn(Adder())
tool = Tool.from_function(Adder())
manager.add_tool(tool)
tool = manager.get_tool("Adder")
assert tool is not None
@ -98,7 +103,8 @@ class TestAddTools:
return x + y
manager = ToolManager()
manager.add_tool_from_fn(Adder())
tool = Tool.from_function(Adder())
manager.add_tool(tool)
tool = manager.get_tool("Adder")
assert tool is not None
@ -113,7 +119,8 @@ class TestAddTools:
return Image(data=data)
manager = ToolManager()
manager.add_tool_from_fn(image_tool)
tool = Tool.from_function(image_tool)
manager.add_tool(tool)
tool = manager.get_tool("image_tool")
result = await tool.run({"data": "test.png"})
@ -123,11 +130,13 @@ class TestAddTools:
def test_add_noncallable_tool(self):
manager = ToolManager()
with pytest.raises(TypeError, match="not a callable object"):
manager.add_tool_from_fn(1) # type: ignore
tool = Tool.from_function(1) # type: ignore
manager.add_tool(tool)
def test_add_lambda(self):
manager = ToolManager()
tool = manager.add_tool_from_fn(lambda x: x, name="my_tool")
tool = Tool.from_function(lambda x: x, name="my_tool")
manager.add_tool(tool)
assert tool.name == "my_tool"
def test_add_lambda_with_no_name(self):
@ -135,7 +144,8 @@ class TestAddTools:
with pytest.raises(
ValueError, match="You must provide a name for lambda functions"
):
manager.add_tool_from_fn(lambda x: x)
tool = Tool.from_function(lambda x: x)
manager.add_tool(tool)
def test_remove_tool_successfully(self):
"""Test removing an added tool by key."""
@ -144,7 +154,8 @@ class TestAddTools:
def add(a: int, b: int) -> int:
return a + b
manager.add_tool_from_fn(add)
tool = Tool.from_function(add)
manager.add_tool(tool)
assert manager.get_tool("add") is not None
manager.remove_tool("add")
@ -164,8 +175,10 @@ class TestAddTools:
def test_fn(x: int) -> int:
return x
manager.add_tool_from_fn(test_fn, name="test_tool")
manager.add_tool_from_fn(test_fn, name="test_tool")
tool1 = Tool.from_function(test_fn, name="test_tool")
manager.add_tool(tool1)
tool2 = Tool.from_function(test_fn, name="test_tool")
manager.add_tool(tool2)
assert "Tool already exists: test_tool" in caplog.text
# Should have the tool
@ -178,9 +191,11 @@ class TestAddTools:
return x
manager = ToolManager(duplicate_behavior="ignore")
manager.add_tool_from_fn(f)
tool1 = Tool.from_function(f)
manager.add_tool(tool1)
with caplog.at_level(logging.WARNING):
manager.add_tool_from_fn(f)
tool2 = Tool.from_function(f)
manager.add_tool(tool2)
assert "Tool already exists: f" not in caplog.text
def test_error_on_duplicate_tools(self):
@ -190,10 +205,12 @@ class TestAddTools:
def test_fn(x: int) -> int:
return x
manager.add_tool_from_fn(test_fn, name="test_tool")
tool1 = Tool.from_function(test_fn, name="test_tool")
manager.add_tool(tool1)
with pytest.raises(ValueError, match="Tool already exists: test_tool"):
manager.add_tool_from_fn(test_fn, name="test_tool")
tool2 = Tool.from_function(test_fn, name="test_tool")
manager.add_tool(tool2)
def test_replace_duplicate_tools(self):
"""Test replacing duplicate tools."""
@ -203,12 +220,14 @@ class TestAddTools:
return x
def replacement_fn(x: int) -> int:
return x * 2
return x + 1
manager.add_tool_from_fn(original_fn, name="test_tool")
manager.add_tool_from_fn(replacement_fn, name="test_tool")
tool1 = Tool.from_function(original_fn, name="test_tool")
manager.add_tool(tool1)
result = Tool.from_function(replacement_fn, name="test_tool")
manager.add_tool(result)
# Should have replaced with the new function
# Should have replaced with the new tool
tool = manager.get_tool("test_tool")
assert tool is not None
assert isinstance(tool, FunctionTool)
@ -224,8 +243,10 @@ class TestAddTools:
def replacement_fn(x: int) -> int:
return x * 2
manager.add_tool_from_fn(original_fn, name="test_tool")
result = manager.add_tool_from_fn(replacement_fn, name="test_tool")
tool1 = Tool.from_function(original_fn, name="test_tool")
manager.add_tool(tool1)
result = Tool.from_function(replacement_fn, name="test_tool")
manager.add_tool(result)
# Should keep the original
tool = manager.get_tool("test_tool")
@ -234,7 +255,7 @@ class TestAddTools:
assert tool.fn.__name__ == "original_fn"
# Result should be the original tool
assert isinstance(result, FunctionTool)
assert result.fn.__name__ == "original_fn"
assert result.fn.__name__ == "replacement_fn"
class TestToolTags:
@ -248,7 +269,8 @@ class TestToolTags:
return x * 2
manager = ToolManager()
tool = manager.add_tool_from_fn(example_tool, tags={"math", "utility"})
tool = Tool.from_function(example_tool, tags={"math", "utility"})
manager.add_tool(tool)
assert tool.tags == {"math", "utility"}
tool = manager.get_tool("example_tool")
@ -263,7 +285,8 @@ class TestToolTags:
return x * 2
manager = ToolManager()
tool = manager.add_tool_from_fn(example_tool, tags=set())
tool = Tool.from_function(example_tool, tags=set())
manager.add_tool(tool)
assert tool.tags == set()
@ -275,7 +298,8 @@ class TestToolTags:
return x * 2
manager = ToolManager()
tool = manager.add_tool_from_fn(example_tool, tags=None)
tool = Tool.from_function(example_tool, tags=None)
manager.add_tool(tool)
assert tool.tags == set()
@ -295,9 +319,12 @@ class TestToolTags:
return str(x)
manager = ToolManager()
manager.add_tool_from_fn(math_tool, tags={"math"})
manager.add_tool_from_fn(string_tool, tags={"string", "utility"})
manager.add_tool_from_fn(mixed_tool, tags={"math", "utility", "string"})
tool1 = Tool.from_function(math_tool, tags={"math"})
manager.add_tool(tool1)
tool2 = Tool.from_function(string_tool, tags={"string", "utility"})
manager.add_tool(tool2)
tool3 = Tool.from_function(mixed_tool, tags={"math", "utility", "string"})
manager.add_tool(tool3)
# Check if we can filter by tags when listing tools
math_tools = [tool for tool in manager.list_tools() if "math" in tool.tags]
@ -318,7 +345,8 @@ class TestCallTools:
return a + b
manager = ToolManager()
manager.add_tool_from_fn(add)
tool = Tool.from_function(add)
manager.add_tool(tool)
result = await manager.call_tool("add", {"a": 1, "b": 2})
assert result[0].text == "3" # type: ignore[attr-defined]
@ -329,7 +357,8 @@ class TestCallTools:
return n * 2
manager = ToolManager()
manager.add_tool_from_fn(double)
tool = Tool.from_function(double)
manager.add_tool(tool)
result = await manager.call_tool("double", {"n": 5})
assert result[0].text == "10" # type: ignore[attr-defined]
@ -342,7 +371,8 @@ class TestCallTools:
return x + y
manager = ToolManager()
manager.add_tool_from_fn(Adder())
tool = Tool.from_function(Adder())
manager.add_tool(tool)
result = await manager.call_tool("Adder", {"x": 1, "y": 2})
assert result[0].text == "3" # type: ignore[attr-defined]
@ -355,7 +385,8 @@ class TestCallTools:
return x + y
manager = ToolManager()
manager.add_tool_from_fn(Adder())
tool = Tool.from_function(Adder())
manager.add_tool(tool)
result = await manager.call_tool("Adder", {"x": 1, "y": 2})
assert result[0].text == "3" # type: ignore[attr-defined]
@ -365,7 +396,8 @@ class TestCallTools:
return a + b
manager = ToolManager()
manager.add_tool_from_fn(add)
tool = Tool.from_function(add)
manager.add_tool(tool)
result = await manager.call_tool("add", {"a": 1})
assert result[0].text == "2" # type: ignore[attr-defined]
@ -376,7 +408,8 @@ class TestCallTools:
return a + b
manager = ToolManager()
manager.add_tool_from_fn(add)
tool = Tool.from_function(add)
manager.add_tool(tool)
with pytest.raises(ToolError):
await manager.call_tool("add", {"a": 1})
@ -390,7 +423,8 @@ class TestCallTools:
return sum(vals)
manager = ToolManager()
manager.add_tool_from_fn(sum_vals)
tool = Tool.from_function(sum_vals)
manager.add_tool(tool)
result = await manager.call_tool("sum_vals", {"vals": [1, 2, 3]})
assert result[0].text == "6" # type: ignore[attr-defined]
@ -402,7 +436,8 @@ class TestCallTools:
return sum(vals)
manager = ToolManager()
manager.add_tool_from_fn(sum_vals)
tool = Tool.from_function(sum_vals)
manager.add_tool(tool)
# Try both with plain list and with JSON list
with temporary_settings(tool_attempt_parse_json_args=True):
@ -414,7 +449,8 @@ class TestCallTools:
return vals if isinstance(vals, str) else "".join(vals)
manager = ToolManager()
manager.add_tool_from_fn(concat_strs)
tool = Tool.from_function(concat_strs)
manager.add_tool(tool)
# Try both with plain python object and with JSON list
result = await manager.call_tool("concat_strs", {"vals": ["a", "b", "c"]})
@ -430,7 +466,8 @@ class TestCallTools:
return vals if isinstance(vals, str) else "".join(vals)
manager = ToolManager()
manager.add_tool_from_fn(concat_strs)
tool = Tool.from_function(concat_strs)
manager.add_tool(tool)
with temporary_settings(tool_attempt_parse_json_args=True):
result = await manager.call_tool("concat_strs", {"vals": '["a", "b", "c"]'})
@ -451,7 +488,8 @@ class TestCallTools:
return [x.name for x in tank.shrimp]
manager = ToolManager()
manager.add_tool_from_fn(name_shrimp)
tool = Tool.from_function(name_shrimp)
manager.add_tool(tool)
mcp = FastMCP()
context = Context(fastmcp=mcp)
@ -481,11 +519,10 @@ class TestCallTools:
mcp = FastMCP(tool_serializer=custom_serializer)
manager = mcp._tool_manager
@mcp.tool()
def get_data() -> dict:
return {"key": "value", "number": 123}
manager.add_tool_from_fn(get_data)
result = await manager.call_tool("get_data", {})
assert result[0].text == 'CUSTOM:{"key": "value", "number": 123}' # type: ignore[attr-defined]
@ -500,14 +537,13 @@ class TestCallTools:
mcp = FastMCP(tool_serializer=custom_serializer)
manager = mcp._tool_manager
@mcp.tool()
def get_data() -> list[dict]:
return [
{"key": "value", "number": 123},
{"key": "value2", "number": 456},
]
manager.add_tool_from_fn(get_data)
result = await manager.call_tool("get_data", {})
assert (
result[0].text # type: ignore[attr-defined]
@ -525,11 +561,10 @@ class TestCallTools:
mcp = FastMCP(tool_serializer=custom_serializer)
manager = mcp._tool_manager
@mcp.tool()
def get_data() -> uuid.UUID:
return uuid_result
manager.add_tool_from_fn(get_data)
result = await manager.call_tool("get_data", {})
assert result[0].text == pydantic_core.to_json(uuid_result).decode() # type: ignore[attr-defined]
@ -540,7 +575,8 @@ class TestToolSchema:
return a
manager = ToolManager()
tool = manager.add_tool_from_fn(something)
tool = Tool.from_function(something)
manager.add_tool(tool)
assert "ctx" not in json.dumps(tool.parameters)
assert "Context" not in json.dumps(tool.parameters)
@ -549,7 +585,8 @@ class TestToolSchema:
return a
manager = ToolManager()
tool = manager.add_tool_from_fn(something)
tool = Tool.from_function(something)
manager.add_tool(tool)
assert "ctx" not in json.dumps(tool.parameters)
assert "Context" not in json.dumps(tool.parameters)
@ -558,7 +595,8 @@ class TestToolSchema:
return a
manager = ToolManager()
tool = manager.add_tool_from_fn(something)
tool = Tool.from_function(something)
manager.add_tool(tool)
assert "ctx" not in json.dumps(tool.parameters)
assert "Context" not in json.dumps(tool.parameters)
@ -574,12 +612,13 @@ class TestContextHandling:
return str(x)
manager = ToolManager()
manager.add_tool_from_fn(tool_with_context)
tool = Tool.from_function(tool_with_context)
manager.add_tool(tool)
def tool_without_context(x: int) -> str:
return str(x)
manager.add_tool_from_fn(tool_without_context)
manager.add_tool(Tool.from_function(tool_without_context))
async def test_context_injection(self):
"""Test that context is properly injected during tool execution."""
@ -589,7 +628,8 @@ class TestContextHandling:
return str(x)
manager = ToolManager()
manager.add_tool_from_fn(tool_with_context)
tool = Tool.from_function(tool_with_context)
manager.add_tool(tool)
mcp = FastMCP()
context = Context(fastmcp=mcp)
@ -606,7 +646,8 @@ class TestContextHandling:
return str(x)
manager = ToolManager()
manager.add_tool_from_fn(async_tool)
tool = Tool.from_function(async_tool)
manager.add_tool(tool)
mcp = FastMCP()
context = Context(fastmcp=mcp)
@ -622,7 +663,8 @@ class TestContextHandling:
return x
manager = ToolManager()
manager.add_tool_from_fn(tool_with_context)
tool = Tool.from_function(tool_with_context)
manager.add_tool(tool)
# Should not raise an error when context is not provided
mcp = FastMCP()
@ -640,14 +682,16 @@ class TestContextHandling:
return str(x)
manager = ToolManager()
manager.add_tool_from_fn(tool_with_context)
tool = Tool.from_function(tool_with_context)
manager.add_tool(tool)
def test_annotated_context_parameter_detection(self):
def tool_with_context(x: int, ctx: Annotated[Context, "ctx"]) -> str:
return str(x)
manager = ToolManager()
manager.add_tool_from_fn(tool_with_context)
tool = Tool.from_function(tool_with_context)
manager.add_tool(tool)
def test_parameterized_union_context_parameter_detection(self):
"""Test that context parameters are properly detected in
@ -657,7 +701,8 @@ class TestContextHandling:
return str(x)
manager = ToolManager()
manager.add_tool_from_fn(tool_with_context)
tool = Tool.from_function(tool_with_context)
manager.add_tool(tool)
async def test_context_error_handling(self):
"""Test error handling when context injection fails."""
@ -666,7 +711,8 @@ class TestContextHandling:
raise ValueError("Test error")
manager = ToolManager()
manager.add_tool_from_fn(tool_with_context)
tool = Tool.from_function(tool_with_context)
manager.add_tool(tool)
mcp = FastMCP()
context = Context(fastmcp=mcp)
@ -688,7 +734,8 @@ class TestCustomToolNames:
return x * 2
manager = ToolManager()
tool = manager.add_tool_from_fn(original_fn, name="custom_name")
tool = Tool.from_function(original_fn, name="custom_name")
manager.add_tool(tool)
# The tool is stored under the custom name and its .name is also set to custom_name
assert manager.get_tool("custom_name") is not None
@ -706,7 +753,7 @@ class TestCustomToolNames:
return x + 1
# Create a tool with a specific name
tool = FunctionTool.from_function(fn, name="my_tool")
tool = Tool.from_function(fn, name="my_tool")
manager = ToolManager()
# Store it under a different name
manager.add_tool(tool, key="proxy_tool")
@ -727,7 +774,8 @@ class TestCustomToolNames:
return a * b
manager = ToolManager()
manager.add_tool_from_fn(multiply, name="custom_multiply")
tool = Tool.from_function(multiply, name="custom_multiply")
manager.add_tool(tool)
# Tool should be callable by its custom name
result = await manager.call_tool("custom_multiply", {"a": 5, "b": 3})
@ -750,11 +798,13 @@ class TestCustomToolNames:
manager = ToolManager(duplicate_behavior="replace")
# Add the original tool
original_tool = manager.add_tool_from_fn(original_fn, name="test_tool")
original_tool = Tool.from_function(original_fn, name="test_tool")
manager.add_tool(original_tool)
assert original_tool.name == "test_tool"
# Replace with a new function but keep the same registered name
replacement_tool = manager.add_tool_from_fn(replacement_fn, name="test_tool")
replacement_tool = Tool.from_function(replacement_fn, name="test_tool")
manager.add_tool(replacement_tool)
# The tool object should have been replaced
stored_tool = manager.get_tool("test_tool")
@ -780,7 +830,7 @@ class TestToolErrorHandling:
"""Tool that raises a ToolError."""
raise ToolError("Specific tool error")
manager.add_tool_from_fn(error_tool)
manager.add_tool(Tool.from_function(error_tool))
with pytest.raises(ToolError, match="Specific tool error"):
await manager.call_tool("error_tool", {"x": 42})
@ -793,7 +843,7 @@ class TestToolErrorHandling:
"""Tool that raises a ValueError."""
raise ValueError("Internal error details")
manager.add_tool_from_fn(buggy_tool)
manager.add_tool(Tool.from_function(buggy_tool))
with pytest.raises(ToolError) as excinfo:
await manager.call_tool("buggy_tool", {"x": 42})
@ -810,7 +860,7 @@ class TestToolErrorHandling:
"""Tool that raises a ValueError."""
raise ValueError("Internal error details")
manager.add_tool_from_fn(buggy_tool)
manager.add_tool(Tool.from_function(buggy_tool))
with pytest.raises(ToolError) as excinfo:
await manager.call_tool("buggy_tool", {"x": 42})
@ -827,7 +877,7 @@ class TestToolErrorHandling:
"""Async tool that raises a ToolError."""
raise ToolError("Async tool error")
manager.add_tool_from_fn(async_error_tool)
manager.add_tool(Tool.from_function(async_error_tool))
with pytest.raises(ToolError, match="Async tool error"):
await manager.call_tool("async_error_tool", {"x": 42})
@ -840,7 +890,7 @@ class TestToolErrorHandling:
"""Async tool that raises a ValueError."""
raise ValueError("Internal async error details")
manager.add_tool_from_fn(async_buggy_tool)
manager.add_tool(Tool.from_function(async_buggy_tool))
with pytest.raises(ToolError) as excinfo:
await manager.call_tool("async_buggy_tool", {"x": 42})
@ -857,7 +907,7 @@ class TestToolErrorHandling:
"""Async tool that raises a ValueError."""
raise ValueError("Internal async error details")
manager.add_tool_from_fn(async_buggy_tool)
manager.add_tool(Tool.from_function(async_buggy_tool))
with pytest.raises(ToolError) as excinfo:
await manager.call_tool("async_buggy_tool", {"x": 42})