Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
90538d7
1
Parent(s): 9d9acc3
Add test for disabled transformation tools
Browse files
tests/tools/test_tool_transform.py
CHANGED
|
@@ -9,6 +9,7 @@ from typing_extensions import TypedDict
|
|
| 9 |
|
| 10 |
from fastmcp import FastMCP
|
| 11 |
from fastmcp.client.client import Client
|
|
|
|
| 12 |
from fastmcp.tools import Tool, forward, forward_raw
|
| 13 |
from fastmcp.tools.tool import FunctionTool
|
| 14 |
from fastmcp.tools.tool_transform import ArgTransform, TransformedTool
|
|
@@ -51,7 +52,7 @@ async def test_tool_defaults_are_maintained_on_unmapped_args(add_tool):
|
|
| 51 |
add_tool, transform_args={"old_x": ArgTransform(name="new_x")}
|
| 52 |
)
|
| 53 |
result = await new_tool.run(arguments={"new_x": 1})
|
| 54 |
-
assert result[0].text == "11" # type: ignore
|
| 55 |
|
| 56 |
|
| 57 |
async def test_tool_defaults_are_maintained_on_mapped_args(add_tool):
|
|
@@ -59,7 +60,7 @@ async def test_tool_defaults_are_maintained_on_mapped_args(add_tool):
|
|
| 59 |
add_tool, transform_args={"old_y": ArgTransform(name="new_y")}
|
| 60 |
)
|
| 61 |
result = await new_tool.run(arguments={"old_x": 1})
|
| 62 |
-
assert result[0].text == "11" # type: ignore
|
| 63 |
|
| 64 |
|
| 65 |
def test_tool_change_arg_name(add_tool):
|
|
@@ -86,7 +87,7 @@ async def test_tool_drop_arg(add_tool):
|
|
| 86 |
)
|
| 87 |
assert sorted(new_tool.parameters["properties"]) == ["old_x"]
|
| 88 |
result = await new_tool.run(arguments={"old_x": 1})
|
| 89 |
-
assert result[0].text == "11" # type: ignore
|
| 90 |
|
| 91 |
|
| 92 |
async def test_dropped_args_error_if_provided(add_tool):
|
|
@@ -108,7 +109,7 @@ async def test_hidden_arg_with_constant_default(add_tool):
|
|
| 108 |
assert sorted(new_tool.parameters["properties"]) == ["old_x"]
|
| 109 |
# Should pass old_x=5 and old_y=20 to parent
|
| 110 |
result = await new_tool.run(arguments={"old_x": 5})
|
| 111 |
-
assert result[0].text == "25" # type: ignore
|
| 112 |
|
| 113 |
|
| 114 |
async def test_hidden_arg_without_default_uses_parent_default(add_tool):
|
|
@@ -120,7 +121,7 @@ async def test_hidden_arg_without_default_uses_parent_default(add_tool):
|
|
| 120 |
assert sorted(new_tool.parameters["properties"]) == ["old_x"]
|
| 121 |
# Should pass old_x=3 and let parent use its default old_y=10
|
| 122 |
result = await new_tool.run(arguments={"old_x": 3})
|
| 123 |
-
assert result[0].text == "13" # type: ignore
|
| 124 |
|
| 125 |
|
| 126 |
async def test_mixed_hidden_args_with_custom_function(add_tool):
|
|
@@ -145,7 +146,7 @@ async def test_mixed_hidden_args_with_custom_function(add_tool):
|
|
| 145 |
assert sorted(new_tool.parameters["properties"]) == ["visible_x"]
|
| 146 |
# Should pass visible_x=7 as old_x=7 and old_y=25 to parent
|
| 147 |
result = await new_tool.run(arguments={"visible_x": 7})
|
| 148 |
-
assert result[0].text == "32" # type: ignore
|
| 149 |
|
| 150 |
|
| 151 |
async def test_hide_required_param_without_default_raises_error():
|
|
@@ -183,7 +184,7 @@ async def test_hide_required_param_with_user_default_works():
|
|
| 183 |
assert sorted(new_tool.parameters["properties"]) == ["optional_param"]
|
| 184 |
# Should pass required_param=5 and optional_param=20 to parent
|
| 185 |
result = await new_tool.run(arguments={"optional_param": 20})
|
| 186 |
-
assert result[0].text == "25" # type: ignore
|
| 187 |
|
| 188 |
|
| 189 |
async def test_forward_with_argument_mapping(add_tool):
|
|
@@ -202,7 +203,7 @@ async def test_forward_with_argument_mapping(add_tool):
|
|
| 202 |
)
|
| 203 |
|
| 204 |
result = await new_tool.run(arguments={"new_x": 2, "new_y": 3})
|
| 205 |
-
assert result[0].text == "5" # type: ignore
|
| 206 |
|
| 207 |
|
| 208 |
async def test_forward_with_incorrect_args_raises_error(add_tool):
|
|
@@ -242,7 +243,7 @@ async def test_forward_raw_without_argument_mapping(add_tool):
|
|
| 242 |
)
|
| 243 |
|
| 244 |
result = await new_tool.run(arguments={"new_x": 2, "new_y": 3})
|
| 245 |
-
assert result[0].text == "5" # type: ignore
|
| 246 |
|
| 247 |
|
| 248 |
async def test_custom_fn_with_kwargs_and_no_transform_args(add_tool):
|
|
@@ -252,7 +253,7 @@ async def test_custom_fn_with_kwargs_and_no_transform_args(add_tool):
|
|
| 252 |
|
| 253 |
new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)
|
| 254 |
result = await new_tool.run(arguments={"extra": 1, "old_x": 2, "old_y": 3})
|
| 255 |
-
assert result[0].text == "6" # type: ignore
|
| 256 |
assert new_tool.parameters["required"] == IsList(
|
| 257 |
"extra", "old_x", check_order=False
|
| 258 |
)
|
|
@@ -269,7 +270,7 @@ async def test_fn_with_kwargs_passes_through_original_args(add_tool):
|
|
| 269 |
|
| 270 |
new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)
|
| 271 |
result = await new_tool.run(arguments={"new_y": 2, "old_y": 3})
|
| 272 |
-
assert result[0].text == "5" # type: ignore
|
| 273 |
|
| 274 |
|
| 275 |
async def test_fn_with_kwargs_receives_transformed_arg_names(add_tool):
|
|
@@ -287,7 +288,7 @@ async def test_fn_with_kwargs_receives_transformed_arg_names(add_tool):
|
|
| 287 |
transform_args={"old_x": ArgTransform(name="new_x")},
|
| 288 |
)
|
| 289 |
result = await new_tool.run(arguments={"new_x": 2, "old_y": 3})
|
| 290 |
-
assert result[0].text == "5" # type: ignore
|
| 291 |
|
| 292 |
|
| 293 |
async def test_fn_with_kwargs_handles_partial_explicit_args(add_tool):
|
|
@@ -307,7 +308,7 @@ async def test_fn_with_kwargs_handles_partial_explicit_args(add_tool):
|
|
| 307 |
result = await new_tool.run(
|
| 308 |
arguments={"new_x": 3, "old_y": 7, "some_other_param": "test"}
|
| 309 |
)
|
| 310 |
-
assert result[0].text == "10" # type: ignore
|
| 311 |
|
| 312 |
|
| 313 |
async def test_fn_with_kwargs_mixed_mapped_and_unmapped_args(add_tool):
|
|
@@ -325,7 +326,7 @@ async def test_fn_with_kwargs_mixed_mapped_and_unmapped_args(add_tool):
|
|
| 325 |
transform_args={"old_x": ArgTransform(name="new_x")},
|
| 326 |
) # only map 'a'
|
| 327 |
result = await new_tool.run(arguments={"new_x": 1, "old_y": 5})
|
| 328 |
-
assert result[0].text == "6" # type: ignore
|
| 329 |
|
| 330 |
|
| 331 |
async def test_fn_with_kwargs_dropped_args_not_in_kwargs(add_tool):
|
|
@@ -468,7 +469,7 @@ async def test_tool_transform_chaining(add_tool):
|
|
| 468 |
tool2 = Tool.from_tool(tool1, transform_args={"x": ArgTransform(name="final_x")})
|
| 469 |
|
| 470 |
result = await tool2.run(arguments={"final_x": 5})
|
| 471 |
-
assert result[0].text == "15" # type: ignore
|
| 472 |
|
| 473 |
# Transform tool1 with custom function that handles all parameters
|
| 474 |
async def custom(final_x: int, **kwargs) -> str:
|
|
@@ -479,7 +480,7 @@ async def test_tool_transform_chaining(add_tool):
|
|
| 479 |
tool1, transform_fn=custom, transform_args={"x": ArgTransform(name="final_x")}
|
| 480 |
)
|
| 481 |
result = await tool3.run(arguments={"final_x": 3, "old_y": 5})
|
| 482 |
-
assert result[0].text == "custom 8" # type: ignore
|
| 483 |
|
| 484 |
|
| 485 |
class MyModel(BaseModel):
|
|
@@ -634,7 +635,7 @@ async def test_arg_transform_precedence_over_function_with_kwargs():
|
|
| 634 |
# Test it works at runtime
|
| 635 |
result = await tool.run(arguments={"y": "test"})
|
| 636 |
# Should use ArgTransform default of 42
|
| 637 |
-
assert "42: test" in result[0].text # type: ignore
|
| 638 |
|
| 639 |
|
| 640 |
def test_arg_transform_combined_attributes():
|
|
@@ -691,8 +692,8 @@ async def test_arg_transform_type_precedence_runtime():
|
|
| 691 |
|
| 692 |
# Test it works with string input
|
| 693 |
result = await tool.run(arguments={"x": "5", "y": 3})
|
| 694 |
-
assert "String input '5'" in result[0].text # type: ignore
|
| 695 |
-
assert "result: 8" in result[0].text # type: ignore
|
| 696 |
|
| 697 |
|
| 698 |
class TestProxy:
|
|
@@ -727,7 +728,7 @@ class TestProxy:
|
|
| 727 |
async with Client(proxy_server) as client:
|
| 728 |
# The tool should be registered with its transformed name
|
| 729 |
result = await client.call_tool("add_transformed", {"new_x": 1, "old_y": 2})
|
| 730 |
-
assert result[0].text == "3" # type: ignore
|
| 731 |
|
| 732 |
|
| 733 |
async def test_arg_transform_default_factory():
|
|
@@ -750,7 +751,7 @@ async def test_arg_transform_default_factory():
|
|
| 750 |
|
| 751 |
# Should work without providing timestamp (gets value from factory)
|
| 752 |
result = await new_tool.run(arguments={"x": 42})
|
| 753 |
-
assert result[0].text == "42_12345.0" # type: ignore
|
| 754 |
|
| 755 |
|
| 756 |
async def test_arg_transform_default_factory_called_each_time():
|
|
@@ -778,11 +779,11 @@ async def test_arg_transform_default_factory_called_each_time():
|
|
| 778 |
|
| 779 |
# First call
|
| 780 |
result1 = await new_tool.run(arguments={"x": 1})
|
| 781 |
-
assert result1[0].text == "1_1" # type: ignore
|
| 782 |
|
| 783 |
# Second call should get a different value
|
| 784 |
result2 = await new_tool.run(arguments={"x": 2})
|
| 785 |
-
assert result2[0].text == "2_2" # type: ignore
|
| 786 |
|
| 787 |
|
| 788 |
async def test_arg_transform_hidden_with_default_factory():
|
|
@@ -807,7 +808,7 @@ async def test_arg_transform_hidden_with_default_factory():
|
|
| 807 |
|
| 808 |
# Should pass hidden request_id with factory value
|
| 809 |
result = await new_tool.run(arguments={"x": 42})
|
| 810 |
-
assert result[0].text == "42_req_123" # type: ignore
|
| 811 |
|
| 812 |
|
| 813 |
async def test_arg_transform_default_and_factory_raises_error():
|
|
@@ -942,3 +943,47 @@ async def test_arg_transform_hide_and_required_raises_error():
|
|
| 942 |
ValueError, match="Cannot specify both 'hide=True' and 'required=True'"
|
| 943 |
):
|
| 944 |
ArgTransform(hide=True, required=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
from fastmcp import FastMCP
|
| 11 |
from fastmcp.client.client import Client
|
| 12 |
+
from fastmcp.exceptions import ToolError
|
| 13 |
from fastmcp.tools import Tool, forward, forward_raw
|
| 14 |
from fastmcp.tools.tool import FunctionTool
|
| 15 |
from fastmcp.tools.tool_transform import ArgTransform, TransformedTool
|
|
|
|
| 52 |
add_tool, transform_args={"old_x": ArgTransform(name="new_x")}
|
| 53 |
)
|
| 54 |
result = await new_tool.run(arguments={"new_x": 1})
|
| 55 |
+
assert result[0].text == "11" # type: ignore[attr-defined]
|
| 56 |
|
| 57 |
|
| 58 |
async def test_tool_defaults_are_maintained_on_mapped_args(add_tool):
|
|
|
|
| 60 |
add_tool, transform_args={"old_y": ArgTransform(name="new_y")}
|
| 61 |
)
|
| 62 |
result = await new_tool.run(arguments={"old_x": 1})
|
| 63 |
+
assert result[0].text == "11" # type: ignore[attr-defined]
|
| 64 |
|
| 65 |
|
| 66 |
def test_tool_change_arg_name(add_tool):
|
|
|
|
| 87 |
)
|
| 88 |
assert sorted(new_tool.parameters["properties"]) == ["old_x"]
|
| 89 |
result = await new_tool.run(arguments={"old_x": 1})
|
| 90 |
+
assert result[0].text == "11" # type: ignore[attr-defined]
|
| 91 |
|
| 92 |
|
| 93 |
async def test_dropped_args_error_if_provided(add_tool):
|
|
|
|
| 109 |
assert sorted(new_tool.parameters["properties"]) == ["old_x"]
|
| 110 |
# Should pass old_x=5 and old_y=20 to parent
|
| 111 |
result = await new_tool.run(arguments={"old_x": 5})
|
| 112 |
+
assert result[0].text == "25" # type: ignore[attr-defined]
|
| 113 |
|
| 114 |
|
| 115 |
async def test_hidden_arg_without_default_uses_parent_default(add_tool):
|
|
|
|
| 121 |
assert sorted(new_tool.parameters["properties"]) == ["old_x"]
|
| 122 |
# Should pass old_x=3 and let parent use its default old_y=10
|
| 123 |
result = await new_tool.run(arguments={"old_x": 3})
|
| 124 |
+
assert result[0].text == "13" # type: ignore[attr-defined]
|
| 125 |
|
| 126 |
|
| 127 |
async def test_mixed_hidden_args_with_custom_function(add_tool):
|
|
|
|
| 146 |
assert sorted(new_tool.parameters["properties"]) == ["visible_x"]
|
| 147 |
# Should pass visible_x=7 as old_x=7 and old_y=25 to parent
|
| 148 |
result = await new_tool.run(arguments={"visible_x": 7})
|
| 149 |
+
assert result[0].text == "32" # type: ignore[attr-defined]
|
| 150 |
|
| 151 |
|
| 152 |
async def test_hide_required_param_without_default_raises_error():
|
|
|
|
| 184 |
assert sorted(new_tool.parameters["properties"]) == ["optional_param"]
|
| 185 |
# Should pass required_param=5 and optional_param=20 to parent
|
| 186 |
result = await new_tool.run(arguments={"optional_param": 20})
|
| 187 |
+
assert result[0].text == "25" # type: ignore[attr-defined]
|
| 188 |
|
| 189 |
|
| 190 |
async def test_forward_with_argument_mapping(add_tool):
|
|
|
|
| 203 |
)
|
| 204 |
|
| 205 |
result = await new_tool.run(arguments={"new_x": 2, "new_y": 3})
|
| 206 |
+
assert result[0].text == "5" # type: ignore[attr-defined]
|
| 207 |
|
| 208 |
|
| 209 |
async def test_forward_with_incorrect_args_raises_error(add_tool):
|
|
|
|
| 243 |
)
|
| 244 |
|
| 245 |
result = await new_tool.run(arguments={"new_x": 2, "new_y": 3})
|
| 246 |
+
assert result[0].text == "5" # type: ignore[attr-defined]
|
| 247 |
|
| 248 |
|
| 249 |
async def test_custom_fn_with_kwargs_and_no_transform_args(add_tool):
|
|
|
|
| 253 |
|
| 254 |
new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)
|
| 255 |
result = await new_tool.run(arguments={"extra": 1, "old_x": 2, "old_y": 3})
|
| 256 |
+
assert result[0].text == "6" # type: ignore[attr-defined]
|
| 257 |
assert new_tool.parameters["required"] == IsList(
|
| 258 |
"extra", "old_x", check_order=False
|
| 259 |
)
|
|
|
|
| 270 |
|
| 271 |
new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)
|
| 272 |
result = await new_tool.run(arguments={"new_y": 2, "old_y": 3})
|
| 273 |
+
assert result[0].text == "5" # type: ignore[attr-defined]
|
| 274 |
|
| 275 |
|
| 276 |
async def test_fn_with_kwargs_receives_transformed_arg_names(add_tool):
|
|
|
|
| 288 |
transform_args={"old_x": ArgTransform(name="new_x")},
|
| 289 |
)
|
| 290 |
result = await new_tool.run(arguments={"new_x": 2, "old_y": 3})
|
| 291 |
+
assert result[0].text == "5" # type: ignore[attr-defined]
|
| 292 |
|
| 293 |
|
| 294 |
async def test_fn_with_kwargs_handles_partial_explicit_args(add_tool):
|
|
|
|
| 308 |
result = await new_tool.run(
|
| 309 |
arguments={"new_x": 3, "old_y": 7, "some_other_param": "test"}
|
| 310 |
)
|
| 311 |
+
assert result[0].text == "10" # type: ignore[attr-defined]
|
| 312 |
|
| 313 |
|
| 314 |
async def test_fn_with_kwargs_mixed_mapped_and_unmapped_args(add_tool):
|
|
|
|
| 326 |
transform_args={"old_x": ArgTransform(name="new_x")},
|
| 327 |
) # only map 'a'
|
| 328 |
result = await new_tool.run(arguments={"new_x": 1, "old_y": 5})
|
| 329 |
+
assert result[0].text == "6" # type: ignore[attr-defined]
|
| 330 |
|
| 331 |
|
| 332 |
async def test_fn_with_kwargs_dropped_args_not_in_kwargs(add_tool):
|
|
|
|
| 469 |
tool2 = Tool.from_tool(tool1, transform_args={"x": ArgTransform(name="final_x")})
|
| 470 |
|
| 471 |
result = await tool2.run(arguments={"final_x": 5})
|
| 472 |
+
assert result[0].text == "15" # type: ignore[attr-defined]
|
| 473 |
|
| 474 |
# Transform tool1 with custom function that handles all parameters
|
| 475 |
async def custom(final_x: int, **kwargs) -> str:
|
|
|
|
| 480 |
tool1, transform_fn=custom, transform_args={"x": ArgTransform(name="final_x")}
|
| 481 |
)
|
| 482 |
result = await tool3.run(arguments={"final_x": 3, "old_y": 5})
|
| 483 |
+
assert result[0].text == "custom 8" # type: ignore[attr-defined]
|
| 484 |
|
| 485 |
|
| 486 |
class MyModel(BaseModel):
|
|
|
|
| 635 |
# Test it works at runtime
|
| 636 |
result = await tool.run(arguments={"y": "test"})
|
| 637 |
# Should use ArgTransform default of 42
|
| 638 |
+
assert "42: test" in result[0].text # type: ignore[attr-defined]
|
| 639 |
|
| 640 |
|
| 641 |
def test_arg_transform_combined_attributes():
|
|
|
|
| 692 |
|
| 693 |
# Test it works with string input
|
| 694 |
result = await tool.run(arguments={"x": "5", "y": 3})
|
| 695 |
+
assert "String input '5'" in result[0].text # type: ignore[attr-defined]
|
| 696 |
+
assert "result: 8" in result[0].text # type: ignore[attr-defined]
|
| 697 |
|
| 698 |
|
| 699 |
class TestProxy:
|
|
|
|
| 728 |
async with Client(proxy_server) as client:
|
| 729 |
# The tool should be registered with its transformed name
|
| 730 |
result = await client.call_tool("add_transformed", {"new_x": 1, "old_y": 2})
|
| 731 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
| 732 |
|
| 733 |
|
| 734 |
async def test_arg_transform_default_factory():
|
|
|
|
| 751 |
|
| 752 |
# Should work without providing timestamp (gets value from factory)
|
| 753 |
result = await new_tool.run(arguments={"x": 42})
|
| 754 |
+
assert result[0].text == "42_12345.0" # type: ignore[attr-defined]
|
| 755 |
|
| 756 |
|
| 757 |
async def test_arg_transform_default_factory_called_each_time():
|
|
|
|
| 779 |
|
| 780 |
# First call
|
| 781 |
result1 = await new_tool.run(arguments={"x": 1})
|
| 782 |
+
assert result1[0].text == "1_1" # type: ignore[attr-defined]
|
| 783 |
|
| 784 |
# Second call should get a different value
|
| 785 |
result2 = await new_tool.run(arguments={"x": 2})
|
| 786 |
+
assert result2[0].text == "2_2" # type: ignore[attr-defined]
|
| 787 |
|
| 788 |
|
| 789 |
async def test_arg_transform_hidden_with_default_factory():
|
|
|
|
| 808 |
|
| 809 |
# Should pass hidden request_id with factory value
|
| 810 |
result = await new_tool.run(arguments={"x": 42})
|
| 811 |
+
assert result[0].text == "42_req_123" # type: ignore[attr-defined]
|
| 812 |
|
| 813 |
|
| 814 |
async def test_arg_transform_default_and_factory_raises_error():
|
|
|
|
| 943 |
ValueError, match="Cannot specify both 'hide=True' and 'required=True'"
|
| 944 |
):
|
| 945 |
ArgTransform(hide=True, required=True)
|
| 946 |
+
|
| 947 |
+
|
| 948 |
+
class TestEnableDisable:
|
| 949 |
+
async def test_transform_disabled_tool(self):
|
| 950 |
+
"""
|
| 951 |
+
Tests that a transformed tool can run even if the parent tool is disabled
|
| 952 |
+
"""
|
| 953 |
+
mcp = FastMCP()
|
| 954 |
+
|
| 955 |
+
@mcp.tool(enabled=False)
|
| 956 |
+
def add(x: int, y: int = 10) -> int:
|
| 957 |
+
return x + y
|
| 958 |
+
|
| 959 |
+
new_add = Tool.from_tool(add, name="new_add")
|
| 960 |
+
mcp.add_tool(new_add)
|
| 961 |
+
|
| 962 |
+
assert new_add.enabled
|
| 963 |
+
|
| 964 |
+
async with Client(mcp) as client:
|
| 965 |
+
tools = await client.list_tools()
|
| 966 |
+
assert {tool.name for tool in tools} == {"new_add"}
|
| 967 |
+
|
| 968 |
+
result = await client.call_tool("new_add", {"x": 1, "y": 2})
|
| 969 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
| 970 |
+
|
| 971 |
+
with pytest.raises(ToolError):
|
| 972 |
+
await client.call_tool("add", {"x": 1, "y": 2})
|
| 973 |
+
|
| 974 |
+
async def test_disable_transformed_tool(self):
|
| 975 |
+
mcp = FastMCP()
|
| 976 |
+
|
| 977 |
+
@mcp.tool(enabled=False)
|
| 978 |
+
def add(x: int, y: int = 10) -> int:
|
| 979 |
+
return x + y
|
| 980 |
+
|
| 981 |
+
new_add = Tool.from_tool(add, name="new_add", enabled=False)
|
| 982 |
+
mcp.add_tool(new_add)
|
| 983 |
+
|
| 984 |
+
async with Client(mcp) as client:
|
| 985 |
+
tools = await client.list_tools()
|
| 986 |
+
assert len(tools) == 0
|
| 987 |
+
|
| 988 |
+
with pytest.raises(ToolError):
|
| 989 |
+
await client.call_tool("new_add", {"x": 1, "y": 2})
|