Skip to content

TransformedTool: sync transform_fn crash, shallow copy mutation, str/None shorthand broken #3821

@strawgate

Description

@strawgate

TransformedTool has four related issues in src/fastmcp/tools/tool_transform.py:

1. run() crashes on sync transform_fn (line 313)

run() unconditionally does await self.fn(**arguments). When transform_fn is a sync function, this raises TypeError: object str can't be used in 'await' expression. FunctionTool.run() handles this correctly with is_coroutine_function + call_sync_fn_in_threadpoolTransformedTool.run() should do the same.

from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool_transform import TransformedTool

def greet(name: str) -> str:
    return f"Hello, {name}!"

def upper_greet(name: str) -> str:
    return f"HELLO, {name}!"

parent = FunctionTool.from_function(greet)
transformed = TransformedTool.from_tool(parent, transform_fn=upper_greet)

import asyncio
asyncio.run(transformed.run({"name": "World"}))
# TypeError: object str can't be used in 'await' expression

2. from_tool() shallow-copies property schemas (line 634)

parent_props = parent_tool.parameters.get("properties", {}).copy() is a shallow copy. Nested schema dicts (like {"items": {"type": "string"}} inside an array property) are shared references between parent and transformed tool. Mutating the transformed schema corrupts the parent.

from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool_transform import TransformedTool, ArgTransform

def my_tool(items: list[str], count: int = 5) -> str:
    return f"{items[:count]}"

parent = FunctionTool.from_function(my_tool)
transformed = TransformedTool.from_tool(
    parent, transform_args={"items": ArgTransform(name="data")}
)

# Nested schemas are shared
print(parent.parameters["properties"]["items"]["items"]
      is transformed.parameters["properties"]["data"]["items"])  # True

# Mutating transformed corrupts parent
transformed.parameters["properties"]["data"]["items"]["type"] = "integer"
print(parent.parameters["properties"]["items"]["items"]["type"])  # "integer" — corrupted

$defs is already deepcopy'd (line 633) but properties is not.

3. from_tool() crashes with str/None in transform_args (line 644–651)

The docstring (lines 388–391) documents that transform_args values can be str (simple rename) or None (drop argument), but line 645 assigns the raw value to transform and line 651 calls transform.hide, which crashes with AttributeError: 'str' object has no attribute 'hide'.

from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool_transform import TransformedTool

def greet(name: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {name}!"

parent = FunctionTool.from_function(greet)
TransformedTool.from_tool(parent, transform_args={"name": "username"})
# AttributeError: 'str' object has no attribute 'hide'

TransformedTool.from_tool(parent, transform_args={"greeting": None})
# AttributeError: 'NoneType' object has no attribute 'hide'

Needs normalization: strArgTransform(name=value), NoneArgTransform(hide=True).

4. output_schema=False rejected despite being documented (line 398)

The docstring says output_schema=False disables output schema and structured outputs, but passing it raises a Pydantic validation error because the model only accepts dict | None.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Labels

bugSomething isn't working. Reports of errors, unexpected behavior, or broken functionality.serverRelated to FastMCP server implementation or server-side functionality.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions