From 18fecc6723718d3abae6dadd3289d4a463bd6f3f Mon Sep 17 00:00:00 2001 From: Mitsuhiro Kotake Date: Tue, 23 Jun 2026 13:58:49 +0900 Subject: [PATCH] Disambiguate flattened MCP tool names --- .../codex-mcp/src/connection_manager_tests.rs | 21 +++++++++++++++++++ codex-rs/codex-mcp/src/tools.rs | 13 ++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 508598d343..9920e71448 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -481,6 +481,27 @@ fn test_normalize_tools_disambiguates_sanitized_tool_name_collisions() { assert_eq!(callable_tool_names.len(), 2); } +#[test] +fn test_normalize_tools_disambiguates_canonical_flat_name_collisions() { + let tools = vec![create_test_tool("a", "b__c"), create_test_tool("a__b", "c")]; + + let model_tools = + normalize_tools_for_model_with_prefix(tools, /*prefix_mcp_tool_names*/ true); + + assert_eq!(model_tools.len(), 2); + assert_eq!(model_tool_names(&model_tools).len(), 2); + let canonical_flat_names = model_tools + .iter() + .map(|tool| { + tool.canonical_tool_name() + .canonical_flat_name() + .into_owned() + }) + .collect::>(); + assert_eq!(canonical_flat_names.len(), 2); + assert!(canonical_flat_names.contains("mcp__a__b__c")); +} + #[test] fn tool_filter_allows_by_default() { let filter = ToolFilter::default(); diff --git a/codex-rs/codex-mcp/src/tools.rs b/codex-rs/codex-mcp/src/tools.rs index 83cbfd5e74..906432a0cf 100644 --- a/codex-rs/codex-mcp/src/tools.rs +++ b/codex-rs/codex-mcp/src/tools.rs @@ -376,8 +376,8 @@ fn unique_callable_parts( used_names: &mut HashSet, reserved_len: usize, ) -> (String, String) { - let model_name = format!("{namespace}{tool_name}"); - if model_name.len() + reserved_len <= MAX_TOOL_NAME_LENGTH && used_names.insert(model_name) { + let canonical_flat_name = canonical_flat_tool_name(namespace, tool_name); + if canonical_flat_name.len() <= MAX_TOOL_NAME_LENGTH && used_names.insert(canonical_flat_name) { return (namespace.to_string(), tool_name.to_string()); } @@ -390,10 +390,15 @@ fn unique_callable_parts( }; let (namespace, tool_name) = fit_callable_parts_with_hash(namespace, tool_name, &hash_input, reserved_len); - let model_name = format!("{namespace}{tool_name}"); - if used_names.insert(model_name) { + if used_names.insert(canonical_flat_tool_name(&namespace, &tool_name)) { return (namespace, tool_name); } attempt = attempt.saturating_add(1); } } + +fn canonical_flat_tool_name(namespace: &str, tool_name: &str) -> String { + ToolName::namespaced(namespace, tool_name) + .canonical_flat_name() + .into_owned() +}