Move MCP tool naming mode into manager (#21576)

## Why

The `non_prefixed_mcp_tool_names` feature should be applied where MCP
tools become model-visible, not by remapping names later in core.
Keeping the decision in `McpConnectionManager` construction makes
`ToolInfo` the single shaped view that spec building, deferred tool
search, routing, and unavailable-tool placeholders can consume directly.

This also preserves the existing external behavior while the feature is
off, and keeps the feature-on behavior for code mode and hooks explicit
at the manager boundary.

## What Changed

- Add `McpToolNameMode` to `codex-mcp` and flow it through `McpConfig`
into `McpConnectionManager::new`.
- Normalize MCP `ToolInfo` names in the manager using either
legacy-prefixed namespaces or non-prefixed namespaces; the legacy path
adds `mcp__` without restoring the old trailing namespace suffix.
- Remove the core-side MCP name remapping path so specs, tool search,
session resolution, and unavailable-tool placeholder construction use
the manager-provided `ToolName` values directly.
- Keep code mode flattening on the `__` namespace separator.
- Preserve hook compatibility by giving non-prefixed MCP hook names
legacy `mcp__...` matcher aliases.
- Add/adjust integration and unit coverage for non-prefixed code-mode
behavior, hook matching with the feature on and off, and manager-level
legacy prefixing.

## Testing

- `cargo test -p codex-mcp --lib`
- `cargo test -p codex-core --lib tools::spec::tests -- --nocapture`
- `cargo test -p codex-core --lib mcp_tools -- --nocapture`
- `cargo test -p codex-core --lib mcp_tool_exposure -- --nocapture`
- `cargo test -p codex-core --test all mcp_tool -- --nocapture`
- `cargo test -p codex-core --test all search_tool -- --nocapture`
- `cargo test -p codex-core --test all hooks_mcp -- --nocapture`
- `cargo test -p codex-core --test all
code_mode_uses_non_prefixed_mcp_tool_names_when_feature_enabled --
--nocapture`
- `cargo test -p codex-tools`
- `cargo test -p codex-features`
This commit is contained in:
pakrym-oai
2026-05-26 08:21:15 -07:00
committed by GitHub
parent b637fd26aa
commit ff7513cd83
30 changed files with 611 additions and 146 deletions
+3 -3
View File
@@ -20,7 +20,7 @@ use serde::Serialize;
use sha1::Digest;
use sha1::Sha1;
pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 2;
pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 3;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodexAppsToolsCacheKey {
@@ -127,9 +127,9 @@ pub(crate) fn normalize_codex_apps_callable_namespace(
if server_name == CODEX_APPS_MCP_SERVER_NAME
&& let Some(connector_name) = connector_name
{
format!("mcp__{}__{}", server_name, sanitize_name(connector_name))
format!("{}__{}", server_name, sanitize_name(connector_name))
} else {
format!("mcp__{server_name}__")
server_name.to_string()
}
}
+17 -4
View File
@@ -33,7 +33,7 @@ use crate::server::EffectiveMcpServer;
use crate::server::McpServerMetadata;
use crate::tools::ToolInfo;
use crate::tools::filter_tools;
use crate::tools::normalize_tools_for_model;
use crate::tools::normalize_tools_for_model_with_prefix;
use crate::tools::tool_with_model_visible_input_schema;
use anyhow::Context;
use anyhow::Result;
@@ -73,6 +73,7 @@ pub struct McpConnectionManager {
server_metadata: HashMap<String, McpServerMetadata>,
tool_plugin_provenance: Arc<ToolPluginProvenance>,
host_owned_codex_apps_enabled: bool,
prefix_mcp_tool_names: bool,
elicitation_requests: ElicitationRequestManager,
startup_cancellation_token: CancellationToken,
}
@@ -81,19 +82,26 @@ impl McpConnectionManager {
pub fn new_uninitialized(
approval_policy: &Constrained<AskForApproval>,
permission_profile: &Constrained<PermissionProfile>,
prefix_mcp_tool_names: bool,
) -> Self {
Self::new_uninitialized_with_permission_profile(approval_policy, permission_profile.get())
Self::new_uninitialized_with_permission_profile(
approval_policy,
permission_profile.get(),
prefix_mcp_tool_names,
)
}
pub fn new_uninitialized_with_permission_profile(
approval_policy: &Constrained<AskForApproval>,
permission_profile: &PermissionProfile,
prefix_mcp_tool_names: bool,
) -> Self {
Self {
clients: HashMap::new(),
server_metadata: HashMap::new(),
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
host_owned_codex_apps_enabled: false,
prefix_mcp_tool_names,
elicitation_requests: ElicitationRequestManager::new(
approval_policy.value(),
permission_profile.clone(),
@@ -180,6 +188,7 @@ impl McpConnectionManager {
codex_home: PathBuf,
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
host_owned_codex_apps_enabled: bool,
prefix_mcp_tool_names: bool,
client_elicitation_capability: ElicitationCapability,
tool_plugin_provenance: ToolPluginProvenance,
auth: Option<&CodexAuth>,
@@ -292,6 +301,7 @@ impl McpConnectionManager {
server_metadata,
tool_plugin_provenance,
host_owned_codex_apps_enabled,
prefix_mcp_tool_names,
elicitation_requests: elicitation_requests.clone(),
startup_cancellation_token: cancel_token.clone(),
};
@@ -381,7 +391,7 @@ impl McpConnectionManager {
.map(|tool| self.with_server_metadata(tool)),
);
}
normalize_tools_for_model(tools)
normalize_tools_for_model_with_prefix(tools, self.prefix_mcp_tool_names)
}
/// Force-refresh codex apps tools by bypassing the in-process cache.
@@ -432,7 +442,10 @@ impl McpConnectionManager {
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
self.with_server_metadata(tool)
});
Ok(normalize_tools_for_model(tools))
Ok(normalize_tools_for_model_with_prefix(
tools,
self.prefix_mcp_tool_names,
))
}
fn with_server_metadata(&self, mut tool: ToolInfo) -> ToolInfo {
@@ -14,7 +14,7 @@ use crate::server::McpServerOrigin;
use crate::tools::ToolFilter;
use crate::tools::ToolInfo;
use crate::tools::filter_tools;
use crate::tools::normalize_tools_for_model;
use crate::tools::normalize_tools_for_model_with_prefix;
use crate::tools::tool_with_model_visible_input_schema;
use codex_config::Constrained;
use codex_config::McpServerConfig;
@@ -37,13 +37,12 @@ use std::sync::Arc;
use tempfile::tempdir;
fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
let tool_namespace = format!("mcp__{server_name}__");
ToolInfo {
server_name: server_name.to_string(),
supports_parallel_tool_calls: false,
server_origin: None,
callable_name: tool_name.to_string(),
callable_namespace: tool_namespace,
callable_namespace: server_name.to_string(),
namespace_description: None,
tool: Tool {
name: tool_name.to_string().into(),
@@ -97,7 +96,10 @@ fn model_tool_names(tools: &[ToolInfo]) -> HashSet<ToolName> {
}
fn model_tool_name_len(name: &ToolName) -> usize {
name.namespace.as_deref().map_or(0, str::len) + name.name.len()
name.namespace
.as_deref()
.map_or(0, |namespace| namespace.len() + "__".len())
+ name.name.len()
}
fn is_code_mode_compatible_tool_name(name: &ToolName) -> bool {
@@ -301,13 +303,14 @@ fn test_normalize_tools_short_non_duplicated_names() {
create_test_tool("server1", "tool2"),
];
let model_tools = normalize_tools_for_model(tools);
let model_tools =
normalize_tools_for_model_with_prefix(tools, /*prefix_mcp_tool_names*/ true);
assert_eq!(
model_tool_names(&model_tools),
HashSet::from([
ToolName::namespaced("mcp__server1__", "tool1"),
ToolName::namespaced("mcp__server1__", "tool2")
ToolName::namespaced("mcp__server1", "tool1"),
ToolName::namespaced("mcp__server1", "tool2")
])
);
}
@@ -319,12 +322,13 @@ fn test_normalize_tools_duplicated_names_skipped() {
create_test_tool("server1", "duplicate_tool"),
];
let model_tools = normalize_tools_for_model(tools);
let model_tools =
normalize_tools_for_model_with_prefix(tools, /*prefix_mcp_tool_names*/ true);
// Only the first tool should remain, the second is skipped
assert_eq!(
model_tool_names(&model_tools),
HashSet::from([ToolName::namespaced("mcp__server1__", "duplicate_tool")])
HashSet::from([ToolName::namespaced("mcp__server1", "duplicate_tool")])
);
}
@@ -343,7 +347,8 @@ fn test_normalize_tools_long_names_same_server() {
),
];
let model_tools = normalize_tools_for_model(tools);
let model_tools =
normalize_tools_for_model_with_prefix(tools, /*prefix_mcp_tool_names*/ true);
assert_eq!(model_tools.len(), 2);
@@ -353,7 +358,7 @@ fn test_normalize_tools_long_names_same_server() {
assert!(
names
.iter()
.all(|name| name.namespace.as_deref() == Some("mcp__my_server__"))
.all(|name| name.namespace.as_deref() == Some("mcp__my_server"))
);
assert!(
names.iter().all(is_code_mode_compatible_tool_name),
@@ -365,14 +370,15 @@ fn test_normalize_tools_long_names_same_server() {
fn test_normalize_tools_sanitizes_invalid_characters() {
let tools = vec![create_test_tool("server.one", "tool.two-three")];
let model_tools = normalize_tools_for_model(tools);
let model_tools =
normalize_tools_for_model_with_prefix(tools, /*prefix_mcp_tool_names*/ true);
assert_eq!(model_tools.len(), 1);
let tool = model_tools.into_iter().next().expect("one tool");
let model_name = tool.canonical_tool_name();
assert_eq!(
model_name,
ToolName::namespaced("mcp__server_one__", "tool_two_three")
ToolName::namespaced("mcp__server_one", "tool_two_three")
);
assert_eq!(
ToolName::namespaced(tool.callable_namespace.clone(), tool.callable_name.clone()),
@@ -381,7 +387,7 @@ fn test_normalize_tools_sanitizes_invalid_characters() {
// The callable parts are sanitized for model-visible tool calls, but the raw
// MCP name is preserved for the actual MCP call.
assert_eq!(tool.server_name, "server.one");
assert_eq!(tool.callable_namespace, "mcp__server_one__");
assert_eq!(tool.callable_namespace, "mcp__server_one");
assert_eq!(tool.callable_name, "tool_two_three");
assert_eq!(tool.tool.name, "tool.two-three");
@@ -395,19 +401,59 @@ fn test_normalize_tools_sanitizes_invalid_characters() {
fn test_normalize_tools_keeps_hyphenated_mcp_tools_callable() {
let tools = vec![create_test_tool("music-studio", "get-strudel-guide")];
let model_tools = normalize_tools_for_model(tools);
let model_tools =
normalize_tools_for_model_with_prefix(tools, /*prefix_mcp_tool_names*/ true);
assert_eq!(model_tools.len(), 1);
let tool = model_tools.into_iter().next().expect("one tool");
assert_eq!(
tool.canonical_tool_name(),
ToolName::namespaced("mcp__music_studio__", "get_strudel_guide")
ToolName::namespaced("mcp__music_studio", "get_strudel_guide")
);
assert_eq!(tool.callable_namespace, "mcp__music_studio__");
assert_eq!(tool.callable_namespace, "mcp__music_studio");
assert_eq!(tool.callable_name, "get_strudel_guide");
assert_eq!(tool.tool.name, "get-strudel-guide");
}
#[test]
fn test_normalize_tools_disambiguates_reserved_unprefixed_namespaces() {
let tools = vec![
create_test_tool("tools", "list"),
create_test_tool("web", "search"),
];
let model_tools =
normalize_tools_for_model_with_prefix(tools, /*prefix_mcp_tool_names*/ false);
assert_eq!(model_tools.len(), 2);
let namespaces = model_tools
.iter()
.map(|tool| tool.callable_namespace.as_str())
.collect::<HashSet<_>>();
assert_eq!(namespaces.len(), 2);
assert!(
namespaces
.iter()
.all(|namespace| !matches!(*namespace, "tools" | "web")),
"reserved namespaces should be disambiguated: {namespaces:?}"
);
assert!(
namespaces
.iter()
.any(|namespace| namespace.starts_with("tools_"))
);
assert!(
namespaces
.iter()
.any(|namespace| namespace.starts_with("web_"))
);
let model_names = model_tool_names(&model_tools);
assert!(
model_names.iter().all(is_code_mode_compatible_tool_name),
"model-visible names must be code-mode compatible: {model_names:?}"
);
}
#[test]
fn test_normalize_tools_disambiguates_sanitized_namespace_collisions() {
let tools = vec![
@@ -415,7 +461,8 @@ fn test_normalize_tools_disambiguates_sanitized_namespace_collisions() {
create_test_tool("basic_server", "query"),
];
let model_tools = normalize_tools_for_model(tools);
let model_tools =
normalize_tools_for_model_with_prefix(tools, /*prefix_mcp_tool_names*/ true);
assert_eq!(model_tools.len(), 2);
let mut namespaces = model_tools
@@ -445,7 +492,8 @@ fn test_normalize_tools_disambiguates_sanitized_tool_name_collisions() {
create_test_tool("server", "tool_name"),
];
let model_tools = normalize_tools_for_model(tools);
let model_tools =
normalize_tools_for_model_with_prefix(tools, /*prefix_mcp_tool_names*/ true);
assert_eq!(model_tools.len(), 2);
let raw_tool_names = model_tools
@@ -691,8 +739,11 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
let mut manager = McpConnectionManager::new_uninitialized(
&approval_policy,
&permission_profile,
/*prefix_mcp_tool_names*/ true,
);
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient {
@@ -709,13 +760,97 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
.iter()
.find(|tool| {
tool.canonical_tool_name()
== ToolName::namespaced("mcp__codex_apps__", "calendar_create_event")
== ToolName::namespaced("mcp__codex_apps", "calendar_create_event")
})
.expect("tool from startup cache");
assert_eq!(tool.server_name, CODEX_APPS_MCP_SERVER_NAME);
assert_eq!(tool.callable_name, "calendar_create_event");
}
#[tokio::test]
async fn list_all_tools_accepts_canonical_namespaced_tool_names() {
let startup_tools = vec![create_test_tool("rmcp", "echo")];
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager = McpConnectionManager::new_uninitialized(
&approval_policy,
&permission_profile,
/*prefix_mcp_tool_names*/ false,
);
manager.clients.insert(
"rmcp".to_string(),
AsyncManagedClient {
client: pending_client,
startup_snapshot: Some(startup_tools),
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
cancel_token: CancellationToken::new(),
},
);
let tools = manager.list_all_tools().await;
let tool = tools
.iter()
.find(|tool| tool.canonical_tool_name() == ToolName::namespaced("rmcp", "echo"))
.expect("split MCP tool namespace and name should resolve");
let expected = ("rmcp", "rmcp", "echo", "echo");
assert_eq!(
(
tool.server_name.as_str(),
tool.callable_namespace.as_str(),
tool.callable_name.as_str(),
tool.tool.name.as_ref(),
),
expected
);
}
#[tokio::test]
async fn list_all_tools_applies_legacy_mcp_prefix_by_default() {
let startup_tools = vec![create_test_tool("rmcp", "echo")];
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager = McpConnectionManager::new_uninitialized(
&approval_policy,
&permission_profile,
/*prefix_mcp_tool_names*/ true,
);
manager.clients.insert(
"rmcp".to_string(),
AsyncManagedClient {
client: pending_client,
startup_snapshot: Some(startup_tools),
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
cancel_token: CancellationToken::new(),
},
);
let tools = manager.list_all_tools().await;
let tool = tools
.iter()
.find(|tool| tool.canonical_tool_name() == ToolName::namespaced("mcp__rmcp", "echo"))
.expect("legacy-prefixed MCP tool name should resolve");
let expected = ("rmcp", "mcp__rmcp", "echo", "echo");
assert_eq!(
(
tool.server_name.as_str(),
tool.callable_namespace.as_str(),
tool.callable_name.as_str(),
tool.tool.name.as_ref(),
),
expected
);
}
#[tokio::test]
async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot() {
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
@@ -723,8 +858,11 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot(
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
let mut manager = McpConnectionManager::new_uninitialized(
&approval_policy,
&permission_profile,
/*prefix_mcp_tool_names*/ true,
);
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient {
@@ -748,8 +886,11 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
let mut manager = McpConnectionManager::new_uninitialized(
&approval_policy,
&permission_profile,
/*prefix_mcp_tool_names*/ true,
);
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient {
@@ -782,8 +923,11 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
let mut manager = McpConnectionManager::new_uninitialized(
&approval_policy,
&permission_profile,
/*prefix_mcp_tool_names*/ true,
);
let startup_complete = Arc::new(std::sync::atomic::AtomicBool::new(true));
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
@@ -801,7 +945,7 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
.iter()
.find(|tool| {
tool.canonical_tool_name()
== ToolName::namespaced("mcp__codex_apps__", "calendar_create_event")
== ToolName::namespaced("mcp__codex_apps", "calendar_create_event")
})
.expect("tool from startup cache");
assert_eq!(tool.server_name, CODEX_APPS_MCP_SERVER_NAME);
@@ -817,8 +961,11 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() {
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
let mut manager = McpConnectionManager::new_uninitialized(
&approval_policy,
&permission_profile,
/*prefix_mcp_tool_names*/ true,
);
manager.server_metadata.insert(
server_name.to_string(),
McpServerMetadata {
@@ -927,6 +1074,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
is_workspace_account: false,
},
/*host_owned_codex_apps_enabled*/ false,
/*prefix_mcp_tool_names*/ true,
ElicitationCapability::default(),
ToolPluginProvenance::default(),
/*auth*/ None,
+5
View File
@@ -132,6 +132,9 @@ pub struct McpConfig {
/// ChatGPT auth is checked separately at runtime before the host-owned apps
/// MCP server is added.
pub apps_enabled: bool,
/// Whether model-visible MCP tool namespaces should keep the legacy
/// `mcp__` prefix.
pub prefix_mcp_tool_names: bool,
/// Client-side elicitation capabilities advertised during MCP initialization.
pub client_elicitation_capability: ElicitationCapability,
/// Config-backed MCP servers keyed by server name.
@@ -288,6 +291,7 @@ pub async fn read_mcp_resource(
config.codex_home.clone(),
codex_apps_tools_cache_key(auth),
host_owned_codex_apps_enabled,
config.prefix_mcp_tool_names,
config.client_elicitation_capability.clone(),
tool_plugin_provenance(config),
auth,
@@ -361,6 +365,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
config.codex_home.clone(),
codex_apps_tools_cache_key(auth),
host_owned_codex_apps_enabled,
config.prefix_mcp_tool_names,
config.client_elicitation_capability.clone(),
tool_plugin_provenance,
auth,
+1
View File
@@ -28,6 +28,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
apps_enabled: false,
prefix_mcp_tool_names: true,
client_elicitation_capability: ElicitationCapability::default(),
configured_mcp_servers: HashMap::new(),
plugin_ids_by_mcp_server_name: HashMap::new(),
+37 -6
View File
@@ -25,6 +25,8 @@ use crate::mcp::sanitize_responses_api_tool_name;
pub(crate) const MCP_TOOLS_CACHE_WRITE_DURATION_METRIC: &str =
"codex.mcp.tools.cache_write.duration_ms";
const LEGACY_MCP_TOOL_NAME_PREFIX: &str = "mcp__";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInfo {
/// Raw MCP server name used for routing the tool call.
@@ -141,7 +143,13 @@ pub(crate) fn filter_tools(tools: Vec<ToolInfo>, filter: &ToolFilter) -> Vec<Too
/// 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 name is unique and <= 64 bytes.
pub(crate) fn normalize_tools_for_model<I>(tools: I) -> Vec<ToolInfo>
///
/// When `prefix_mcp_tool_names` is true, the historical `mcp__` namespace
/// prefix is added without restoring the old trailing `__` namespace suffix.
pub(crate) fn normalize_tools_for_model_with_prefix<I>(
tools: I,
prefix_mcp_tool_names: bool,
) -> Vec<ToolInfo>
where
I: IntoIterator<Item = ToolInfo>,
{
@@ -163,8 +171,19 @@ where
continue;
}
let mut callable_namespace = callable_namespace_with_prefix(
&sanitize_responses_api_tool_name(&tool.callable_namespace),
prefix_mcp_tool_names,
);
if !prefix_mcp_tool_names
&& RESERVED_UNPREFIXED_MCP_NAMESPACES.contains(&callable_namespace.as_str())
{
callable_namespace =
append_namespace_hash_suffix(&callable_namespace, &raw_namespace_identity);
}
candidates.push(CallableToolCandidate {
callable_namespace: sanitize_responses_api_tool_name(&tool.callable_namespace),
callable_namespace,
callable_name: sanitize_responses_api_tool_name(&tool.callable_name),
raw_namespace_identity,
raw_tool_identity,
@@ -226,6 +245,7 @@ where
&candidate.callable_name,
&candidate.raw_tool_identity,
&mut used_names,
MCP_TOOL_NAME_DELIMITER.len(),
);
candidate.tool.callable_namespace = callable_namespace;
candidate.tool.callable_name = callable_name;
@@ -247,6 +267,15 @@ const MCP_TOOL_NAME_DELIMITER: &str = "__";
const MAX_TOOL_NAME_LENGTH: usize = 64;
const CALLABLE_NAME_HASH_LEN: usize = 12;
const META_OPENAI_FILE_PARAMS: &str = "openai/fileParams";
const RESERVED_UNPREFIXED_MCP_NAMESPACES: &[&str] = &["tools", "web"];
fn callable_namespace_with_prefix(namespace: &str, prefix_mcp_tool_names: bool) -> String {
if !prefix_mcp_tool_names || namespace.starts_with(LEGACY_MCP_TOOL_NAME_PREFIX) {
namespace.to_string()
} else {
format!("{LEGACY_MCP_TOOL_NAME_PREFIX}{namespace}")
}
}
fn mask_input_schema_for_file_path_params(input_schema: &mut JsonValue, file_params: &[String]) {
let Some(properties) = input_schema
@@ -331,9 +360,10 @@ fn fit_callable_parts_with_hash(
namespace: &str,
tool_name: &str,
raw_identity: &str,
reserved_len: usize,
) -> (String, String) {
let suffix = callable_name_hash_suffix(raw_identity);
let max_tool_len = MAX_TOOL_NAME_LENGTH.saturating_sub(namespace.len());
let max_tool_len = MAX_TOOL_NAME_LENGTH.saturating_sub(namespace.len() + reserved_len);
if max_tool_len >= suffix.len() {
let prefix_len = max_tool_len - suffix.len();
return (
@@ -342,7 +372,7 @@ fn fit_callable_parts_with_hash(
);
}
let max_namespace_len = MAX_TOOL_NAME_LENGTH - suffix.len();
let max_namespace_len = MAX_TOOL_NAME_LENGTH.saturating_sub(suffix.len() + reserved_len);
(truncate_name(namespace, max_namespace_len), suffix)
}
@@ -351,9 +381,10 @@ fn unique_callable_parts(
tool_name: &str,
raw_identity: &str,
used_names: &mut HashSet<String>,
reserved_len: usize,
) -> (String, String) {
let model_name = format!("{namespace}{tool_name}");
if model_name.len() <= MAX_TOOL_NAME_LENGTH && used_names.insert(model_name) {
if model_name.len() + reserved_len <= MAX_TOOL_NAME_LENGTH && used_names.insert(model_name) {
return (namespace.to_string(), tool_name.to_string());
}
@@ -365,7 +396,7 @@ fn unique_callable_parts(
format!("{raw_identity}\0{attempt}")
};
let (namespace, tool_name) =
fit_callable_parts_with_hash(namespace, tool_name, &hash_input);
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) {
return (namespace, tool_name);