diff --git a/src/agents/items.py b/src/agents/items.py index 9d6219f37d..f04a29b845 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -358,6 +358,9 @@ class ToolCallItem(RunItemBase[Any]): title: str | None = None """Optional short display label if known at item creation time.""" + mcp_server_name: str | None = None + """Name of the MCP server that provided this tool, if applicable.""" + ToolCallOutputTypes: TypeAlias = Union[ FunctionCallOutput, 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/run_internal/run_loop.py b/src/agents/run_internal/run_loop.py index 36e34b4f56..cc9d79c2cb 100644 --- a/src/agents/run_internal/run_loop.py +++ b/src/agents/run_internal/run_loop.py @@ -1385,6 +1385,7 @@ async def rewind_model_request() -> None: ) tool_description: str | None = None tool_title: str | None = None + tool_mcp_server_name: str | None = None if isinstance(output_item, McpCall): metadata = hosted_mcp_tool_metadata.get( (output_item.server_label, output_item.name) @@ -1392,15 +1393,18 @@ async def rewind_model_request() -> None: if metadata is not None: tool_description = metadata.description tool_title = metadata.title + tool_mcp_server_name = output_item.server_label elif matched_tool is not None: tool_description = getattr(matched_tool, "description", None) tool_title = getattr(matched_tool, "_mcp_title", None) + tool_mcp_server_name = getattr(matched_tool, "_mcp_server_name", None) tool_item = ToolCallItem( raw_item=cast(ToolCallItemTypes, output_item), agent=agent, description=tool_description, title=tool_title, + mcp_server_name=tool_mcp_server_name, ) streamed_result._event_queue.put_nowait( RunItemStreamEvent(item=tool_item, name="tool_called") diff --git a/src/agents/run_internal/turn_resolution.py b/src/agents/run_internal/turn_resolution.py index c34c720fcc..c5dddedffa 100644 --- a/src/agents/run_internal/turn_resolution.py +++ b/src/agents/run_internal/turn_resolution.py @@ -1523,6 +1523,7 @@ def _dump_output_item(raw_item: Any) -> dict[str, Any]: agent=agent, description=metadata.description if metadata is not None else None, title=metadata.title if metadata is not None else None, + mcp_server_name=output.server_label, ) ) tools_used.append("mcp") @@ -1659,6 +1660,7 @@ def _dump_output_item(raw_item: Any) -> dict[str, Any]: agent=agent, description=func_tool.description, title=func_tool._mcp_title, + mcp_server_name=func_tool._mcp_server_name, ) ) functions.append( diff --git a/src/agents/tool.py b/src/agents/tool.py index 1ac3c29ae3..9cbdebfa06 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -326,6 +326,9 @@ class FunctionTool: _mcp_title: str | None = field(default=None, kw_only=True, repr=False) """Internal MCP display title used for ToolCallItem metadata.""" + _mcp_server_name: str | None = field(default=None, kw_only=True, repr=False) + """Internal MCP server name identifying which server provided this tool.""" + @property def qualified_name(self) -> str: """Return the public qualified name used to identify this function tool.""" @@ -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( @@ -453,6 +457,7 @@ def _build_wrapped_function_tool( timeout_error_function=timeout_error_function, defer_loading=defer_loading, _mcp_title=mcp_title, + _mcp_server_name=mcp_server_name, ), failure_error_function, ) diff --git a/tests/mcp/test_mcp_util.py b/tests/mcp/test_mcp_util.py index c992e25e03..116fee555f 100644 --- a/tests/mcp/test_mcp_util.py +++ b/tests/mcp/test_mcp_util.py @@ -1455,3 +1455,55 @@ def test_to_function_tool_description_falls_back_to_mcp_title(): assert function_tool.description == "Search Docs" assert function_tool._mcp_title == "Search Docs" + + +def test_to_function_tool_stores_mcp_server_name(): + """Test that to_function_tool stores the MCP server name on the converted FunctionTool.""" + server = FakeMCPServer(server_name="my_git_server") + tool = MCPTool( + name="list_commits", + inputSchema={}, + description="List recent commits", + ) + + function_tool = MCPUtil.to_function_tool(tool, server, convert_schemas_to_strict=False) + + assert function_tool._mcp_server_name == "my_git_server" + + +def test_to_function_tool_default_mcp_server_name(): + """Test that FunctionTool defaults _mcp_server_name to None for non-MCP tools.""" + tool = FunctionTool( + name="plain_tool", + description="A plain tool", + params_json_schema={"type": "object", "properties": {}}, + on_invoke_tool=lambda ctx, args: None, # type: ignore[arg-type, return-value] + strict_json_schema=False, + ) + + assert tool._mcp_server_name is None + + +@pytest.mark.asyncio +async def test_get_all_function_tools_preserve_server_name(): + """Test that get_all_function_tools preserves server name across multiple servers.""" + server_a = FakeMCPServer(server_name="server_a") + server_a.add_tool("tool_x", {"type": "object", "properties": {}}) + + server_b = FakeMCPServer(server_name="server_b") + server_b.add_tool("tool_y", {"type": "object", "properties": {}}) + + agent = Agent(name="test", instructions="test") + run_context = RunContextWrapper(context=None) + + tools = await MCPUtil.get_all_function_tools( + [server_a, server_b], + convert_schemas_to_strict=False, + run_context=run_context, + agent=agent, + ) + + func_tools = [t for t in tools if isinstance(t, FunctionTool)] + tool_names_to_server = {t.name: t._mcp_server_name for t in func_tools} + assert tool_names_to_server["tool_x"] == "server_a" + assert tool_names_to_server["tool_y"] == "server_b"