mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-18 03:29:11 +02:00
Use hide instead of drop
This commit is contained in:
parent
642f9149d4
commit
52700a4e22
2 changed files with 125 additions and 13 deletions
|
|
@ -23,7 +23,7 @@ new_tool = Tool.from_tool(
|
|||
original_tool,
|
||||
transform_args={
|
||||
"old_name": ArgTransform(name="new_name", description="Updated desc"),
|
||||
"unwanted": ArgTransform(drop=True),
|
||||
"hidden_param": ArgTransform(hide=True, default="constant_value"),
|
||||
"simple": "renamed"
|
||||
}
|
||||
)
|
||||
|
|
@ -73,7 +73,7 @@ new_tool = Tool.from_tool(
|
|||
- `name`: Rename the argument
|
||||
- `description`: Change the description
|
||||
- `default`: Add/change default value
|
||||
- `drop=True`: Remove the argument entirely
|
||||
- `hide=True`: Hide the argument from clients (pass constant value to parent)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
|
|
@ -91,10 +91,13 @@ enhanced = Tool.from_tool(
|
|||
# No transform_args = all parent args pass through unchanged
|
||||
)
|
||||
|
||||
# Drop specific arguments
|
||||
# Hide specific arguments with constant values
|
||||
simplified = Tool.from_tool(
|
||||
complex_tool,
|
||||
transform_args={"complex_config": None} # Drops only this arg
|
||||
transform_args={
|
||||
"api_key": ArgTransform(hide=True, default="secret_key"), # Hidden constant
|
||||
"debug": ArgTransform(hide=True) # Hidden, uses parent's default
|
||||
}
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
|
@ -188,14 +191,14 @@ class ArgTransform:
|
|||
|
||||
This class allows fine-grained control over how individual arguments are transformed
|
||||
when creating a new tool from an existing one. You can rename arguments, change their
|
||||
descriptions, add default values, or drop them entirely.
|
||||
descriptions, add default values, or hide them from clients while passing constants.
|
||||
|
||||
Attributes:
|
||||
name: New name for the argument. Use None to keep original name, or ... for no change.
|
||||
description: New description for the argument. Use None to remove description, or ... for no change.
|
||||
default: New default value for the argument. Use ... for no change.
|
||||
type: New type for the argument. Use ... for no change.
|
||||
drop: If True, remove this argument from the transformed tool's schema.
|
||||
hide: If True, hide this argument from clients but pass a constant value to parent.
|
||||
|
||||
Examples:
|
||||
# Rename argument 'old_name' to 'new_name'
|
||||
|
|
@ -210,8 +213,11 @@ class ArgTransform:
|
|||
# Change the type
|
||||
ArgTransform(type=str)
|
||||
|
||||
# Drop the argument entirely
|
||||
ArgTransform(drop=True)
|
||||
# Hide the argument entirely from clients
|
||||
ArgTransform(hide=True)
|
||||
|
||||
# Hide argument but pass a constant value to parent
|
||||
ArgTransform(hide=True, default="constant_value")
|
||||
|
||||
# Combine multiple transformations
|
||||
ArgTransform(name="new_name", description="New desc", default=None, type=int)
|
||||
|
|
@ -221,7 +227,7 @@ class ArgTransform:
|
|||
description: str | None | EllipsisType = ...
|
||||
default: Any | EllipsisType = ...
|
||||
type: Any | EllipsisType = ...
|
||||
drop: bool = False
|
||||
hide: bool = False
|
||||
|
||||
|
||||
class TransformedTool(Tool):
|
||||
|
|
@ -393,7 +399,7 @@ class TransformedTool(Tool):
|
|||
for old_name, transform in transform_args.items():
|
||||
if isinstance(transform, str):
|
||||
new_names.append(transform)
|
||||
elif isinstance(transform, ArgTransform) and not transform.drop:
|
||||
elif isinstance(transform, ArgTransform) and not transform.hide:
|
||||
if transform.name is not ... and transform.name is not None:
|
||||
new_names.append(transform.name)
|
||||
else:
|
||||
|
|
@ -456,6 +462,7 @@ class TransformedTool(Tool):
|
|||
new_props = {}
|
||||
new_required = set()
|
||||
new_to_old = {}
|
||||
hidden_defaults = {} # Track hidden parameters with constant values
|
||||
|
||||
for old_name, old_schema in parent_props.items():
|
||||
# Check if parameter is in transform_args
|
||||
|
|
@ -464,6 +471,21 @@ class TransformedTool(Tool):
|
|||
else:
|
||||
transform = ... # Default behavior - pass through
|
||||
|
||||
# Handle hidden parameters with defaults
|
||||
if isinstance(transform, ArgTransform) and transform.hide:
|
||||
# Validate that hidden parameters without user defaults have parent defaults
|
||||
if transform.default is ... and old_name in parent_required:
|
||||
raise ValueError(
|
||||
f"Hidden parameter '{old_name}' has no default value in parent tool "
|
||||
f"and no default provided in ArgTransform. Either provide a default "
|
||||
f"in ArgTransform or don't hide required parameters."
|
||||
)
|
||||
if transform.default is not ...:
|
||||
# Hidden parameter with a constant value
|
||||
hidden_defaults[old_name] = transform.default
|
||||
# Skip adding to schema (not exposed to clients)
|
||||
continue
|
||||
|
||||
transform_result = cls._apply_single_transform(
|
||||
old_name,
|
||||
old_schema,
|
||||
|
|
@ -509,6 +531,9 @@ class TransformedTool(Tool):
|
|||
old_name = new_to_old.get(new_name, new_name)
|
||||
parent_args[old_name] = value
|
||||
|
||||
# Add hidden defaults (constant values for hidden parameters)
|
||||
parent_args.update(hidden_defaults)
|
||||
|
||||
return await parent_tool.run(parent_args)
|
||||
|
||||
return schema, _forward
|
||||
|
|
@ -548,7 +573,7 @@ class TransformedTool(Tool):
|
|||
return transform, old_schema.copy(), is_required
|
||||
|
||||
if isinstance(transform, ArgTransform):
|
||||
if transform.drop:
|
||||
if transform.hide:
|
||||
return None
|
||||
|
||||
if transform.name is not ...:
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ async def test_tool_drop_arg_with_none(add_tool):
|
|||
|
||||
async def test_tool_drop_arg_with_arg_transform(add_tool):
|
||||
new_tool = Tool.from_tool(
|
||||
add_tool, transform_args={"old_y": ArgTransform(drop=True)}
|
||||
add_tool, transform_args={"old_y": ArgTransform(hide=True)}
|
||||
)
|
||||
assert sorted(new_tool.parameters["properties"]) == ["old_x"]
|
||||
result = await new_tool.run(arguments={"old_x": 1})
|
||||
|
|
@ -102,7 +102,7 @@ async def test_tool_drop_arg_with_arg_transform(add_tool):
|
|||
|
||||
async def test_dropped_args_error_if_provided(add_tool):
|
||||
new_tool = Tool.from_tool(
|
||||
add_tool, transform_args={"old_y": ArgTransform(drop=True)}
|
||||
add_tool, transform_args={"old_y": ArgTransform(hide=True)}
|
||||
)
|
||||
with pytest.raises(
|
||||
TypeError, match="Got unexpected keyword argument\\(s\\): old_y"
|
||||
|
|
@ -110,6 +110,93 @@ async def test_dropped_args_error_if_provided(add_tool):
|
|||
await new_tool.run(arguments={"old_x": 1, "old_y": 2})
|
||||
|
||||
|
||||
async def test_hidden_arg_with_constant_default(add_tool):
|
||||
"""Test that hidden argument with default value passes constant to parent."""
|
||||
new_tool = Tool.from_tool(
|
||||
add_tool, transform_args={"old_y": ArgTransform(hide=True, default=20)}
|
||||
)
|
||||
# Only old_x should be exposed
|
||||
assert sorted(new_tool.parameters["properties"]) == ["old_x"]
|
||||
# Should pass old_x=5 and old_y=20 to parent
|
||||
result = await new_tool.run(arguments={"old_x": 5})
|
||||
assert result[0].text == "25" # type: ignore
|
||||
|
||||
|
||||
async def test_hidden_arg_without_default_uses_parent_default(add_tool):
|
||||
"""Test that hidden argument without default uses parent's default."""
|
||||
new_tool = Tool.from_tool(
|
||||
add_tool, transform_args={"old_y": ArgTransform(hide=True)}
|
||||
)
|
||||
# Only old_x should be exposed
|
||||
assert sorted(new_tool.parameters["properties"]) == ["old_x"]
|
||||
# Should pass old_x=3 and let parent use its default old_y=10
|
||||
result = await new_tool.run(arguments={"old_x": 3})
|
||||
assert result[0].text == "13" # type: ignore
|
||||
|
||||
|
||||
async def test_mixed_hidden_args_with_custom_function(add_tool):
|
||||
"""Test custom function with both hidden constant and hidden default parameters."""
|
||||
|
||||
async def custom_fn(visible_x: int) -> int:
|
||||
# This custom function should receive the transformed visible parameter
|
||||
# and the hidden parameters should be automatically handled
|
||||
result = await forward(visible_x=visible_x)
|
||||
return result
|
||||
|
||||
new_tool = Tool.from_tool(
|
||||
add_tool,
|
||||
transform_fn=custom_fn,
|
||||
transform_args={
|
||||
"old_x": "visible_x", # Rename and expose
|
||||
"old_y": ArgTransform(hide=True, default=25), # Hidden with constant
|
||||
},
|
||||
)
|
||||
|
||||
# Only visible_x should be exposed
|
||||
assert sorted(new_tool.parameters["properties"]) == ["visible_x"]
|
||||
# Should pass visible_x=7 as old_x=7 and old_y=25 to parent
|
||||
result = await new_tool.run(arguments={"visible_x": 7})
|
||||
assert result[0].text == "32" # type: ignore
|
||||
|
||||
|
||||
async def test_hide_required_param_without_default_raises_error():
|
||||
"""Test that hiding a required parameter without providing default raises error."""
|
||||
|
||||
@Tool.from_function
|
||||
def tool_with_required_param(required_param: int, optional_param: int = 10) -> int:
|
||||
return required_param + optional_param
|
||||
|
||||
# This should raise an error because required_param has no default and we're not providing one
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"Hidden parameter 'required_param' has no default value in parent tool",
|
||||
):
|
||||
Tool.from_tool(
|
||||
tool_with_required_param,
|
||||
transform_args={"required_param": ArgTransform(hide=True)},
|
||||
)
|
||||
|
||||
|
||||
async def test_hide_required_param_with_user_default_works():
|
||||
"""Test that hiding a required parameter works when user provides a default."""
|
||||
|
||||
@Tool.from_function
|
||||
def tool_with_required_param(required_param: int, optional_param: int = 10) -> int:
|
||||
return required_param + optional_param
|
||||
|
||||
# This should work because we're providing a default for the hidden required param
|
||||
new_tool = Tool.from_tool(
|
||||
tool_with_required_param,
|
||||
transform_args={"required_param": ArgTransform(hide=True, default=5)},
|
||||
)
|
||||
|
||||
# Only optional_param should be exposed
|
||||
assert sorted(new_tool.parameters["properties"]) == ["optional_param"]
|
||||
# Should pass required_param=5 and optional_param=20 to parent
|
||||
result = await new_tool.run(arguments={"optional_param": 20})
|
||||
assert result[0].text == "25" # type: ignore
|
||||
|
||||
|
||||
async def test_forward_with_argument_mapping(add_tool):
|
||||
"""Test that forward() applies argument mapping correctly."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue