Skip to content

Commit b9f25db

Browse files
committed
fix(mcp): disambiguate colliding server prefixes
1 parent 9179a99 commit b9f25db

2 files changed

Lines changed: 71 additions & 3 deletions

File tree

src/agents/mcp/util.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import copy
55
import functools
6+
import hashlib
67
import inspect
78
import json
89
from collections.abc import Awaitable
@@ -212,6 +213,9 @@ async def get_all_function_tools(
212213
"""Get all function tools from a list of MCP servers."""
213214
tools = []
214215
tool_names: set[str] = set()
216+
server_tool_name_prefixes = (
217+
cls._server_tool_name_prefixes(servers) if include_server_in_tool_names else {}
218+
)
215219
for server in servers:
216220
server_tools = await cls.get_function_tools(
217221
server,
@@ -220,6 +224,7 @@ async def get_all_function_tools(
220224
agent,
221225
failure_error_function=failure_error_function,
222226
include_server_in_tool_names=include_server_in_tool_names,
227+
tool_name_prefix=server_tool_name_prefixes.get(id(server)),
223228
)
224229
server_tool_names = {tool.name for tool in server_tools}
225230
if len(server_tool_names & tool_names) > 0:
@@ -241,16 +246,18 @@ async def get_function_tools(
241246
agent: AgentBase,
242247
failure_error_function: ToolErrorFunction | None = default_tool_error_function,
243248
include_server_in_tool_names: bool = False,
249+
tool_name_prefix: str | None = None,
244250
) -> list[Tool]:
245251
"""Get all function tools from a single MCP server."""
246252

247253
with mcp_tools_span(server=server.name) as span:
248254
tools = await server.list_tools(run_context, agent)
249255
span.span_data.result = [tool.name for tool in tools]
250256

251-
tool_name_prefix = (
252-
cls._server_tool_name_prefix(server.name) if include_server_in_tool_names else ""
253-
)
257+
if tool_name_prefix is None:
258+
tool_name_prefix = (
259+
cls._server_tool_name_prefix(server.name) if include_server_in_tool_names else ""
260+
)
254261
return [
255262
cls.to_function_tool(
256263
tool,
@@ -273,6 +280,30 @@ def _server_tool_name_prefix(server_name: str) -> str:
273280
normalized = "server"
274281
return f"{normalized}_"
275282

283+
@classmethod
284+
def _server_tool_name_prefixes(cls, servers: list[MCPServer]) -> dict[int, str]:
285+
normalized_to_servers: dict[str, list[MCPServer]] = {}
286+
for server in servers:
287+
normalized_prefix = cls._server_tool_name_prefix(server.name)[:-1]
288+
normalized_to_servers.setdefault(normalized_prefix, []).append(server)
289+
290+
prefixes: dict[int, str] = {}
291+
for normalized_prefix, grouped_servers in normalized_to_servers.items():
292+
if len(grouped_servers) == 1:
293+
prefixes[id(grouped_servers[0])] = f"{normalized_prefix}_"
294+
continue
295+
296+
seen_prefixes: set[str] = set()
297+
for index, server in enumerate(grouped_servers, start=1):
298+
hash_suffix = hashlib.sha1(server.name.encode("utf-8")).hexdigest()[:8]
299+
prefix = f"{normalized_prefix}_{hash_suffix}"
300+
if prefix in seen_prefixes:
301+
prefix = f"{prefix}_{index}"
302+
seen_prefixes.add(prefix)
303+
prefixes[id(server)] = f"{prefix}_"
304+
305+
return prefixes
306+
276307
@classmethod
277308
def to_function_tool(
278309
cls,

tests/mcp/test_mcp_util.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,43 @@ async def test_get_all_function_tools_prefix_falls_back_for_empty_server_name_sl
178178
assert server.tool_calls == ["search"]
179179

180180

181+
@pytest.mark.asyncio
182+
async def test_get_all_function_tools_disambiguates_colliding_server_name_prefixes():
183+
server1 = FakeMCPServer(server_name="GitHub MCP Server")
184+
server1.add_tool("create_issue", {})
185+
186+
server2 = FakeMCPServer(server_name="GitHub_MCP_Server")
187+
server2.add_tool("create_issue", {})
188+
189+
run_context = RunContextWrapper(context=None)
190+
agent = Agent(
191+
name="test_agent",
192+
instructions="Test agent",
193+
mcp_servers=[server1, server2],
194+
mcp_config={"include_server_in_tool_names": True},
195+
)
196+
197+
tools = await agent.get_mcp_tools(run_context)
198+
tool_names = {tool.name for tool in tools}
199+
assert len(tool_names) == 2
200+
assert all(tool_name.startswith("GitHub_MCP_Server_") for tool_name in tool_names)
201+
assert all(tool_name.endswith("_create_issue") for tool_name in tool_names)
202+
203+
for idx, tool in enumerate(tools, start=1):
204+
assert isinstance(tool, FunctionTool)
205+
tool_context = ToolContext(
206+
context=None,
207+
tool_name=tool.name,
208+
tool_call_id=f"prefixed_collision_{idx}",
209+
tool_arguments='{"title":"collision"}',
210+
)
211+
result = await tool.on_invoke_tool(tool_context, '{"title":"collision"}')
212+
assert isinstance(result, dict)
213+
214+
assert server1.tool_calls == ["create_issue"]
215+
assert server2.tool_calls == ["create_issue"]
216+
217+
181218
@pytest.mark.asyncio
182219
async def test_invoke_mcp_tool():
183220
"""Test that the invoke_mcp_tool function invokes an MCP tool and returns the result."""

0 commit comments

Comments
 (0)