[codex] Remove string-keyed MCP tool maps (#21454)

## Summary

This PR removes the synthetic `HashMap<String, ToolInfo>` keys from MCP
tool discovery. `McpConnectionManager::list_all_tools()` now returns
normalized `Vec<ToolInfo>`, and downstream code derives identity from
`ToolInfo::canonical_tool_name()`.

The motivation is to keep model-visible tool identity on
`ToolName`/`ToolInfo` instead of parallel string map keys, so future
namespace changes do not have to preserve otherwise-unused lookup keys.

## Changes

- Rename the MCP normalization path from `qualify_tools` to
`normalize_tools_for_model` and return tool values directly.
- Flow MCP tool lists through connectors, plugin injection, router/spec
building, code mode, and tool search as vectors/slices.
- Keep direct/deferred subtraction local to `mcp_tool_exposure`, using
`ToolName` values.
- Update tests to compare `ToolName` instances where MCP identity
matters.

## Validation

- `cargo test -p codex-mcp test_normalize_tools`
- `cargo test -p codex-core mcp_tool_exposure`
- `cargo test -p codex-core
direct_mcp_tools_register_namespaced_handlers`
- `cargo test -p codex-core
search_tool_registers_namespaced_mcp_tool_aliases`
- `just fix -p codex-mcp`
- `just fix -p codex-core`
This commit is contained in:
pakrym-oai authored and GitHub committed 2026-05-07 10:16:10 -07:00
1 parent 114bac1409
commit 857e731478
20 files changed
+459 -471

No files matched your search

+15 -15
View File
@@ -1,4 +1,4 @@
//! MCP tool metadata, filtering, schema shaping, and name qualification.
//! MCP tool metadata, filtering, schema shaping, and name normalization.
//!
//! Raw MCP tool identities must be preserved for protocol calls, while
//! model-visible tool names must be sanitized, deduplicated, and kept within API
@@ -130,12 +130,12 @@ pub(crate) fn filter_tools(tools: Vec<ToolInfo>, filter: &ToolFilter) -> Vec<Too
.collect()
}
/// Returns a qualified-name lookup for MCP tools.
/// Returns MCP tools with model-visible names normalized.
///
/// Raw MCP server/tool names are kept on each [`ToolInfo`] for protocol calls, while
/// `callable_namespace` / `callable_name` are sanitized and, when necessary, hashed so
/// every model-visible `mcp__namespace__tool` name is unique and <= 64 bytes.
pub(crate) fn qualify_tools<I>(tools: I) -> HashMap<String, ToolInfo>
/// every model-visible name is unique and <= 64 bytes.
pub(crate) fn normalize_tools_for_model<I>(tools: I) -> Vec<ToolInfo>
where
I: IntoIterator<Item = ToolInfo>,
{
@@ -213,9 +213,9 @@ where
candidates.sort_by(|left, right| left.raw_tool_identity.cmp(&right.raw_tool_identity));
let mut used_names = HashSet::new();
let mut qualified_tools = HashMap::new();
let mut model_tools = Vec::new();
for mut candidate in candidates {
let (callable_namespace, callable_name, qualified_name) = unique_callable_parts(
let (callable_namespace, callable_name) = unique_callable_parts(
&candidate.callable_namespace,
&candidate.callable_name,
&candidate.raw_tool_identity,
@@ -223,9 +223,9 @@ where
);
candidate.tool.callable_namespace = callable_namespace;
candidate.tool.callable_name = callable_name;
qualified_tools.insert(qualified_name, candidate.tool);
model_tools.push(candidate.tool);
}
qualified_tools
model_tools
}
#[derive(Debug)]
@@ -345,10 +345,10 @@ fn unique_callable_parts(
tool_name: &str,
raw_identity: &str,
used_names: &mut HashSet<String>,
) -> (String, String, String) {
let qualified_name = format!("{namespace}{tool_name}");
if qualified_name.len() <= MAX_TOOL_NAME_LENGTH && used_names.insert(qualified_name.clone()) {
return (namespace.to_string(), tool_name.to_string(), qualified_name);
) -> (String, String) {
let model_name = format!("{namespace}{tool_name}");
if model_name.len() <= MAX_TOOL_NAME_LENGTH && used_names.insert(model_name) {
return (namespace.to_string(), tool_name.to_string());
}
let mut attempt = 0_u32;
@@ -360,9 +360,9 @@ fn unique_callable_parts(
};
let (namespace, tool_name) =
fit_callable_parts_with_hash(namespace, tool_name, &hash_input);
let qualified_name = format!("{namespace}{tool_name}");
if used_names.insert(qualified_name.clone()) {
return (namespace, tool_name, qualified_name);
let model_name = format!("{namespace}{tool_name}");
if used_names.insert(model_name) {
return (namespace, tool_name);
}
attempt = attempt.saturating_add(1);
}