Jeremiah Lowin commited on
Commit
90538d7
·
1 Parent(s): 9d9acc3

Add test for disabled transformation tools

Browse files
Files changed (1) hide show
  1. tests/tools/test_tool_transform.py +69 -24
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})