đ„Fixing MCP Tool Name Collisions When Using Multiple MCP Servers
aka: Why does every MCP server define "search" as a tool?
This blog series focuses on presenting complex DevOps projects as simple and approachable via plain language and lots of pictures. You can do it!
These articles are supported by readers, please consider subscribing to support me writing more of these articles <3 :)
Hey all!
Iâve been building out my Slack Strands Agentic bot, a bot that comes with built-in MCPs that could connect to:
PagerDuty
Confluence / Jira
GitHub
As well as read our Bedrock knowledge base. I want the bot to be able to read the real status of resources, like in AWS or Azure.
When I added the Azure MCP, I worked through the bugs, and when I got it right⊠it crashed.
ERROR: Tool name âsearchâ already existsMCP (Model Context Protocol) is how AI agents connect to external tools. Itâs basically an API, but instead of you calling endpoints, the agent does. You give it access to a GitHub MCP server, and suddenly it can search code, open issues, list repos. Same with Atlassian - it gets tools for Confluence and Jira. Add PagerDuty and Azure, and youâve got a pretty capable agent.
Except when two servers both provide a tool called search.
Apparently when you import tools to your bots, if the tool names are identical, it crashes the bot (at least for the Strands library).
Atlassian MCP has search for finding Confluence pages, and Azure MCP has search for Azure resources. Both correct, and both needed, but both have the same name.
Itâs apparently not like variable scoping where context matters. This is a flat namespace.
I debugged the tools that are registering, you can see them below.
| GitHub MCP: Returning 25 tools: ['get_commit', 'get_file_contents', 'get_issue', 'get_issue_comments', 'get_label', 'get_latest_release', 'get_me', 'get_release_by_tag', 'get_tag', 'get_team_members', 'get_teams', 'list_branches', 'list_commits', 'list_issue_types', 'list_issues', 'list_label', 'list_pull_requests', 'list_releases', 'list_sub_issues', 'list_tags', 'search_code', 'search_issues', 'search_pull_requests', 'search_repositories', 'search_users'] | |
| Atlassian MCP: Returning 19 tools: ['atlassianUserInfo', 'getAccessibleAtlassianResources', 'getConfluenceSpaces', 'getConfluencePage', 'getPagesInConfluenceSpace', 'getConfluencePageFooterComments', 'getConfluencePageInlineComments', 'getConfluencePageDescendants', 'searchConfluenceUsingCql', 'getJiraIssue', 'getTransitionsForJiraIssue', 'lookupJiraAccountId', 'searchJiraIssuesUsingJql', 'getJiraIssueRemoteIssueLinks', 'getVisibleJiraProjects', 'getJiraProjectIssueTypesMetadata', 'getJiraIssueTypeMetaWithFields', 'search', 'fetch'] | |
| PagerDuty MCP: Returning 15 tools: ['list_incidents', 'get_incident', 'list_services', 'get_service', 'list_teams', 'get_team', 'list_team_members', 'get_user_data', 'list_users', 'list_schedules', 'get_schedule', 'list_schedule_users', 'list_oncalls', 'list_escalation_policies', 'get_escalation_policy'] | |
| Azure MCP: Returning 47 tools: ['documentation', 'azd', 'get_bestpractices', 'aks', 'appconfig', 'applens', 'appservice', 'role', 'datadog', 'managedlustre', 'azureterraformbestpractices', 'deploy', 'eventgrid', 'acr', 'bicepschema', 'cosmos', 'cloudarchitect', 'confidentialledger', 'eventhubs', 'foundry', 'functionapp', 'grafana', 'keyvault', 'kusto', 'loadtesting', 'marketplace', 'quota', 'monitor', 'applicationinsights', 'mysql', 'postgres', 'redis', 'communication', 'resourcehealth', 'search', 'speech', 'servicebus', 'signalr', 'sql', 'storage', 'virtualdesktop', 'workbooks', 'group_list', 'subscription_list', 'extension_azqr', 'extension_cli_generate', 'extension_cli_install'] | |
| ERROR: Tool name 'search' already exists |
Both Atlassian and the new Azure MCP try to register a tool called âsearchâ, which isnât permitted.
When you connect multiple independent MCP servers to the same agent, eventually their tool names will overlap. The more servers you add, the more likely it becomes.
I donât want to prune back those tools, I want all of them! Which meant I needed to fix the collision without modifying the MCP servers themselves (theyâre first-party remote and hosted by the companies) and without breaking how the Strands SDK registers tools.
Cool! Lets do this.
The Solution: Wrap It, Prefix It, Ship It
How most tools handle this is namespaces. If a tool is imported from Azure called âsearchâ, call it âazure_searchâ. That way collisions should be much more rare. And thatâs what I built.
I expected I could just pass an argument to the Strands agent when I give it tools, and itâll ingest the tool names with a prefix.
Iâm working on a PR for Strands to do this.
We need a wrapper! Something that sits between the MCP tool and the Strands registry, changes the name, but otherwise acts exactly like the original tool.
Hereâs what I built:
| class PrefixedMCPTool(AgentTool): | |
| """Wrapper that adds a prefix to an MCP tool's name.""" | |
| def __init__(self, tool, prefix): | |
| super().__init__() | |
| self._original_tool = tool | |
| self._prefix = prefix | |
| original_spec = tool.tool_spec | |
| self._prefixed_spec = original_spec.copy() | |
| self._prefixed_spec["name"] = f"{prefix}{original_spec['name']}" | |
| @property | |
| def tool_spec(self): | |
| """Return the modified tool spec with prefixed name""" | |
| return self._prefixed_spec | |
| @property | |
| def tool_name(self): | |
| """Return the prefixed tool name""" | |
| return self._prefixed_spec["name"] | |
| @property | |
| def tool_type(self): | |
| """Delegate to the original tool's type""" | |
| return self._original_tool.tool_type | |
| def stream(self, tool_use, *args, **kwargs): | |
| """Delegate to the original tool with all arguments | |
| Important: The *args and **kwargs are crucial here! | |
| The Strands framework passes additional arguments like | |
| invocation_state that must be forwarded to the original tool. | |
| """ | |
| return self._original_tool.stream(tool_use, *args, **kwargs) |
The `PrefixedMCPTool` class does three things:
1. Inherits from `AgentTool`: This is critical. The Strands SDK registry checks `isinstance(tool, AgentTool)`. If your wrapper doesnât inherit from `AgentTool`, the registry rejects it. No exceptions.
2. Modifies the tool name: Takes the original toolâs spec, copies it, and changes just the name. Everything else stays the same - description, parameters, all of it. Just the name gets the prefix.
3. Delegates everything else: All the actual work gets passed to the original tool. The wrapper is just changing the name on the way in.
The key part is the `stream()` method:
def stream(self, tool_use, *args, **kwargs):
return self._original_tool.stream(tool_use, *args, **kwargs)That `*args, **kwargs` matters. The Strands framework passes extra arguments to `stream()` - stuff like `invocation_state` that the tool needs. If you donât forward those arguments, your tools break at runtime (I learned this the hard way).
So now instead of registering a tool called `search`, you register a `PrefixedMCPTool` that wraps the original `search` tool but tells the registry its name is `atlassian_search`. The registry sees a unique name. The tool still works exactly the same way.
This ended up working SUPER WELL and Iâm going to do this in ever bot I build going forward (unless the MCP itself already prefixes the platform ahead of the tool name, which they obviously should, but most donât today).
## Making It Reusable
I didnât want to wrap every single tool manually, or define this same function in every MCP ingestion script (which are separate files for each MCP due to filtering, authentication, and utilization differences). I needed a single helper function that could take all the tools from an MCP server and prefix them in one shot.
Hereâs what I built:
| def add_prefix_to_mcp_tools(tools, prefix): | |
| prefix = f"{prefix}_" | |
| return [PrefixedMCPTool(tool, prefix) for tool in tools] | |
| # Get tools from Azure MCP - | |
| azure_mcp_client, azure_tools = build_Azure_mcp_client(azure_client_id, azure_client_secret, azure_tenant_id) | |
| # Apply prefix - tools like 'search' become 'azure_search' | |
| azure_tools = add_prefix_to_mcp_tools(azure_tools, "azure") | |
| # Add to agent's tool list | |
| tools.extend(azure_tools) |
The `add_prefix_to_mcp_tools()` function takes two arguments:
- `tools`: The list of tools from your MCP server
- `prefix`: The prefix you want to add (like âgithubâ or âazureâ)
It automatically adds the underscore, so you just pass `âgithubâ` and it creates `github_search`, `github_get_issue`, etc.
Do this for each MCP server with a different prefix:
atlassian_tools = add_prefix_to_mcp_tools(atlassian_tools, âatlassianâ)
github_tools = add_prefix_to_mcp_tools(azure_tools, âgithubâ)
pagerduty_tools = add_prefix_to_mcp_tools(pagerduty_tools, âpagerdutyâ)Before prefixing, I had:
- Atlassian: `search`, `fetch`
- Azure: `search`
After prefixing:
- Atlassian: `atlassian_search`, `atlassian_fetch`
- Azure: `azure_search`
- No collision, bot works
The agent now has access to 106 tools total (25 from GitHub, 19 from Atlassian, 15 from PagerDuty, 47 from Azure), and every single one has a unique name, and all are accessible at runtime.
With my luck, AWS (my next target) will also have a âsearchâ tool, but now I wonât really care, we wonât see the collision even if it does.
Summary
If youâre using multiple MCP servers with your AI agents, youâll hit tool name collisions eventually. Itâd be amazing if some registry came out that helped folks establish a standard naming scheme, validated partners and sources, much like the GitHub Marketplace, or Azure Marketplace, Hashi Marketplace etc., but it doesnât exist yet.
Iâm excited to no longer care what tools we register - now I can just add MCPs and their tools, and go back to worrying about AIs deleting my environment, or taking over the world.
Thanks yaâll. Good luck out there.
kyler


