Hmbown/CodeWhale · error
qualified MCP tool name
Error message
qualified MCP tool name '{qualified_tool_name}' is ambiguous within server '{server_name}' What it means
Raised by `resolve_original_tool_name` (crates/mcp/src/lib.rs) during `call_qualified_tool`: more than one tool within a single server's tool list qualifies to the same qualified name `mcp__<server>__<tool>`, so the library cannot tell which underlying tool the caller meant. It refuses to guess and returns the ambiguity error.
Solutions
- Fix or configure the MCP server so its tool names sanitize to distinct components.
- List the server's tools (`list_tools`) to see the conflicting names, then call the intended one by its unique unqualified name.
- Filter tools (ToolFilter) so only one of the colliding tools is visible, making resolution unambiguous.
- If a server upgrade caused it, pin or roll back the server version until names are unique again.
Example fix
// before: server exposes 'run.tests' and 'run_tests' -> both qualify as mcp__srv__run_tests
call_qualified_tool("srv", "mcp__srv__run_tests", input)
// after: distinct tool names on the server
call_qualified_tool("srv", "mcp__srv__run_tests", input) // with 'run.tests' renamed server-side Defensive patterns
Strategy: validation
Validate before calling
// Rust: ensure the qualified name is unique within the server before calling
let target = "mcp__srv__run_tests";
let matches: Vec<_> = manager.list_tools()?.into_iter()
.filter(|t| t.server == "srv" && qualify_tool_name("srv", &t.tool_name) == target)
.collect();
anyhow::ensure!(matches.len() == 1, "ambiguous tool: {}", target); Type guard
fn resolves_uniquely(server: &str, qualified: &str, tools: &[McpToolDescriptor]) -> bool {
tools.iter().filter(|t| qualify_tool_name(server, &t.tool_name) == qualified).count() == 1
} Try / catch
match manager.call_qualified_tool(server, qualified, input) {
Err(e) if e.to_string().contains("is ambiguous within server") => {
// list tools, pick the exact unqualified name, retry
}
other => other?,
} Prevention
- Keep tool names within one server unique after sanitizing (no case/punctuation-only differences).
- Build qualified names from list_tools output, never by hand-concatenation.
- Use ToolFilter to hide deprecated duplicates when a server publishes both.
When it happens
Trigger: Calling `call_qualified_tool` when one server exposes two tools whose names sanitize to the same component (e.g. 'run.tests' and 'run_tests'), making `qualify_tool_name` match multiple entries in the filtered tool list.
Common situations: An MCP server publishes tools with names differing only by punctuation/case after sanitization; a server upgrade renames tools into a collision; the caller hand-built the qualified string from a stale tool listing.
Related errors
- MCP server ' ' collides with already-registered server ' '…
- qualified MCP tool name
- qualified MCP tool name
- qualified MCP tool name
- A managed, project or plugin connector already uses this…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/378c7d414c211e6a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/mcp/src/lib.rs:312
server_name: &str,
tool_segment: &str,
qualified_tool_name: &str,
) -> Result<String> {
let Some(client) = self.clients.get(server_name) else {
return Ok(tool_segment.to_string());
};
let Ok(tools) = client.list_tools() else {
return Ok(tool_segment.to_string());
};
let filter = self.configs.get(server_name).map(|(_, filter)| filter);
let mut matches = tools.iter().filter(|tool| {
filter.is_none_or(|filter| allowed_by_filter(&tool.tool_name, filter))
&& qualify_tool_name(server_name, &tool.tool_name) == qualified_tool_name
});
match (matches.next(), matches.next()) {
(Some(tool), None) => Ok(tool.tool_name.clone()),
(None, _) => Ok(tool_segment.to_string()),
(Some(_), Some(_)) => bail!(
"qualified MCP tool name '{qualified_tool_name}' is ambiguous within server '{server_name}'"
),
}
}
/// Start all registered servers, emitting status updates via the callback.
///
/// Returns a summary of which servers are ready, failed, or cancelled.
pub fn start_all<F>(&self, mut emit: F) -> McpStartupCompleteEvent
where
F: FnMut(McpStartupUpdateEvent),
{
let mut ready = Vec::new();
let mut failed = Vec::new();
let mut cancelled = Vec::new();
for (server_name, (cfg, _)) in &self.configs {
if !cfg.enabled {
emit(McpStartupUpdateEvent {View on GitHub (pinned to 73e0f67d83)