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
3 changes: 3 additions & 0 deletions src/agents/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist MCP server metadata in run state

ToolCallItem now exposes mcp_server_name, but run-state persistence still only handles description/title in RunState._serialize_item and _deserialize_items, so this field is lost whenever a run is saved and reloaded. In interruption or session-resume flows, MCP-originated tool calls will come back with mcp_server_name=None, which breaks the new server-disambiguation behavior for restored runs.

Useful? React with 👍 / 👎.

"""Name of the MCP server that provided this tool, if applicable."""


ToolCallOutputTypes: TypeAlias = Union[
FunctionCallOutput,
Expand Down
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
4 changes: 4 additions & 0 deletions src/agents/run_internal/run_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -1385,22 +1385,26 @@ 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)
)
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")
Expand Down
2 changes: 2 additions & 0 deletions src/agents/run_internal/turn_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions src/agents/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
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 @@ -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,
)
Expand Down
52 changes: 52 additions & 0 deletions tests/mcp/test_mcp_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"