Spaces:
Running
Running
Jeremiah Lowin Claude commited on
Commit ·
bf961cc
1
Parent(s): c32211c
Fix title field preservation in tool transformations
Browse filesAdd title parameter to Tool.from_tool() and TransformedTool.from_tool()
to properly inherit or override title field when transforming tools.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
src/fastmcp/tools/tool.py
CHANGED
|
@@ -185,8 +185,9 @@ class Tool(FastMCPComponent):
|
|
| 185 |
tool: Tool,
|
| 186 |
transform_fn: Callable[..., Any] | None = None,
|
| 187 |
name: str | None = None,
|
|
|
|
| 188 |
transform_args: dict[str, ArgTransform] | None = None,
|
| 189 |
-
description: str | None =
|
| 190 |
tags: set[str] | None = None,
|
| 191 |
annotations: ToolAnnotations | None = None,
|
| 192 |
output_schema: dict[str, Any] | None | Literal[False] = None,
|
|
@@ -199,6 +200,7 @@ class Tool(FastMCPComponent):
|
|
| 199 |
tool=tool,
|
| 200 |
transform_fn=transform_fn,
|
| 201 |
name=name,
|
|
|
|
| 202 |
transform_args=transform_args,
|
| 203 |
description=description,
|
| 204 |
tags=tags,
|
|
|
|
| 185 |
tool: Tool,
|
| 186 |
transform_fn: Callable[..., Any] | None = None,
|
| 187 |
name: str | None = None,
|
| 188 |
+
title: str | None | NotSetT = NotSet,
|
| 189 |
transform_args: dict[str, ArgTransform] | None = None,
|
| 190 |
+
description: str | None | NotSetT = NotSet,
|
| 191 |
tags: set[str] | None = None,
|
| 192 |
annotations: ToolAnnotations | None = None,
|
| 193 |
output_schema: dict[str, Any] | None | Literal[False] = None,
|
|
|
|
| 200 |
tool=tool,
|
| 201 |
transform_fn=transform_fn,
|
| 202 |
name=name,
|
| 203 |
+
title=title,
|
| 204 |
transform_args=transform_args,
|
| 205 |
description=description,
|
| 206 |
tags=tags,
|
src/fastmcp/tools/tool_transform.py
CHANGED
|
@@ -325,7 +325,8 @@ class TransformedTool(Tool):
|
|
| 325 |
cls,
|
| 326 |
tool: Tool,
|
| 327 |
name: str | None = None,
|
| 328 |
-
|
|
|
|
| 329 |
tags: set[str] | None = None,
|
| 330 |
transform_fn: Callable[..., Any] | None = None,
|
| 331 |
transform_args: dict[str, ArgTransform] | None = None,
|
|
@@ -342,6 +343,7 @@ class TransformedTool(Tool):
|
|
| 342 |
to call the parent tool. Functions with **kwargs receive transformed
|
| 343 |
argument names.
|
| 344 |
name: New name for the tool. Defaults to parent tool's name.
|
|
|
|
| 345 |
transform_args: Optional transformations for parent tool arguments.
|
| 346 |
Only specified arguments are transformed, others pass through unchanged:
|
| 347 |
- Simple rename (str)
|
|
@@ -506,13 +508,18 @@ class TransformedTool(Tool):
|
|
| 506 |
f"{', '.join(sorted(duplicates))}"
|
| 507 |
)
|
| 508 |
|
| 509 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
transformed_tool = cls(
|
| 512 |
fn=final_fn,
|
| 513 |
forwarding_fn=forwarding_fn,
|
| 514 |
parent_tool=tool,
|
| 515 |
-
name=
|
|
|
|
| 516 |
description=final_description,
|
| 517 |
parameters=final_schema,
|
| 518 |
output_schema=final_output_schema,
|
|
|
|
| 325 |
cls,
|
| 326 |
tool: Tool,
|
| 327 |
name: str | None = None,
|
| 328 |
+
title: str | None | NotSetT = NotSet,
|
| 329 |
+
description: str | None | NotSetT = NotSet,
|
| 330 |
tags: set[str] | None = None,
|
| 331 |
transform_fn: Callable[..., Any] | None = None,
|
| 332 |
transform_args: dict[str, ArgTransform] | None = None,
|
|
|
|
| 343 |
to call the parent tool. Functions with **kwargs receive transformed
|
| 344 |
argument names.
|
| 345 |
name: New name for the tool. Defaults to parent tool's name.
|
| 346 |
+
title: New title for the tool. Defaults to parent tool's title.
|
| 347 |
transform_args: Optional transformations for parent tool arguments.
|
| 348 |
Only specified arguments are transformed, others pass through unchanged:
|
| 349 |
- Simple rename (str)
|
|
|
|
| 508 |
f"{', '.join(sorted(duplicates))}"
|
| 509 |
)
|
| 510 |
|
| 511 |
+
final_name = name or tool.name
|
| 512 |
+
final_description = (
|
| 513 |
+
description if not isinstance(description, NotSetT) else tool.description
|
| 514 |
+
)
|
| 515 |
+
final_title = title if not isinstance(title, NotSetT) else tool.title
|
| 516 |
|
| 517 |
transformed_tool = cls(
|
| 518 |
fn=final_fn,
|
| 519 |
forwarding_fn=forwarding_fn,
|
| 520 |
parent_tool=tool,
|
| 521 |
+
name=final_name,
|
| 522 |
+
title=final_title,
|
| 523 |
description=final_description,
|
| 524 |
parameters=final_schema,
|
| 525 |
output_schema=final_output_schema,
|
tests/tools/test_tool_transform.py
CHANGED
|
@@ -1316,3 +1316,88 @@ class TestTransformToolOutputSchema:
|
|
| 1316 |
# Should use ToolResult content directly
|
| 1317 |
assert result.content[0].text == "Direct: 6" # type: ignore[attr-defined]
|
| 1318 |
assert result.structured_content == {"direct_value": 6, "doubled": 12}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1316 |
# Should use ToolResult content directly
|
| 1317 |
assert result.content[0].text == "Direct: 6" # type: ignore[attr-defined]
|
| 1318 |
assert result.structured_content == {"direct_value": 6, "doubled": 12}
|
| 1319 |
+
|
| 1320 |
+
|
| 1321 |
+
@pytest.fixture
|
| 1322 |
+
def sample_tool():
|
| 1323 |
+
"""Sample tool for testing transformations."""
|
| 1324 |
+
|
| 1325 |
+
def sample_func(x: int) -> str:
|
| 1326 |
+
return f"Result: {x}"
|
| 1327 |
+
|
| 1328 |
+
return Tool.from_function(
|
| 1329 |
+
sample_func,
|
| 1330 |
+
name="sample_tool",
|
| 1331 |
+
title="Original Tool Title",
|
| 1332 |
+
description="Original description",
|
| 1333 |
+
)
|
| 1334 |
+
|
| 1335 |
+
|
| 1336 |
+
@pytest.fixture
|
| 1337 |
+
def sample_tool_no_title():
|
| 1338 |
+
"""Sample tool without title for testing."""
|
| 1339 |
+
|
| 1340 |
+
def sample_func(x: int) -> str:
|
| 1341 |
+
return f"Result: {x}"
|
| 1342 |
+
|
| 1343 |
+
return Tool.from_function(sample_func, name="no_title_tool")
|
| 1344 |
+
|
| 1345 |
+
|
| 1346 |
+
def test_transform_inherits_title(sample_tool):
|
| 1347 |
+
"""Test that transformed tools inherit title when none specified."""
|
| 1348 |
+
transformed = Tool.from_tool(sample_tool)
|
| 1349 |
+
assert transformed.title == "Original Tool Title"
|
| 1350 |
+
|
| 1351 |
+
|
| 1352 |
+
def test_transform_overrides_title(sample_tool):
|
| 1353 |
+
"""Test that transformed tools can override title."""
|
| 1354 |
+
transformed = Tool.from_tool(sample_tool, title="New Tool Title")
|
| 1355 |
+
assert transformed.title == "New Tool Title"
|
| 1356 |
+
|
| 1357 |
+
|
| 1358 |
+
def test_transform_sets_title_to_none(sample_tool):
|
| 1359 |
+
"""Test that transformed tools can explicitly set title to None."""
|
| 1360 |
+
transformed = Tool.from_tool(sample_tool, title=None)
|
| 1361 |
+
assert transformed.title is None
|
| 1362 |
+
|
| 1363 |
+
|
| 1364 |
+
def test_transform_inherits_none_title(sample_tool_no_title):
|
| 1365 |
+
"""Test that transformed tools inherit None title."""
|
| 1366 |
+
transformed = Tool.from_tool(sample_tool_no_title)
|
| 1367 |
+
assert transformed.title is None
|
| 1368 |
+
|
| 1369 |
+
|
| 1370 |
+
def test_transform_adds_title_to_none(sample_tool_no_title):
|
| 1371 |
+
"""Test that transformed tools can add title when parent has None."""
|
| 1372 |
+
transformed = Tool.from_tool(sample_tool_no_title, title="Added Title")
|
| 1373 |
+
assert transformed.title == "Added Title"
|
| 1374 |
+
|
| 1375 |
+
|
| 1376 |
+
def test_transform_inherits_description(sample_tool):
|
| 1377 |
+
"""Test that transformed tools inherit description when none specified."""
|
| 1378 |
+
transformed = Tool.from_tool(sample_tool)
|
| 1379 |
+
assert transformed.description == "Original description"
|
| 1380 |
+
|
| 1381 |
+
|
| 1382 |
+
def test_transform_overrides_description(sample_tool):
|
| 1383 |
+
"""Test that transformed tools can override description."""
|
| 1384 |
+
transformed = Tool.from_tool(sample_tool, description="New description")
|
| 1385 |
+
assert transformed.description == "New description"
|
| 1386 |
+
|
| 1387 |
+
|
| 1388 |
+
def test_transform_sets_description_to_none(sample_tool):
|
| 1389 |
+
"""Test that transformed tools can explicitly set description to None."""
|
| 1390 |
+
transformed = Tool.from_tool(sample_tool, description=None)
|
| 1391 |
+
assert transformed.description is None
|
| 1392 |
+
|
| 1393 |
+
|
| 1394 |
+
def test_transform_inherits_none_description(sample_tool_no_title):
|
| 1395 |
+
"""Test that transformed tools inherit None description."""
|
| 1396 |
+
transformed = Tool.from_tool(sample_tool_no_title)
|
| 1397 |
+
assert transformed.description is None
|
| 1398 |
+
|
| 1399 |
+
|
| 1400 |
+
def test_transform_adds_description_to_none(sample_tool_no_title):
|
| 1401 |
+
"""Test that transformed tools can add description when parent has None."""
|
| 1402 |
+
transformed = Tool.from_tool(sample_tool_no_title, description="Added description")
|
| 1403 |
+
assert transformed.description == "Added description"
|