Jeremiah Lowin commited on
Commit
b5feac0
·
1 Parent(s): 41cf7e2

Use `hide` instead of `drop`

Browse files
src/fastmcp/tools/tool_transform.py CHANGED
@@ -23,7 +23,7 @@ new_tool = Tool.from_tool(
23
  original_tool,
24
  transform_args={
25
  "old_name": ArgTransform(name="new_name", description="Updated desc"),
26
- "unwanted": ArgTransform(drop=True),
27
  "simple": "renamed"
28
  }
29
  )
@@ -73,7 +73,7 @@ new_tool = Tool.from_tool(
73
  - `name`: Rename the argument
74
  - `description`: Change the description
75
  - `default`: Add/change default value
76
- - `drop=True`: Remove the argument entirely
77
 
78
  ## Common Patterns
79
 
@@ -91,10 +91,13 @@ enhanced = Tool.from_tool(
91
  # No transform_args = all parent args pass through unchanged
92
  )
93
 
94
- # Drop specific arguments
95
  simplified = Tool.from_tool(
96
  complex_tool,
97
- transform_args={"complex_config": None} # Drops only this arg
 
 
 
98
  )
99
  ```
100
  """
@@ -188,14 +191,14 @@ class ArgTransform:
188
 
189
  This class allows fine-grained control over how individual arguments are transformed
190
  when creating a new tool from an existing one. You can rename arguments, change their
191
- descriptions, add default values, or drop them entirely.
192
 
193
  Attributes:
194
  name: New name for the argument. Use None to keep original name, or ... for no change.
195
  description: New description for the argument. Use None to remove description, or ... for no change.
196
  default: New default value for the argument. Use ... for no change.
197
  type: New type for the argument. Use ... for no change.
198
- drop: If True, remove this argument from the transformed tool's schema.
199
 
200
  Examples:
201
  # Rename argument 'old_name' to 'new_name'
@@ -210,8 +213,11 @@ class ArgTransform:
210
  # Change the type
211
  ArgTransform(type=str)
212
 
213
- # Drop the argument entirely
214
- ArgTransform(drop=True)
 
 
 
215
 
216
  # Combine multiple transformations
217
  ArgTransform(name="new_name", description="New desc", default=None, type=int)
@@ -221,7 +227,7 @@ class ArgTransform:
221
  description: str | None | EllipsisType = ...
222
  default: Any | EllipsisType = ...
223
  type: Any | EllipsisType = ...
224
- drop: bool = False
225
 
226
 
227
  class TransformedTool(Tool):
@@ -393,7 +399,7 @@ class TransformedTool(Tool):
393
  for old_name, transform in transform_args.items():
394
  if isinstance(transform, str):
395
  new_names.append(transform)
396
- elif isinstance(transform, ArgTransform) and not transform.drop:
397
  if transform.name is not ... and transform.name is not None:
398
  new_names.append(transform.name)
399
  else:
@@ -456,6 +462,7 @@ class TransformedTool(Tool):
456
  new_props = {}
457
  new_required = set()
458
  new_to_old = {}
 
459
 
460
  for old_name, old_schema in parent_props.items():
461
  # Check if parameter is in transform_args
@@ -464,6 +471,21 @@ class TransformedTool(Tool):
464
  else:
465
  transform = ... # Default behavior - pass through
466
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  transform_result = cls._apply_single_transform(
468
  old_name,
469
  old_schema,
@@ -509,6 +531,9 @@ class TransformedTool(Tool):
509
  old_name = new_to_old.get(new_name, new_name)
510
  parent_args[old_name] = value
511
 
 
 
 
512
  return await parent_tool.run(parent_args)
513
 
514
  return schema, _forward
@@ -548,7 +573,7 @@ class TransformedTool(Tool):
548
  return transform, old_schema.copy(), is_required
549
 
550
  if isinstance(transform, ArgTransform):
551
- if transform.drop:
552
  return None
553
 
554
  if transform.name is not ...:
 
23
  original_tool,
24
  transform_args={
25
  "old_name": ArgTransform(name="new_name", description="Updated desc"),
26
+ "hidden_param": ArgTransform(hide=True, default="constant_value"),
27
  "simple": "renamed"
28
  }
29
  )
 
73
  - `name`: Rename the argument
74
  - `description`: Change the description
75
  - `default`: Add/change default value
76
+ - `hide=True`: Hide the argument from clients (pass constant value to parent)
77
 
78
  ## Common Patterns
79
 
 
91
  # No transform_args = all parent args pass through unchanged
92
  )
93
 
94
+ # Hide specific arguments with constant values
95
  simplified = Tool.from_tool(
96
  complex_tool,
97
+ transform_args={
98
+ "api_key": ArgTransform(hide=True, default="secret_key"), # Hidden constant
99
+ "debug": ArgTransform(hide=True) # Hidden, uses parent's default
100
+ }
101
  )
102
  ```
103
  """
 
191
 
192
  This class allows fine-grained control over how individual arguments are transformed
193
  when creating a new tool from an existing one. You can rename arguments, change their
194
+ descriptions, add default values, or hide them from clients while passing constants.
195
 
196
  Attributes:
197
  name: New name for the argument. Use None to keep original name, or ... for no change.
198
  description: New description for the argument. Use None to remove description, or ... for no change.
199
  default: New default value for the argument. Use ... for no change.
200
  type: New type for the argument. Use ... for no change.
201
+ hide: If True, hide this argument from clients but pass a constant value to parent.
202
 
203
  Examples:
204
  # Rename argument 'old_name' to 'new_name'
 
213
  # Change the type
214
  ArgTransform(type=str)
215
 
216
+ # Hide the argument entirely from clients
217
+ ArgTransform(hide=True)
218
+
219
+ # Hide argument but pass a constant value to parent
220
+ ArgTransform(hide=True, default="constant_value")
221
 
222
  # Combine multiple transformations
223
  ArgTransform(name="new_name", description="New desc", default=None, type=int)
 
227
  description: str | None | EllipsisType = ...
228
  default: Any | EllipsisType = ...
229
  type: Any | EllipsisType = ...
230
+ hide: bool = False
231
 
232
 
233
  class TransformedTool(Tool):
 
399
  for old_name, transform in transform_args.items():
400
  if isinstance(transform, str):
401
  new_names.append(transform)
402
+ elif isinstance(transform, ArgTransform) and not transform.hide:
403
  if transform.name is not ... and transform.name is not None:
404
  new_names.append(transform.name)
405
  else:
 
462
  new_props = {}
463
  new_required = set()
464
  new_to_old = {}
465
+ hidden_defaults = {} # Track hidden parameters with constant values
466
 
467
  for old_name, old_schema in parent_props.items():
468
  # Check if parameter is in transform_args
 
471
  else:
472
  transform = ... # Default behavior - pass through
473
 
474
+ # Handle hidden parameters with defaults
475
+ if isinstance(transform, ArgTransform) and transform.hide:
476
+ # Validate that hidden parameters without user defaults have parent defaults
477
+ if transform.default is ... and old_name in parent_required:
478
+ raise ValueError(
479
+ f"Hidden parameter '{old_name}' has no default value in parent tool "
480
+ f"and no default provided in ArgTransform. Either provide a default "
481
+ f"in ArgTransform or don't hide required parameters."
482
+ )
483
+ if transform.default is not ...:
484
+ # Hidden parameter with a constant value
485
+ hidden_defaults[old_name] = transform.default
486
+ # Skip adding to schema (not exposed to clients)
487
+ continue
488
+
489
  transform_result = cls._apply_single_transform(
490
  old_name,
491
  old_schema,
 
531
  old_name = new_to_old.get(new_name, new_name)
532
  parent_args[old_name] = value
533
 
534
+ # Add hidden defaults (constant values for hidden parameters)
535
+ parent_args.update(hidden_defaults)
536
+
537
  return await parent_tool.run(parent_args)
538
 
539
  return schema, _forward
 
573
  return transform, old_schema.copy(), is_required
574
 
575
  if isinstance(transform, ArgTransform):
576
+ if transform.hide:
577
  return None
578
 
579
  if transform.name is not ...:
tests/tools/test_tool_transform.py CHANGED
@@ -93,7 +93,7 @@ async def test_tool_drop_arg_with_none(add_tool):
93
 
94
  async def test_tool_drop_arg_with_arg_transform(add_tool):
95
  new_tool = Tool.from_tool(
96
- add_tool, transform_args={"old_y": ArgTransform(drop=True)}
97
  )
98
  assert sorted(new_tool.parameters["properties"]) == ["old_x"]
99
  result = await new_tool.run(arguments={"old_x": 1})
@@ -102,7 +102,7 @@ async def test_tool_drop_arg_with_arg_transform(add_tool):
102
 
103
  async def test_dropped_args_error_if_provided(add_tool):
104
  new_tool = Tool.from_tool(
105
- add_tool, transform_args={"old_y": ArgTransform(drop=True)}
106
  )
107
  with pytest.raises(
108
  TypeError, match="Got unexpected keyword argument\\(s\\): old_y"
@@ -110,6 +110,93 @@ async def test_dropped_args_error_if_provided(add_tool):
110
  await new_tool.run(arguments={"old_x": 1, "old_y": 2})
111
 
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  async def test_forward_with_argument_mapping(add_tool):
114
  """Test that forward() applies argument mapping correctly."""
115
 
 
93
 
94
  async def test_tool_drop_arg_with_arg_transform(add_tool):
95
  new_tool = Tool.from_tool(
96
+ add_tool, transform_args={"old_y": ArgTransform(hide=True)}
97
  )
98
  assert sorted(new_tool.parameters["properties"]) == ["old_x"]
99
  result = await new_tool.run(arguments={"old_x": 1})
 
102
 
103
  async def test_dropped_args_error_if_provided(add_tool):
104
  new_tool = Tool.from_tool(
105
+ add_tool, transform_args={"old_y": ArgTransform(hide=True)}
106
  )
107
  with pytest.raises(
108
  TypeError, match="Got unexpected keyword argument\\(s\\): old_y"
 
110
  await new_tool.run(arguments={"old_x": 1, "old_y": 2})
111
 
112
 
113
+ async def test_hidden_arg_with_constant_default(add_tool):
114
+ """Test that hidden argument with default value passes constant to parent."""
115
+ new_tool = Tool.from_tool(
116
+ add_tool, transform_args={"old_y": ArgTransform(hide=True, default=20)}
117
+ )
118
+ # Only old_x should be exposed
119
+ assert sorted(new_tool.parameters["properties"]) == ["old_x"]
120
+ # Should pass old_x=5 and old_y=20 to parent
121
+ result = await new_tool.run(arguments={"old_x": 5})
122
+ assert result[0].text == "25" # type: ignore
123
+
124
+
125
+ async def test_hidden_arg_without_default_uses_parent_default(add_tool):
126
+ """Test that hidden argument without default uses parent's default."""
127
+ new_tool = Tool.from_tool(
128
+ add_tool, transform_args={"old_y": ArgTransform(hide=True)}
129
+ )
130
+ # Only old_x should be exposed
131
+ assert sorted(new_tool.parameters["properties"]) == ["old_x"]
132
+ # Should pass old_x=3 and let parent use its default old_y=10
133
+ result = await new_tool.run(arguments={"old_x": 3})
134
+ assert result[0].text == "13" # type: ignore
135
+
136
+
137
+ async def test_mixed_hidden_args_with_custom_function(add_tool):
138
+ """Test custom function with both hidden constant and hidden default parameters."""
139
+
140
+ async def custom_fn(visible_x: int) -> int:
141
+ # This custom function should receive the transformed visible parameter
142
+ # and the hidden parameters should be automatically handled
143
+ result = await forward(visible_x=visible_x)
144
+ return result
145
+
146
+ new_tool = Tool.from_tool(
147
+ add_tool,
148
+ transform_fn=custom_fn,
149
+ transform_args={
150
+ "old_x": "visible_x", # Rename and expose
151
+ "old_y": ArgTransform(hide=True, default=25), # Hidden with constant
152
+ },
153
+ )
154
+
155
+ # Only visible_x should be exposed
156
+ assert sorted(new_tool.parameters["properties"]) == ["visible_x"]
157
+ # Should pass visible_x=7 as old_x=7 and old_y=25 to parent
158
+ result = await new_tool.run(arguments={"visible_x": 7})
159
+ assert result[0].text == "32" # type: ignore
160
+
161
+
162
+ async def test_hide_required_param_without_default_raises_error():
163
+ """Test that hiding a required parameter without providing default raises error."""
164
+
165
+ @Tool.from_function
166
+ def tool_with_required_param(required_param: int, optional_param: int = 10) -> int:
167
+ return required_param + optional_param
168
+
169
+ # This should raise an error because required_param has no default and we're not providing one
170
+ with pytest.raises(
171
+ ValueError,
172
+ match=r"Hidden parameter 'required_param' has no default value in parent tool",
173
+ ):
174
+ Tool.from_tool(
175
+ tool_with_required_param,
176
+ transform_args={"required_param": ArgTransform(hide=True)},
177
+ )
178
+
179
+
180
+ async def test_hide_required_param_with_user_default_works():
181
+ """Test that hiding a required parameter works when user provides a default."""
182
+
183
+ @Tool.from_function
184
+ def tool_with_required_param(required_param: int, optional_param: int = 10) -> int:
185
+ return required_param + optional_param
186
+
187
+ # This should work because we're providing a default for the hidden required param
188
+ new_tool = Tool.from_tool(
189
+ tool_with_required_param,
190
+ transform_args={"required_param": ArgTransform(hide=True, default=5)},
191
+ )
192
+
193
+ # Only optional_param should be exposed
194
+ assert sorted(new_tool.parameters["properties"]) == ["optional_param"]
195
+ # Should pass required_param=5 and optional_param=20 to parent
196
+ result = await new_tool.run(arguments={"optional_param": 20})
197
+ assert result[0].text == "25" # type: ignore
198
+
199
+
200
  async def test_forward_with_argument_mapping(add_tool):
201
  """Test that forward() applies argument mapping correctly."""
202