Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/agents/mcp/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ def to_function_tool(
strict_json_schema=is_strict,
needs_approval=needs_approval,
mcp_title=resolve_mcp_tool_title(tool),
mcp_server_name=server.name,
)
return function_tool

Expand Down
5 changes: 5 additions & 0 deletions src/agents/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,9 @@ class FunctionTool:
defer_loading: bool = False
"""Whether the Responses API should hide this tool definition until tool search loads it."""

mcp_server_name: str | None = field(default=None, kw_only=True)
"""The MCP server name this tool originated from, if it was converted from an MCP tool."""

_failure_error_function: ToolErrorFunction | None = field(
default=None,
kw_only=True,
Expand Down Expand Up @@ -428,6 +431,7 @@ def _build_wrapped_function_tool(
defer_loading: bool = False,
sync_invoker: bool = False,
mcp_title: str | None = None,
mcp_server_name: str | None = None,
) -> FunctionTool:
"""Create a FunctionTool with copied-tool-aware failure handling bound in one place."""
on_invoke_tool = with_function_tool_failure_error_handler(
Expand All @@ -452,6 +456,7 @@ def _build_wrapped_function_tool(
timeout_behavior=timeout_behavior,
timeout_error_function=timeout_error_function,
defer_loading=defer_loading,
mcp_server_name=mcp_server_name,
_mcp_title=mcp_title,
),
failure_error_function,
Expand Down
12 changes: 12 additions & 0 deletions tests/mcp/test_mcp_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ async def test_get_all_function_tools():
assert len(tools) == 5
assert all(tool.name in names for tool in tools)

expected_server_names = [
server1.name,
server1.name,
server2.name,
server2.name,
server3.name,
]
for tool, expected_server_name in zip(tools, expected_server_names):
assert isinstance(tool, FunctionTool)
assert tool.mcp_server_name == expected_server_name


@pytest.mark.asyncio
async def test_invoke_mcp_tool():
Expand Down Expand Up @@ -159,6 +170,7 @@ async def test_to_function_tool_passes_static_mcp_meta():
)

function_tool = MCPUtil.to_function_tool(tool, server, convert_schemas_to_strict=False)
assert function_tool.mcp_server_name == server.name
tool_context = ToolContext(
context=None,
tool_name="test_tool_1",
Expand Down
20 changes: 20 additions & 0 deletions tests/test_function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ async def run_function(ctx: RunContextWrapper[Any], args: str) -> str:

assert tool.name == "test"
assert tool.description == "Processes extracted user data"
assert tool.mcp_server_name is None
for key, value in FunctionArgs.model_json_schema().items():
assert tool.params_json_schema[key] == value
assert tool.strict_json_schema
Expand Down Expand Up @@ -702,6 +703,25 @@ def boom() -> None:
assert cast(Any, copied_tool).custom_state is custom_state


def test_function_tool_copy_preserves_mcp_server_name() -> None:
async def invoke(_ctx: ToolContext[Any], _args: str) -> str:
return "ok"

original_tool = FunctionTool(
name="tool_name",
description="tool_description",
params_json_schema={"type": "object", "properties": {}},
on_invoke_tool=invoke,
mcp_server_name="filesystem",
)

shallow_copied_tool = copy.copy(original_tool)
replaced_tool = dataclasses.replace(original_tool, name="copied_tool")

assert shallow_copied_tool.mcp_server_name == "filesystem"
assert replaced_tool.mcp_server_name == "filesystem"


@pytest.mark.asyncio
@pytest.mark.parametrize("copy_style", ["replace", "shallow_copy"])
async def test_copied_function_tool_invalid_input_uses_current_name(copy_style: str) -> None:
Expand Down