mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fix MCP tools duplicated on second turn when runtime tools are present (#4432)
* Fix MCP tools duplicated on second turn when runtime tools are present When AG-UI's collect_server_tools pre-expands MCP functions on turn 2 (after the MCP server is connected), _prepare_run_context unconditionally appends them again from self.mcp_tools, duplicating every MCP tool. Skip MCP functions whose names already exist in the final tool list, following the same name-based dedup pattern used in _merge_options. Fixes #4381 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * mypy fix * Remove issue-specific references from test docstring Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
b5edb529b7
commit
b0ac3939c1
@@ -1051,10 +1051,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
else:
|
||||
final_tools.append(tool) # type: ignore
|
||||
|
||||
existing_names = {name for t in final_tools if (name := _get_tool_name(t)) is not None}
|
||||
for mcp_server in self.mcp_tools:
|
||||
if not mcp_server.is_connected:
|
||||
await self._async_exit_stack.enter_async_context(mcp_server)
|
||||
final_tools.extend(mcp_server.functions)
|
||||
final_tools.extend(f for f in mcp_server.functions if f.name not in existing_names)
|
||||
|
||||
# Merge runtime kwargs into additional_function_arguments so they're available
|
||||
# in function middleware context and tool invocation.
|
||||
|
||||
@@ -755,6 +755,49 @@ async def test_chat_agent_with_local_mcp_tools(client: SupportsChatGetResponse)
|
||||
pass
|
||||
|
||||
|
||||
async def test_mcp_tools_not_duplicated_when_passed_as_runtime_tools(chat_client_base: Any) -> None:
|
||||
"""Test that MCP tool functions from self.mcp_tools are not duplicated when already present in runtime tools."""
|
||||
captured_options: list[dict[str, Any]] = []
|
||||
|
||||
original_inner = chat_client_base._inner_get_response
|
||||
|
||||
async def capturing_inner(
|
||||
*, messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any
|
||||
) -> ChatResponse:
|
||||
captured_options.append(dict(options))
|
||||
return await original_inner(messages=messages, options=options, **kwargs)
|
||||
|
||||
chat_client_base._inner_get_response = capturing_inner
|
||||
|
||||
# Create FunctionTool instances that simulate expanded MCP functions
|
||||
mcp_func_a = FunctionTool(func=lambda: "a", name="tool_a", description="Tool A")
|
||||
mcp_func_b = FunctionTool(func=lambda: "b", name="tool_b", description="Tool B")
|
||||
|
||||
# Create a mock MCP tool that is already connected (simulates turn 2)
|
||||
mock_mcp_tool = MagicMock(spec=MCPTool)
|
||||
mock_mcp_tool.is_connected = True
|
||||
mock_mcp_tool.functions = [mcp_func_a, mcp_func_b]
|
||||
mock_mcp_tool.__aenter__ = AsyncMock(return_value=mock_mcp_tool)
|
||||
mock_mcp_tool.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
# Agent has the MCP tool in its constructor (stored in self.mcp_tools)
|
||||
agent = Agent(client=chat_client_base, name="TestAgent", tools=[mock_mcp_tool])
|
||||
|
||||
# Simulate AG-UI turn 2: pass already-expanded MCP functions + a client tool as runtime tools
|
||||
client_tool = FunctionTool(func=lambda: "client", name="client_tool", description="Client tool")
|
||||
runtime_tools = [mcp_func_a, mcp_func_b, client_tool]
|
||||
|
||||
await agent.run("hello", tools=runtime_tools)
|
||||
|
||||
# Verify the chat client received each tool exactly once
|
||||
assert len(captured_options) >= 1
|
||||
tool_names = [t.name for t in captured_options[0]["tools"]]
|
||||
assert tool_names.count("tool_a") == 1, f"tool_a duplicated: {tool_names}"
|
||||
assert tool_names.count("tool_b") == 1, f"tool_b duplicated: {tool_names}"
|
||||
assert "client_tool" in tool_names
|
||||
assert len(tool_names) == 3
|
||||
|
||||
|
||||
async def test_agent_tool_receives_session_in_kwargs(chat_client_base: Any) -> None:
|
||||
"""Verify tool execution receives 'session' inside **kwargs when function is called by client."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user