diff --git a/docs/patterns/tool-transformation.mdx b/docs/patterns/tool-transformation.mdx index 753a28765..3d7c5bd43 100644 --- a/docs/patterns/tool-transformation.mdx +++ b/docs/patterns/tool-transformation.mdx @@ -92,6 +92,7 @@ The `Tool.from_tool()` class method is the primary way to create a transformed t - `tags`: An optional set of tags for the new tool. - `annotations`: An optional set of `ToolAnnotations` for the new tool. - `serializer`: An optional function that will be called to serialize the result of the new tool. +- `meta`: Control meta information for the tool. Use `None` to remove meta, any dict to set meta, or leave unset to inherit from parent. The result is a new `TransformedTool` object that wraps the parent tool and applies the transformations you specify. You can add this tool to your MCP server using its `add_tool()` method. @@ -255,6 +256,50 @@ transform_args = { `default_factory` can only be used with `hide=True`. This is because visible parameters need static defaults that can be represented in a JSON schema for the client. +### Meta Information + + + +You can control meta information on transformed tools using the `meta` parameter. Meta information is additional data about the tool that doesn't affect its functionality but can be used by clients for categorization, routing, or other purposes. + +```python {15-17} +from fastmcp import FastMCP +from fastmcp.tools import Tool + +mcp = FastMCP() + +@mcp.tool +def analyze_data(data: str) -> dict: + """Analyzes the provided data.""" + return {"result": f"Analysis of {data}"} + +# Add custom meta information +enhanced_tool = Tool.from_tool( + analyze_data, + name="enhanced_analyzer", + meta={ + "category": "analytics", + "priority": "high", + "requires_auth": True + } +) + +mcp.add_tool(enhanced_tool) +``` + +You can also remove meta information entirely: + +```python {6} +# Remove meta information from parent tool +simplified_tool = Tool.from_tool( + analyze_data, + name="simple_analyzer", + meta=None # Removes any meta information +) +``` + +If you don't specify the `meta` parameter, the transformed tool inherits the parent tool's meta information. + ### Required Values In rare cases where you want to make an optional argument required, you can set `required=True`. This has no effect if the argument was already required. @@ -456,6 +501,10 @@ directly in the MCPConfig json file. "weather_get_forecast": { "name": "miami_weather", "description": "Get the weather for Miami", + "meta": { + "category": "weather", + "location": "miami" + }, "arguments": { "city": { "name": "city", diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index 0377027c6..eab2df209 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -208,6 +208,7 @@ class Tool(FastMCPComponent): annotations: ToolAnnotations | None = None, output_schema: dict[str, Any] | None | Literal[False] = None, serializer: Callable[[Any], str] | None = None, + meta: dict[str, Any] | None | NotSetT = NotSet, enabled: bool | None = None, ) -> TransformedTool: from fastmcp.tools.tool_transform import TransformedTool @@ -223,6 +224,7 @@ class Tool(FastMCPComponent): annotations=annotations, output_schema=output_schema, serializer=serializer, + meta=meta, enabled=enabled, ) diff --git a/src/fastmcp/tools/tool_transform.py b/src/fastmcp/tools/tool_transform.py index 61ecf6907..a51237196 100644 --- a/src/fastmcp/tools/tool_transform.py +++ b/src/fastmcp/tools/tool_transform.py @@ -366,6 +366,7 @@ class TransformedTool(Tool): annotations: ToolAnnotations | None = None, output_schema: dict[str, Any] | None | Literal[False] = None, serializer: Callable[[Any], str] | None = None, + meta: dict[str, Any] | None | NotSetT = NotSet, enabled: bool | None = None, ) -> TransformedTool: """Create a transformed tool from a parent tool. @@ -390,6 +391,10 @@ class TransformedTool(Tool): - dict: Use custom output schema - False: Disable output schema and structured outputs serializer: New serializer. Defaults to parent's serializer. + meta: Control meta information: + - NotSet (default): Inherit from parent tool + - dict: Use custom meta information + - None: Remove meta information Returns: TransformedTool with the specified transformations. @@ -546,6 +551,7 @@ class TransformedTool(Tool): description if not isinstance(description, NotSetT) else tool.description ) final_title = title if not isinstance(title, NotSetT) else tool.title + final_meta = meta if not isinstance(meta, NotSetT) else tool.meta transformed_tool = cls( fn=final_fn, @@ -559,6 +565,7 @@ class TransformedTool(Tool): tags=tags or tool.tags, annotations=annotations or tool.annotations, serializer=serializer or tool.serializer, + meta=final_meta, transform_args=transform_args, enabled=enabled if enabled is not None else True, ) @@ -851,6 +858,10 @@ class ToolTransformConfig(FastMCPBaseModel): default_factory=set, description="The new tags for the tool.", ) + meta: dict[str, Any] | None = Field( + default=None, + description="The new meta information for the tool.", + ) enabled: bool = Field( default=True, diff --git a/tests/tools/test_tool_transform.py b/tests/tools/test_tool_transform.py index f498d01ad..6481791cc 100644 --- a/tests/tools/test_tool_transform.py +++ b/tests/tools/test_tool_transform.py @@ -13,7 +13,11 @@ from fastmcp.client.client import Client from fastmcp.exceptions import ToolError from fastmcp.tools import Tool, forward, forward_raw from fastmcp.tools.tool import FunctionTool, ToolResult -from fastmcp.tools.tool_transform import ArgTransform, TransformedTool +from fastmcp.tools.tool_transform import ( + ArgTransform, + ToolTransformConfig, + TransformedTool, +) def get_property(tool: Tool, name: str) -> dict[str, Any]: @@ -1428,3 +1432,65 @@ def test_transform_adds_description_to_none(sample_tool_no_title): """Test that transformed tools can add description when parent has None.""" transformed = Tool.from_tool(sample_tool_no_title, description="Added description") assert transformed.description == "Added description" + + +# Meta transformation tests +def test_transform_inherits_meta(sample_tool): + """Test that transformed tools inherit meta when none specified.""" + sample_tool.meta = {"original": True, "version": "1.0"} + transformed = Tool.from_tool(sample_tool) + assert transformed.meta == {"original": True, "version": "1.0"} + + +def test_transform_overrides_meta(sample_tool): + """Test that transformed tools can override meta.""" + sample_tool.meta = {"original": True, "version": "1.0"} + transformed = Tool.from_tool(sample_tool, meta={"custom": True, "priority": "high"}) + assert transformed.meta == {"custom": True, "priority": "high"} + + +def test_transform_sets_meta_to_none(sample_tool): + """Test that transformed tools can explicitly set meta to None.""" + sample_tool.meta = {"original": True, "version": "1.0"} + transformed = Tool.from_tool(sample_tool, meta=None) + assert transformed.meta is None + + +def test_transform_inherits_none_meta(sample_tool_no_title): + """Test that transformed tools inherit None meta.""" + sample_tool_no_title.meta = None + transformed = Tool.from_tool(sample_tool_no_title) + assert transformed.meta is None + + +def test_transform_adds_meta_to_none(sample_tool_no_title): + """Test that transformed tools can add meta when parent has None.""" + sample_tool_no_title.meta = None + transformed = Tool.from_tool(sample_tool_no_title, meta={"added": True}) + assert transformed.meta == {"added": True} + + +def test_tool_transform_config_inherits_meta(sample_tool): + """Test that ToolTransformConfig inherits meta when unset.""" + sample_tool.meta = {"original": True, "version": "1.0"} + config = ToolTransformConfig(name="config_tool") + transformed = config.apply(sample_tool) + assert transformed.meta == {"original": True, "version": "1.0"} + + +def test_tool_transform_config_overrides_meta(sample_tool): + """Test that ToolTransformConfig can override meta.""" + sample_tool.meta = {"original": True, "version": "1.0"} + config = ToolTransformConfig( + name="config_tool", meta={"config": True, "priority": "high"} + ) + transformed = config.apply(sample_tool) + assert transformed.meta == {"config": True, "priority": "high"} + + +def test_tool_transform_config_removes_meta(sample_tool): + """Test that ToolTransformConfig can remove meta with None.""" + sample_tool.meta = {"original": True, "version": "1.0"} + config = ToolTransformConfig(name="config_tool", meta=None) + transformed = config.apply(sample_tool) + assert transformed.meta is None