Spaces:
Running
Running
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 |
-
"
|
| 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 |
-
- `
|
| 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 |
-
#
|
| 95 |
simplified = Tool.from_tool(
|
| 96 |
complex_tool,
|
| 97 |
-
transform_args={
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 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 |
-
#
|
| 214 |
-
ArgTransform(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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.
|
| 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.
|
| 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(
|
| 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(
|
| 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 |
|