Spaces:
Running
Running
Jeremiah Lowin commited on
Add meta support to tool transformation utilities (#1295)
Browse files
docs/patterns/tool-transformation.mdx
CHANGED
|
@@ -92,6 +92,7 @@ The `Tool.from_tool()` class method is the primary way to create a transformed t
|
|
| 92 |
- `tags`: An optional set of tags for the new tool.
|
| 93 |
- `annotations`: An optional set of `ToolAnnotations` for the new tool.
|
| 94 |
- `serializer`: An optional function that will be called to serialize the result of the new tool.
|
|
|
|
| 95 |
|
| 96 |
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.
|
| 97 |
|
|
@@ -255,6 +256,50 @@ transform_args = {
|
|
| 255 |
`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.
|
| 256 |
</Warning>
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
### Required Values
|
| 259 |
|
| 260 |
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.
|
|
| 456 |
"weather_get_forecast": {
|
| 457 |
"name": "miami_weather",
|
| 458 |
"description": "Get the weather for Miami",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
"arguments": {
|
| 460 |
"city": {
|
| 461 |
"name": "city",
|
|
|
|
| 92 |
- `tags`: An optional set of tags for the new tool.
|
| 93 |
- `annotations`: An optional set of `ToolAnnotations` for the new tool.
|
| 94 |
- `serializer`: An optional function that will be called to serialize the result of the new tool.
|
| 95 |
+
- `meta`: Control meta information for the tool. Use `None` to remove meta, any dict to set meta, or leave unset to inherit from parent.
|
| 96 |
|
| 97 |
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.
|
| 98 |
|
|
|
|
| 256 |
`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.
|
| 257 |
</Warning>
|
| 258 |
|
| 259 |
+
### Meta Information
|
| 260 |
+
|
| 261 |
+
<VersionBadge version="2.11.0" />
|
| 262 |
+
|
| 263 |
+
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.
|
| 264 |
+
|
| 265 |
+
```python {15-17}
|
| 266 |
+
from fastmcp import FastMCP
|
| 267 |
+
from fastmcp.tools import Tool
|
| 268 |
+
|
| 269 |
+
mcp = FastMCP()
|
| 270 |
+
|
| 271 |
+
@mcp.tool
|
| 272 |
+
def analyze_data(data: str) -> dict:
|
| 273 |
+
"""Analyzes the provided data."""
|
| 274 |
+
return {"result": f"Analysis of {data}"}
|
| 275 |
+
|
| 276 |
+
# Add custom meta information
|
| 277 |
+
enhanced_tool = Tool.from_tool(
|
| 278 |
+
analyze_data,
|
| 279 |
+
name="enhanced_analyzer",
|
| 280 |
+
meta={
|
| 281 |
+
"category": "analytics",
|
| 282 |
+
"priority": "high",
|
| 283 |
+
"requires_auth": True
|
| 284 |
+
}
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
mcp.add_tool(enhanced_tool)
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
You can also remove meta information entirely:
|
| 291 |
+
|
| 292 |
+
```python {6}
|
| 293 |
+
# Remove meta information from parent tool
|
| 294 |
+
simplified_tool = Tool.from_tool(
|
| 295 |
+
analyze_data,
|
| 296 |
+
name="simple_analyzer",
|
| 297 |
+
meta=None # Removes any meta information
|
| 298 |
+
)
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
If you don't specify the `meta` parameter, the transformed tool inherits the parent tool's meta information.
|
| 302 |
+
|
| 303 |
### Required Values
|
| 304 |
|
| 305 |
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.
|
|
|
|
| 501 |
"weather_get_forecast": {
|
| 502 |
"name": "miami_weather",
|
| 503 |
"description": "Get the weather for Miami",
|
| 504 |
+
"meta": {
|
| 505 |
+
"category": "weather",
|
| 506 |
+
"location": "miami"
|
| 507 |
+
},
|
| 508 |
"arguments": {
|
| 509 |
"city": {
|
| 510 |
"name": "city",
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -208,6 +208,7 @@ class Tool(FastMCPComponent):
|
|
| 208 |
annotations: ToolAnnotations | None = None,
|
| 209 |
output_schema: dict[str, Any] | None | Literal[False] = None,
|
| 210 |
serializer: Callable[[Any], str] | None = None,
|
|
|
|
| 211 |
enabled: bool | None = None,
|
| 212 |
) -> TransformedTool:
|
| 213 |
from fastmcp.tools.tool_transform import TransformedTool
|
|
@@ -223,6 +224,7 @@ class Tool(FastMCPComponent):
|
|
| 223 |
annotations=annotations,
|
| 224 |
output_schema=output_schema,
|
| 225 |
serializer=serializer,
|
|
|
|
| 226 |
enabled=enabled,
|
| 227 |
)
|
| 228 |
|
|
|
|
| 208 |
annotations: ToolAnnotations | None = None,
|
| 209 |
output_schema: dict[str, Any] | None | Literal[False] = None,
|
| 210 |
serializer: Callable[[Any], str] | None = None,
|
| 211 |
+
meta: dict[str, Any] | None | NotSetT = NotSet,
|
| 212 |
enabled: bool | None = None,
|
| 213 |
) -> TransformedTool:
|
| 214 |
from fastmcp.tools.tool_transform import TransformedTool
|
|
|
|
| 224 |
annotations=annotations,
|
| 225 |
output_schema=output_schema,
|
| 226 |
serializer=serializer,
|
| 227 |
+
meta=meta,
|
| 228 |
enabled=enabled,
|
| 229 |
)
|
| 230 |
|
src/fastmcp/tools/tool_transform.py
CHANGED
|
@@ -366,6 +366,7 @@ class TransformedTool(Tool):
|
|
| 366 |
annotations: ToolAnnotations | None = None,
|
| 367 |
output_schema: dict[str, Any] | None | Literal[False] = None,
|
| 368 |
serializer: Callable[[Any], str] | None = None,
|
|
|
|
| 369 |
enabled: bool | None = None,
|
| 370 |
) -> TransformedTool:
|
| 371 |
"""Create a transformed tool from a parent tool.
|
|
@@ -390,6 +391,10 @@ class TransformedTool(Tool):
|
|
| 390 |
- dict: Use custom output schema
|
| 391 |
- False: Disable output schema and structured outputs
|
| 392 |
serializer: New serializer. Defaults to parent's serializer.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
|
| 394 |
Returns:
|
| 395 |
TransformedTool with the specified transformations.
|
|
@@ -546,6 +551,7 @@ class TransformedTool(Tool):
|
|
| 546 |
description if not isinstance(description, NotSetT) else tool.description
|
| 547 |
)
|
| 548 |
final_title = title if not isinstance(title, NotSetT) else tool.title
|
|
|
|
| 549 |
|
| 550 |
transformed_tool = cls(
|
| 551 |
fn=final_fn,
|
|
@@ -559,6 +565,7 @@ class TransformedTool(Tool):
|
|
| 559 |
tags=tags or tool.tags,
|
| 560 |
annotations=annotations or tool.annotations,
|
| 561 |
serializer=serializer or tool.serializer,
|
|
|
|
| 562 |
transform_args=transform_args,
|
| 563 |
enabled=enabled if enabled is not None else True,
|
| 564 |
)
|
|
@@ -851,6 +858,10 @@ class ToolTransformConfig(FastMCPBaseModel):
|
|
| 851 |
default_factory=set,
|
| 852 |
description="The new tags for the tool.",
|
| 853 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 854 |
|
| 855 |
enabled: bool = Field(
|
| 856 |
default=True,
|
|
|
|
| 366 |
annotations: ToolAnnotations | None = None,
|
| 367 |
output_schema: dict[str, Any] | None | Literal[False] = None,
|
| 368 |
serializer: Callable[[Any], str] | None = None,
|
| 369 |
+
meta: dict[str, Any] | None | NotSetT = NotSet,
|
| 370 |
enabled: bool | None = None,
|
| 371 |
) -> TransformedTool:
|
| 372 |
"""Create a transformed tool from a parent tool.
|
|
|
|
| 391 |
- dict: Use custom output schema
|
| 392 |
- False: Disable output schema and structured outputs
|
| 393 |
serializer: New serializer. Defaults to parent's serializer.
|
| 394 |
+
meta: Control meta information:
|
| 395 |
+
- NotSet (default): Inherit from parent tool
|
| 396 |
+
- dict: Use custom meta information
|
| 397 |
+
- None: Remove meta information
|
| 398 |
|
| 399 |
Returns:
|
| 400 |
TransformedTool with the specified transformations.
|
|
|
|
| 551 |
description if not isinstance(description, NotSetT) else tool.description
|
| 552 |
)
|
| 553 |
final_title = title if not isinstance(title, NotSetT) else tool.title
|
| 554 |
+
final_meta = meta if not isinstance(meta, NotSetT) else tool.meta
|
| 555 |
|
| 556 |
transformed_tool = cls(
|
| 557 |
fn=final_fn,
|
|
|
|
| 565 |
tags=tags or tool.tags,
|
| 566 |
annotations=annotations or tool.annotations,
|
| 567 |
serializer=serializer or tool.serializer,
|
| 568 |
+
meta=final_meta,
|
| 569 |
transform_args=transform_args,
|
| 570 |
enabled=enabled if enabled is not None else True,
|
| 571 |
)
|
|
|
|
| 858 |
default_factory=set,
|
| 859 |
description="The new tags for the tool.",
|
| 860 |
)
|
| 861 |
+
meta: dict[str, Any] | None = Field(
|
| 862 |
+
default=None,
|
| 863 |
+
description="The new meta information for the tool.",
|
| 864 |
+
)
|
| 865 |
|
| 866 |
enabled: bool = Field(
|
| 867 |
default=True,
|
tests/tools/test_tool_transform.py
CHANGED
|
@@ -13,7 +13,11 @@ from fastmcp.client.client import Client
|
|
| 13 |
from fastmcp.exceptions import ToolError
|
| 14 |
from fastmcp.tools import Tool, forward, forward_raw
|
| 15 |
from fastmcp.tools.tool import FunctionTool, ToolResult
|
| 16 |
-
from fastmcp.tools.tool_transform import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
|
| 19 |
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):
|
|
| 1428 |
"""Test that transformed tools can add description when parent has None."""
|
| 1429 |
transformed = Tool.from_tool(sample_tool_no_title, description="Added description")
|
| 1430 |
assert transformed.description == "Added description"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
from fastmcp.exceptions import ToolError
|
| 14 |
from fastmcp.tools import Tool, forward, forward_raw
|
| 15 |
from fastmcp.tools.tool import FunctionTool, ToolResult
|
| 16 |
+
from fastmcp.tools.tool_transform import (
|
| 17 |
+
ArgTransform,
|
| 18 |
+
ToolTransformConfig,
|
| 19 |
+
TransformedTool,
|
| 20 |
+
)
|
| 21 |
|
| 22 |
|
| 23 |
def get_property(tool: Tool, name: str) -> dict[str, Any]:
|
|
|
|
| 1432 |
"""Test that transformed tools can add description when parent has None."""
|
| 1433 |
transformed = Tool.from_tool(sample_tool_no_title, description="Added description")
|
| 1434 |
assert transformed.description == "Added description"
|
| 1435 |
+
|
| 1436 |
+
|
| 1437 |
+
# Meta transformation tests
|
| 1438 |
+
def test_transform_inherits_meta(sample_tool):
|
| 1439 |
+
"""Test that transformed tools inherit meta when none specified."""
|
| 1440 |
+
sample_tool.meta = {"original": True, "version": "1.0"}
|
| 1441 |
+
transformed = Tool.from_tool(sample_tool)
|
| 1442 |
+
assert transformed.meta == {"original": True, "version": "1.0"}
|
| 1443 |
+
|
| 1444 |
+
|
| 1445 |
+
def test_transform_overrides_meta(sample_tool):
|
| 1446 |
+
"""Test that transformed tools can override meta."""
|
| 1447 |
+
sample_tool.meta = {"original": True, "version": "1.0"}
|
| 1448 |
+
transformed = Tool.from_tool(sample_tool, meta={"custom": True, "priority": "high"})
|
| 1449 |
+
assert transformed.meta == {"custom": True, "priority": "high"}
|
| 1450 |
+
|
| 1451 |
+
|
| 1452 |
+
def test_transform_sets_meta_to_none(sample_tool):
|
| 1453 |
+
"""Test that transformed tools can explicitly set meta to None."""
|
| 1454 |
+
sample_tool.meta = {"original": True, "version": "1.0"}
|
| 1455 |
+
transformed = Tool.from_tool(sample_tool, meta=None)
|
| 1456 |
+
assert transformed.meta is None
|
| 1457 |
+
|
| 1458 |
+
|
| 1459 |
+
def test_transform_inherits_none_meta(sample_tool_no_title):
|
| 1460 |
+
"""Test that transformed tools inherit None meta."""
|
| 1461 |
+
sample_tool_no_title.meta = None
|
| 1462 |
+
transformed = Tool.from_tool(sample_tool_no_title)
|
| 1463 |
+
assert transformed.meta is None
|
| 1464 |
+
|
| 1465 |
+
|
| 1466 |
+
def test_transform_adds_meta_to_none(sample_tool_no_title):
|
| 1467 |
+
"""Test that transformed tools can add meta when parent has None."""
|
| 1468 |
+
sample_tool_no_title.meta = None
|
| 1469 |
+
transformed = Tool.from_tool(sample_tool_no_title, meta={"added": True})
|
| 1470 |
+
assert transformed.meta == {"added": True}
|
| 1471 |
+
|
| 1472 |
+
|
| 1473 |
+
def test_tool_transform_config_inherits_meta(sample_tool):
|
| 1474 |
+
"""Test that ToolTransformConfig inherits meta when unset."""
|
| 1475 |
+
sample_tool.meta = {"original": True, "version": "1.0"}
|
| 1476 |
+
config = ToolTransformConfig(name="config_tool")
|
| 1477 |
+
transformed = config.apply(sample_tool)
|
| 1478 |
+
assert transformed.meta == {"original": True, "version": "1.0"}
|
| 1479 |
+
|
| 1480 |
+
|
| 1481 |
+
def test_tool_transform_config_overrides_meta(sample_tool):
|
| 1482 |
+
"""Test that ToolTransformConfig can override meta."""
|
| 1483 |
+
sample_tool.meta = {"original": True, "version": "1.0"}
|
| 1484 |
+
config = ToolTransformConfig(
|
| 1485 |
+
name="config_tool", meta={"config": True, "priority": "high"}
|
| 1486 |
+
)
|
| 1487 |
+
transformed = config.apply(sample_tool)
|
| 1488 |
+
assert transformed.meta == {"config": True, "priority": "high"}
|
| 1489 |
+
|
| 1490 |
+
|
| 1491 |
+
def test_tool_transform_config_removes_meta(sample_tool):
|
| 1492 |
+
"""Test that ToolTransformConfig can remove meta with None."""
|
| 1493 |
+
sample_tool.meta = {"original": True, "version": "1.0"}
|
| 1494 |
+
config = ToolTransformConfig(name="config_tool", meta=None)
|
| 1495 |
+
transformed = config.apply(sample_tool)
|
| 1496 |
+
assert transformed.meta is None
|