mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-18 11:39:12 +02:00
Add enabled field to ToolTransformConfig (#2991)
This commit is contained in:
parent
b77484e1a7
commit
c1f50e49be
4 changed files with 198 additions and 2 deletions
|
|
@ -93,6 +93,7 @@ Both mechanisms support the same modifications.
|
|||
| `tags` | Set of tags for categorization |
|
||||
| `annotations` | MCP ToolAnnotations |
|
||||
| `meta` | Custom metadata dictionary |
|
||||
| `enabled` | Whether the tool is visible to clients (default `True`) |
|
||||
|
||||
**Argument-level options** (via `ArgTransform` or `ArgTransformConfig`):
|
||||
|
||||
|
|
|
|||
|
|
@ -892,6 +892,29 @@ class TransformedTool(Tool):
|
|||
)
|
||||
|
||||
|
||||
def _set_visibility_metadata(tool: Tool, *, enabled: bool) -> None:
|
||||
"""Set visibility state in tool metadata.
|
||||
|
||||
This uses the same metadata format as the Visibility transform,
|
||||
so tools marked here will be filtered by the standard visibility system.
|
||||
|
||||
Args:
|
||||
tool: Tool to mark.
|
||||
enabled: Whether the tool should be visible to clients.
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from fastmcp.server.transforms.visibility import _FASTMCP_KEY, _INTERNAL_KEY
|
||||
|
||||
if tool.meta is None:
|
||||
tool.meta = {_FASTMCP_KEY: {_INTERNAL_KEY: {"visibility": enabled}}}
|
||||
else:
|
||||
old_fastmcp = tool.meta.get(_FASTMCP_KEY, {})
|
||||
old_internal = old_fastmcp.get(_INTERNAL_KEY, {})
|
||||
new_internal = {**old_internal, "visibility": enabled}
|
||||
new_fastmcp = {**old_fastmcp, _INTERNAL_KEY: new_internal}
|
||||
tool.meta = {**tool.meta, _FASTMCP_KEY: new_fastmcp}
|
||||
|
||||
|
||||
class ToolTransformConfig(FastMCPBaseModel):
|
||||
"""Provides a way to transform a tool."""
|
||||
|
||||
|
|
@ -915,6 +938,10 @@ class ToolTransformConfig(FastMCPBaseModel):
|
|||
default=None,
|
||||
description="The new meta information for the tool.",
|
||||
)
|
||||
enabled: bool = Field(
|
||||
default=True,
|
||||
description="Whether the tool is enabled. If False, the tool will be hidden from clients.",
|
||||
)
|
||||
|
||||
arguments: dict[str, ArgTransformConfig] = Field(
|
||||
default_factory=dict,
|
||||
|
|
@ -925,15 +952,22 @@ class ToolTransformConfig(FastMCPBaseModel):
|
|||
"""Create a TransformedTool from a provided tool and this transformation configuration."""
|
||||
|
||||
tool_changes: dict[str, Any] = self.model_dump(
|
||||
exclude_unset=True, exclude={"arguments"}
|
||||
exclude_unset=True, exclude={"arguments", "enabled"}
|
||||
)
|
||||
|
||||
return TransformedTool.from_tool(
|
||||
transformed = TransformedTool.from_tool(
|
||||
tool=tool,
|
||||
**tool_changes,
|
||||
transform_args={k: v.to_arg_transform() for k, v in self.arguments.items()},
|
||||
)
|
||||
|
||||
# Set visibility metadata if enabled was explicitly provided.
|
||||
# This allows enabled=True to override an earlier disable (later transforms win).
|
||||
if "enabled" in self.model_fields_set:
|
||||
_set_visibility_metadata(transformed, enabled=self.enabled)
|
||||
|
||||
return transformed
|
||||
|
||||
|
||||
def apply_transformations_to_tools(
|
||||
tools: dict[str, Tool],
|
||||
|
|
|
|||
|
|
@ -118,3 +118,92 @@ async def test_server_level_transforms_apply_to_mounted_servers():
|
|||
|
||||
assert "renamed_sub_tool" in tool_names
|
||||
assert "sub_tool" not in tool_names
|
||||
|
||||
|
||||
async def test_tool_transform_config_enabled_false_hides_tool():
|
||||
"""Test that ToolTransformConfig with enabled=False hides the tool from list_tools."""
|
||||
mcp = FastMCP("Test Server")
|
||||
|
||||
@mcp.tool()
|
||||
def visible_tool() -> str:
|
||||
return "visible"
|
||||
|
||||
@mcp.tool()
|
||||
def hidden_tool() -> str:
|
||||
return "hidden"
|
||||
|
||||
# Disable one tool via transformation
|
||||
mcp.add_transform(
|
||||
ToolTransform({"hidden_tool": ToolTransformConfig(enabled=False)})
|
||||
)
|
||||
|
||||
tools = await mcp.list_tools()
|
||||
tool_names = [t.name for t in tools]
|
||||
|
||||
assert "visible_tool" in tool_names
|
||||
assert "hidden_tool" not in tool_names
|
||||
|
||||
|
||||
async def test_tool_transform_config_enabled_false_with_rename():
|
||||
"""Test that enabled=False works together with other transformations like rename."""
|
||||
mcp = FastMCP("Test Server")
|
||||
|
||||
@mcp.tool()
|
||||
def my_tool() -> str:
|
||||
return "result"
|
||||
|
||||
# Rename AND disable
|
||||
mcp.add_transform(
|
||||
ToolTransform(
|
||||
{"my_tool": ToolTransformConfig(name="renamed_and_disabled", enabled=False)}
|
||||
)
|
||||
)
|
||||
|
||||
tools = await mcp.list_tools()
|
||||
tool_names = [t.name for t in tools]
|
||||
|
||||
# Tool should be hidden regardless of rename
|
||||
assert "my_tool" not in tool_names
|
||||
assert "renamed_and_disabled" not in tool_names
|
||||
|
||||
|
||||
async def test_tool_transform_config_enabled_true_keeps_tool_visible():
|
||||
"""Test that ToolTransformConfig with enabled=True (explicit) keeps the tool visible."""
|
||||
mcp = FastMCP("Test Server")
|
||||
|
||||
@mcp.tool()
|
||||
def my_tool() -> str:
|
||||
return "result"
|
||||
|
||||
# Explicitly set enabled=True (should be same as default)
|
||||
mcp.add_transform(ToolTransform({"my_tool": ToolTransformConfig(enabled=True)}))
|
||||
|
||||
tools = await mcp.list_tools()
|
||||
tool_names = [t.name for t in tools]
|
||||
|
||||
assert "my_tool" in tool_names
|
||||
|
||||
|
||||
async def test_tool_transform_config_enabled_true_overrides_earlier_disable():
|
||||
"""Test that ToolTransformConfig with enabled=True can re-enable a previously disabled tool."""
|
||||
mcp = FastMCP("Test Server")
|
||||
|
||||
@mcp.tool()
|
||||
def my_tool() -> str:
|
||||
return "result"
|
||||
|
||||
# Disable the tool first
|
||||
mcp.disable(names={"my_tool"})
|
||||
|
||||
# Verify tool is initially hidden
|
||||
tools = await mcp.list_tools()
|
||||
assert "my_tool" not in [t.name for t in tools]
|
||||
|
||||
# Re-enable via transformation (later transforms win)
|
||||
mcp.add_transform(ToolTransform({"my_tool": ToolTransformConfig(enabled=True)}))
|
||||
|
||||
tools = await mcp.list_tools()
|
||||
tool_names = [t.name for t in tools]
|
||||
|
||||
# Tool should now be visible
|
||||
assert "my_tool" in tool_names
|
||||
|
|
|
|||
|
|
@ -170,3 +170,75 @@ def test_tool_transform_config_removes_meta(sample_tool):
|
|||
config = ToolTransformConfig(name="config_tool", meta=None)
|
||||
transformed = config.apply(sample_tool)
|
||||
assert transformed.meta is None
|
||||
|
||||
|
||||
# Enabled field tests
|
||||
def test_tool_transform_config_enabled_defaults_to_true(sample_tool):
|
||||
"""Test that enabled defaults to True and no visibility metadata is set."""
|
||||
config = ToolTransformConfig(name="enabled_tool")
|
||||
transformed = config.apply(sample_tool)
|
||||
|
||||
# No visibility metadata should be set when enabled=True (default)
|
||||
meta = transformed.meta or {}
|
||||
internal = meta.get("fastmcp", {}).get("_internal", {})
|
||||
assert "visibility" not in internal
|
||||
|
||||
|
||||
def test_tool_transform_config_enabled_false_sets_visibility_metadata(sample_tool):
|
||||
"""Test that enabled=False sets visibility metadata to hide the tool."""
|
||||
from fastmcp.server.transforms.visibility import is_enabled
|
||||
|
||||
config = ToolTransformConfig(name="disabled_tool", enabled=False)
|
||||
transformed = config.apply(sample_tool)
|
||||
|
||||
# The is_enabled helper should return False
|
||||
assert is_enabled(transformed) is False
|
||||
|
||||
# Check the raw metadata structure
|
||||
assert transformed.meta is not None
|
||||
assert transformed.meta["fastmcp"]["_internal"]["visibility"] is False
|
||||
|
||||
|
||||
def test_tool_transform_config_enabled_true_explicit_sets_visibility(sample_tool):
|
||||
"""Test that enabled=True explicitly sets visibility metadata to allow overriding earlier disables."""
|
||||
from fastmcp.server.transforms.visibility import is_enabled
|
||||
|
||||
config = ToolTransformConfig(name="explicit_enabled", enabled=True)
|
||||
transformed = config.apply(sample_tool)
|
||||
|
||||
# Visibility metadata should be set to True when enabled=True is explicit
|
||||
# This allows later transforms to override earlier disables
|
||||
assert is_enabled(transformed) is True
|
||||
assert transformed.meta is not None
|
||||
assert transformed.meta["fastmcp"]["_internal"]["visibility"] is True
|
||||
|
||||
|
||||
def test_tool_transform_config_enabled_false_preserves_existing_meta(sample_tool):
|
||||
"""Test that enabled=False preserves existing meta while adding visibility."""
|
||||
from fastmcp.server.transforms.visibility import is_enabled
|
||||
|
||||
sample_tool.meta = {"custom_key": "custom_value"}
|
||||
config = ToolTransformConfig(enabled=False)
|
||||
transformed = config.apply(sample_tool)
|
||||
|
||||
# Original meta should be preserved
|
||||
assert transformed.meta is not None
|
||||
assert transformed.meta["custom_key"] == "custom_value"
|
||||
# Visibility should be set
|
||||
assert is_enabled(transformed) is False
|
||||
|
||||
|
||||
def test_tool_transform_config_enabled_false_merges_with_config_meta(sample_tool):
|
||||
"""Test that enabled=False works with explicit meta override."""
|
||||
from fastmcp.server.transforms.visibility import is_enabled
|
||||
|
||||
sample_tool.meta = {"original": True}
|
||||
config = ToolTransformConfig(meta={"overridden": True}, enabled=False)
|
||||
transformed = config.apply(sample_tool)
|
||||
|
||||
# Config meta should override original
|
||||
assert transformed.meta is not None
|
||||
assert "original" not in transformed.meta
|
||||
assert transformed.meta["overridden"] is True
|
||||
# But visibility should still be set
|
||||
assert is_enabled(transformed) is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue