Skip to content

Commit 76aa10b

Browse files
CopilotJesuTerraz
andauthored
Prevent duplicate MCP server registration in Google ADK tooling extension (#168)
* Initial plan * Add de-duplication logic to prevent duplicate MCP server registration Co-authored-by: JesuTerraz <96103167+JesuTerraz@users.noreply.github.com> * Optimize duplicate detection with set for O(1) lookup performance Co-authored-by: JesuTerraz <96103167+JesuTerraz@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JesuTerraz <96103167+JesuTerraz@users.noreply.github.com>
1 parent a3cbd62 commit 76aa10b

2 files changed

Lines changed: 153 additions & 10 deletions

File tree

libraries/microsoft-agents-a365-tooling-extensions-googleadk/microsoft_agents_a365/tooling/extensions/googleadk/services/mcp_tool_registration_service.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,31 @@ async def add_tool_servers_to_agent(
9191

9292
self._logger.info(f"Loaded {len(mcp_server_configs)} MCP server configurations")
9393

94-
# Convert MCP server configs to McpToolset objects
94+
# Collect existing server URLs to prevent duplicates (use set for O(1) lookup)
95+
existing_server_urls = set()
96+
for tool in agent.tools:
97+
# Check if the tool is an McpToolset and has a connection_params.url
98+
if hasattr(tool, "connection_params") and hasattr(tool.connection_params, "url"):
99+
existing_server_urls.add(tool.connection_params.url)
100+
101+
self._logger.debug(f"Found {len(existing_server_urls)} existing MCP servers in agent")
102+
103+
# Convert MCP server configs to McpToolset objects (only new ones)
95104
mcp_servers_info = []
96105
mcp_server_headers = {
97106
Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}",
98107
Constants.Headers.USER_AGENT: Utility.get_user_agent_header(self._orchestrator_name),
99108
}
100109

101110
for server_config in mcp_server_configs:
111+
# Skip if server URL already exists
112+
if server_config.url in existing_server_urls:
113+
self._logger.debug(
114+
f"Skipping MCP server '{server_config.mcp_server_name}' "
115+
f"at {server_config.url} - already exists in agent"
116+
)
117+
continue
118+
102119
try:
103120
server_info = McpToolset(
104121
connection_params=StreamableHTTPConnectionParams(
@@ -109,6 +126,7 @@ async def add_tool_servers_to_agent(
109126

110127
mcp_servers_info.append(server_info)
111128
self._connected_servers.append(server_info)
129+
existing_server_urls.add(server_config.url)
112130
self._logger.info(
113131
f"Created MCP toolset for '{server_config.mcp_server_name}' "
114132
f"at {server_config.url}"
@@ -119,15 +137,16 @@ async def add_tool_servers_to_agent(
119137
self._logger.warning(f"Failed to create MCP toolset for {server_name}: {tool_ex}")
120138
continue
121139

122-
# Combine existing tools with new MCP servers
123-
all_tools = list(agent.tools) + mcp_servers_info
124-
125-
self._logger.info(
126-
f"Successfully configured agent with {len(mcp_servers_info)} MCP tool servers "
127-
f"(total tools: {len(all_tools)})"
128-
)
129-
130-
agent.tools = all_tools
140+
# Only modify agent.tools if we have new servers to add
141+
if mcp_servers_info:
142+
all_tools = list(agent.tools) + mcp_servers_info
143+
agent.tools = all_tools
144+
self._logger.info(
145+
f"Successfully configured agent with {len(mcp_servers_info)} new MCP tool servers "
146+
f"(total tools: {len(all_tools)})"
147+
)
148+
else:
149+
self._logger.info("No new MCP servers to add to agent")
131150

132151
async def cleanup(self):
133152
"""Clean up any resources used by the service."""

tests/tooling/extensions/googleadk/test_mcp_tool_registration_service.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,130 @@ async def test_add_tool_servers_handles_toolset_creation_error(
345345
# Existing tools should still be present
346346
assert existing_tool in mock_agent.tools
347347

348+
@pytest.mark.asyncio
349+
@pytest.mark.unit
350+
async def test_add_tool_servers_skips_duplicate_server_urls(
351+
self, mock_agent, mock_authorization, mock_turn_context
352+
):
353+
"""Test that duplicate server URLs are not added multiple times."""
354+
with (
355+
patch(
356+
"microsoft_agents_a365.tooling.extensions.googleadk.services.mcp_tool_registration_service.McpToolServerConfigurationService"
357+
) as mock_config_service_class,
358+
patch(
359+
"microsoft_agents_a365.tooling.extensions.googleadk.services.mcp_tool_registration_service.Utility"
360+
) as mock_utility,
361+
patch(
362+
"microsoft_agents_a365.tooling.extensions.googleadk.services.mcp_tool_registration_service.McpToolset"
363+
) as mock_toolset_class,
364+
):
365+
# Setup mocks
366+
mock_utility.resolve_agent_identity.return_value = "agent-123"
367+
mock_utility.get_user_agent_header.return_value = "Agent365SDK/1.0"
368+
369+
# Create two server configs with the same URL
370+
mock_server_config1 = MagicMock()
371+
mock_server_config1.mcp_server_name = "server-1"
372+
mock_server_config1.url = "https://test-server.example.com/mcp"
373+
374+
mock_server_config2 = MagicMock()
375+
mock_server_config2.mcp_server_name = "server-2"
376+
mock_server_config2.url = "https://test-server.example.com/mcp" # Same URL
377+
378+
mock_config_service = AsyncMock()
379+
mock_config_service.list_tool_servers = AsyncMock(
380+
return_value=[mock_server_config1, mock_server_config2]
381+
)
382+
mock_config_service_class.return_value = mock_config_service
383+
384+
# Create mock toolsets
385+
mock_toolset1 = MagicMock()
386+
mock_toolset1.connection_params = MagicMock()
387+
mock_toolset1.connection_params.url = "https://test-server.example.com/mcp"
388+
389+
mock_toolset_class.return_value = mock_toolset1
390+
391+
from microsoft_agents_a365.tooling.extensions.googleadk import (
392+
McpToolRegistrationService,
393+
)
394+
395+
service = McpToolRegistrationService()
396+
397+
# Set up agent with no existing tools
398+
mock_agent.tools = []
399+
400+
# Act
401+
await service.add_tool_servers_to_agent(
402+
agent=mock_agent,
403+
auth=mock_authorization,
404+
auth_handler_name="graph",
405+
context=mock_turn_context,
406+
auth_token="test-token",
407+
)
408+
409+
# Assert - toolset should be created only once despite two configs
410+
assert mock_toolset_class.call_count == 1
411+
assert len(service._connected_servers) == 1
412+
assert len(mock_agent.tools) == 1
413+
414+
@pytest.mark.asyncio
415+
@pytest.mark.unit
416+
async def test_add_tool_servers_skips_existing_servers_in_agent(
417+
self, mock_agent, mock_authorization, mock_turn_context
418+
):
419+
"""Test that servers already in the agent are not added again."""
420+
with (
421+
patch(
422+
"microsoft_agents_a365.tooling.extensions.googleadk.services.mcp_tool_registration_service.McpToolServerConfigurationService"
423+
) as mock_config_service_class,
424+
patch(
425+
"microsoft_agents_a365.tooling.extensions.googleadk.services.mcp_tool_registration_service.Utility"
426+
) as mock_utility,
427+
patch(
428+
"microsoft_agents_a365.tooling.extensions.googleadk.services.mcp_tool_registration_service.McpToolset"
429+
) as mock_toolset_class,
430+
):
431+
# Setup mocks
432+
mock_utility.resolve_agent_identity.return_value = "agent-123"
433+
mock_utility.get_user_agent_header.return_value = "Agent365SDK/1.0"
434+
435+
# Create server config
436+
mock_server_config = MagicMock()
437+
mock_server_config.mcp_server_name = "existing-server"
438+
mock_server_config.url = "https://existing-server.example.com/mcp"
439+
440+
mock_config_service = AsyncMock()
441+
mock_config_service.list_tool_servers = AsyncMock(return_value=[mock_server_config])
442+
mock_config_service_class.return_value = mock_config_service
443+
444+
from microsoft_agents_a365.tooling.extensions.googleadk import (
445+
McpToolRegistrationService,
446+
)
447+
448+
service = McpToolRegistrationService()
449+
450+
# Set up agent with existing toolset that has the same URL
451+
existing_toolset = MagicMock()
452+
existing_toolset.connection_params = MagicMock()
453+
existing_toolset.connection_params.url = "https://existing-server.example.com/mcp"
454+
mock_agent.tools = [existing_toolset]
455+
456+
# Act
457+
await service.add_tool_servers_to_agent(
458+
agent=mock_agent,
459+
auth=mock_authorization,
460+
auth_handler_name="graph",
461+
context=mock_turn_context,
462+
auth_token="test-token",
463+
)
464+
465+
# Assert - no new toolset should be created
466+
mock_toolset_class.assert_not_called()
467+
assert len(service._connected_servers) == 0
468+
# Agent should still have the one existing tool
469+
assert len(mock_agent.tools) == 1
470+
assert mock_agent.tools[0] == existing_toolset
471+
348472

349473
class TestCleanup:
350474
"""Tests for cleanup method."""

0 commit comments

Comments
 (0)