From 26669991580f2ef5333e3bb240e32f3e593262f0 Mon Sep 17 00:00:00 2001 From: Ignazio De Santis Date: Thu, 26 Mar 2026 21:55:42 +0800 Subject: [PATCH] feat(mcp): expose server name on function tools --- src/agents/mcp/util.py | 1 + src/agents/tool.py | 5 +++++ tests/mcp/test_mcp_util.py | 12 ++++++++++++ tests/test_function_tool.py | 20 ++++++++++++++++++++ 4 files changed, 38 insertions(+) diff --git a/src/agents/mcp/util.py b/src/agents/mcp/util.py index 33bea065c5..d143c7a707 100644 --- a/src/agents/mcp/util.py +++ b/src/agents/mcp/util.py @@ -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 diff --git a/src/agents/tool.py b/src/agents/tool.py index 1ac3c29ae3..c860449487 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -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, @@ -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( @@ -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, diff --git a/tests/mcp/test_mcp_util.py b/tests/mcp/test_mcp_util.py index 5a9cbd140c..b11bda42e4 100644 --- a/tests/mcp/test_mcp_util.py +++ b/tests/mcp/test_mcp_util.py @@ -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(): @@ -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", diff --git a/tests/test_function_tool.py b/tests/test_function_tool.py index 11eb5d7c2e..c07b935b37 100644 --- a/tests/test_function_tool.py +++ b/tests/test_function_tool.py @@ -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 @@ -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: