{"record":{"id":"4ce222924f345e41","repo":"Hmbown/CodeWhale","slug":"ambiguous-mcp-tool-name-prefixed-name-matches","errorCode":null,"errorMessage":"Ambiguous MCP tool name '{prefixed_name}' matches more than one server/tool authority","messagePattern":"Ambiguous MCP tool name '(.+?)' matches more than one server/tool authority","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":3032,"sourceCode":"    /// Parse a prefixed name into (server_name, tool_name)\n    pub(crate) fn parse_prefixed_name(&self, prefixed_name: &str) -> Result<(String, String)> {\n        let Some(rest) = prefixed_name.strip_prefix(\"mcp_\") else {\n            anyhow::bail!(\"Invalid MCP tool name: {prefixed_name}\");\n        };\n\n        let mut matched: Option<(String, String)> = None;\n        for (server, connection) in &self.connections {\n            if !connection.catalog_authorized() {\n                continue;\n            }\n            for tool in connection.tools() {\n                if !connection.config().is_tool_enabled(&tool.name)\n                    || format!(\"{server}_{}\", tool.name) != rest\n                {\n                    continue;\n                }\n                if matched.is_some() {\n                    anyhow::bail!(\n                        \"Ambiguous MCP tool name '{prefixed_name}' matches more than one server/tool authority\"\n                    );\n                }\n                matched = Some((server.clone(), tool.name.clone()));\n            }\n        }\n        if let Some(matched) = matched {\n            return Ok(matched);\n        }\n\n        Err(anyhow::anyhow!(\"Unknown MCP tool name: {prefixed_name}\"))\n    }\n\n    /// Resolve an MCP tool through an exact advertised catalog. A configured\n    /// but lazy server may be connected and asked for `tools/list`; the\n    /// requested suffix is never treated as authority on its own.\n    async fn resolve_advertised_tool(&mut self, prefixed_name: &str) -> Result<McpToolRoute> {\n        if let Ok((server_name, tool_name)) = self.parse_prefixed_name(prefixed_name) {","sourceCodeStart":3014,"sourceCodeEnd":3050,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L3014-L3050","documentation":"parse_prefixed_name iterates every catalog-authorized connection and collects every (server, tool) pair whose composed name format!(\"{server}_{tool}\") equals the suffix after mcp_. If more than one pair matches, the name is ambiguous and resolution aborts rather than silently picking one — e.g. server 'a' with tool 'b_c' and server 'a_b' with tool 'c' both produce mcp_a_b_c.","triggerScenarios":"Two servers whose names/tool names concatenate to the same string (server 'db' + tool 'backup_list' vs server 'db_backup' + tool 'list'); duplicate server registrations in static and dynamic registries exposing the same tool; a dynamic server registered under a name that prefixes another server's name.","commonSituations":"Registering the same MCP server twice under different names (one static, one dynamic); renaming servers without checking composed-name collisions; plugin bundles that add servers whose names collide with user-configured ones.","solutions":["Rename one of the colliding servers (or the colliding tool on the server side) so the composed mcp_{server}_{tool} strings differ.","Remove duplicate registrations — check both static config and dynamic_servers for the same underlying server/tool.","After renaming, refresh the model-facing catalog so stale names disappear.","Avoid server names that are prefixes of other server names plus underscore boundaries when tools share suffixes."],"exampleFix":"# before (.mcp.json) — both produce mcp_db_backup_list\n{\"servers\": {\"db\": {\"command\": \"m1\"}, \"db_backup\": {\"command\": \"m2\"}}}\n# db exposes tool 'backup_list'; db_backup exposes tool 'list'\n\n# after\n{\"servers\": {\"db\": {\"command\": \"m1\"}, \"backup\": {\"command\": \"m2\"}}}\n# composed names: mcp_db_backup_list vs mcp_backup_list — unambiguous","handlingStrategy":"validation","validationCode":"// Rust: detect ambiguity before dispatching\nfn unique_owner(pool: &McpPool, rest: &str) -> Option<(String, String)> {\n    let mut hits = pool.connections().iter()\n        .filter(|(_, c)| c.catalog_authorized())\n        .flat_map(|(s, c)| c.tools().iter().filter(move |t| format!(\"{s}_{}\", t.name) == rest).map(move |t| (s.clone(), t.name.clone())))\n        .collect::<Vec<_>>();\n    (hits.len() == 1).then(|| hits.pop().unwrap())\n}\nensure!(unique_owner(&pool, rest).is_some(), \"ambiguous or unknown tool\");","typeGuard":null,"tryCatchPattern":"// Rust: surface both colliding owners to the operator\nmatch pool.parse_prefixed_name(name) {\n    Err(e) if e.to_string().contains(\"Ambiguous MCP tool name\") => {\n        // report the colliding (server, tool) pairs so one can be renamed\n        bail!(\"{e:#}; rename a server or tool so names differ\")\n    }\n    r => r,\n}","preventionTips":["Don't register the same server twice (check both static config and dynamic registrations).","Avoid server names that are prefixes of other server names when tools share name tails.","When renaming servers, re-check composed mcp_{server}_{tool} uniqueness across the whole catalog."],"tags":["mcp","tools","ambiguity","naming"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}