From 58fecf7c432ac35a71e036c5c794bb71f99205e9 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 27 Apr 2026 01:11:45 +0300 Subject: [PATCH] Extract MCP Codex Apps support Move Codex Apps tool cache, cache key, connector filtering, and callable-name normalization into codex-mcp/src/apps.rs. Update client, manager, exports, and tests to import those helpers from the new module. Co-authored-by: Codex --- codex-rs/codex-mcp/src/apps.rs | 250 +++++++++++++++++ codex-rs/codex-mcp/src/client.rs | 18 +- codex-rs/codex-mcp/src/lib.rs | 7 +- codex-rs/codex-mcp/src/manager.rs | 6 +- codex-rs/codex-mcp/src/mcp/mod.rs | 2 +- codex-rs/codex-mcp/src/mcp_connection.rs | 253 +----------------- .../src/mcp_connection_manager_tests.rs | 10 +- 7 files changed, 274 insertions(+), 272 deletions(-) create mode 100644 codex-rs/codex-mcp/src/apps.rs diff --git a/codex-rs/codex-mcp/src/apps.rs b/codex-rs/codex-mcp/src/apps.rs new file mode 100644 index 0000000000..f54ab070ba --- /dev/null +++ b/codex-rs/codex-mcp/src/apps.rs @@ -0,0 +1,250 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::time::Instant; + +use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; +use crate::mcp_connection::MCP_TOOLS_CACHE_WRITE_DURATION_METRIC; +use crate::mcp_connection::ToolInfo; +use crate::mcp_connection::emit_duration; +use codex_login::CodexAuth; +use codex_utils_plugins::mcp_connector::is_connector_id_allowed; +use codex_utils_plugins::mcp_connector::sanitize_name; +use serde::Deserialize; +use serde::Serialize; +use sha1::Digest; +use sha1::Sha1; + +pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 2; +const CODEX_APPS_TOOLS_CACHE_DIR: &str = "cache/codex_apps_tools"; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CodexAppsToolsCacheKey { + pub(crate) account_id: Option, + pub(crate) chatgpt_user_id: Option, + pub(crate) is_workspace_account: bool, +} + +pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCacheKey { + CodexAppsToolsCacheKey { + account_id: auth.and_then(CodexAuth::get_account_id), + chatgpt_user_id: auth.and_then(CodexAuth::get_chatgpt_user_id), + is_workspace_account: auth.is_some_and(CodexAuth::is_workspace_account), + } +} + +pub fn filter_non_codex_apps_mcp_tools_only( + mcp_tools: &HashMap, +) -> HashMap { + mcp_tools + .iter() + .filter(|(_, tool)| tool.server_name != CODEX_APPS_MCP_SERVER_NAME) + .map(|(name, tool)| (name.clone(), tool.clone())) + .collect() +} + +fn sha1_hex(s: &str) -> String { + let mut hasher = Sha1::new(); + hasher.update(s.as_bytes()); + let sha1 = hasher.finalize(); + format!("{sha1:x}") +} + +#[derive(Clone)] +pub(crate) struct CodexAppsToolsCacheContext { + pub(crate) codex_home: PathBuf, + pub(crate) user_key: CodexAppsToolsCacheKey, +} + +impl CodexAppsToolsCacheContext { + pub(crate) fn cache_path(&self) -> PathBuf { + let user_key_json = serde_json::to_string(&self.user_key).unwrap_or_default(); + let user_key_hash = sha1_hex(&user_key_json); + self.codex_home + .join(CODEX_APPS_TOOLS_CACHE_DIR) + .join(format!("{user_key_hash}.json")) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CodexAppsToolsDiskCache { + schema_version: u8, + tools: Vec, +} + +pub(crate) enum CachedCodexAppsToolsLoad { + Hit(Vec), + Missing, + Invalid, +} + +pub(crate) fn normalize_codex_apps_tool_title( + server_name: &str, + connector_name: Option<&str>, + value: &str, +) -> String { + if server_name != CODEX_APPS_MCP_SERVER_NAME { + return value.to_string(); + } + + let Some(connector_name) = connector_name + .map(str::trim) + .filter(|name| !name.is_empty()) + else { + return value.to_string(); + }; + + let prefix = format!("{connector_name}_"); + if let Some(stripped) = value.strip_prefix(&prefix) + && !stripped.is_empty() + { + return stripped.to_string(); + } + + value.to_string() +} + +pub(crate) fn normalize_codex_apps_callable_name( + server_name: &str, + tool_name: &str, + connector_id: Option<&str>, + connector_name: Option<&str>, +) -> String { + if server_name != CODEX_APPS_MCP_SERVER_NAME { + return tool_name.to_string(); + } + + let tool_name = sanitize_name(tool_name); + + if let Some(connector_name) = connector_name + .map(str::trim) + .map(sanitize_name) + .filter(|name| !name.is_empty()) + && let Some(stripped) = tool_name.strip_prefix(&connector_name) + && !stripped.is_empty() + { + return stripped.to_string(); + } + + if let Some(connector_id) = connector_id + .map(str::trim) + .map(sanitize_name) + .filter(|name| !name.is_empty()) + && let Some(stripped) = tool_name.strip_prefix(&connector_id) + && !stripped.is_empty() + { + return stripped.to_string(); + } + + tool_name +} + +pub(crate) fn normalize_codex_apps_callable_namespace( + server_name: &str, + connector_name: Option<&str>, +) -> String { + if server_name == CODEX_APPS_MCP_SERVER_NAME + && let Some(connector_name) = connector_name + { + format!("mcp__{}__{}", server_name, sanitize_name(connector_name)) + } else { + format!("mcp__{server_name}__") + } +} + +pub(crate) fn write_cached_codex_apps_tools_if_needed( + server_name: &str, + cache_context: Option<&CodexAppsToolsCacheContext>, + tools: &[ToolInfo], +) { + if server_name != CODEX_APPS_MCP_SERVER_NAME { + return; + } + + if let Some(cache_context) = cache_context { + let cache_write_start = Instant::now(); + write_cached_codex_apps_tools(cache_context, tools); + emit_duration( + MCP_TOOLS_CACHE_WRITE_DURATION_METRIC, + cache_write_start.elapsed(), + &[], + ); + } +} + +pub(crate) fn load_startup_cached_codex_apps_tools_snapshot( + server_name: &str, + cache_context: Option<&CodexAppsToolsCacheContext>, +) -> Option> { + if server_name != CODEX_APPS_MCP_SERVER_NAME { + return None; + } + + let cache_context = cache_context?; + + match load_cached_codex_apps_tools(cache_context) { + CachedCodexAppsToolsLoad::Hit(tools) => Some(tools), + CachedCodexAppsToolsLoad::Missing | CachedCodexAppsToolsLoad::Invalid => None, + } +} + +#[cfg(test)] +pub(crate) fn read_cached_codex_apps_tools( + cache_context: &CodexAppsToolsCacheContext, +) -> Option> { + match load_cached_codex_apps_tools(cache_context) { + CachedCodexAppsToolsLoad::Hit(tools) => Some(tools), + CachedCodexAppsToolsLoad::Missing | CachedCodexAppsToolsLoad::Invalid => None, + } +} + +pub(crate) fn load_cached_codex_apps_tools( + cache_context: &CodexAppsToolsCacheContext, +) -> CachedCodexAppsToolsLoad { + let cache_path = cache_context.cache_path(); + let bytes = match std::fs::read(cache_path) { + Ok(bytes) => bytes, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return CachedCodexAppsToolsLoad::Missing; + } + Err(_) => return CachedCodexAppsToolsLoad::Invalid, + }; + let cache: CodexAppsToolsDiskCache = match serde_json::from_slice(&bytes) { + Ok(cache) => cache, + Err(_) => return CachedCodexAppsToolsLoad::Invalid, + }; + if cache.schema_version != CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION { + return CachedCodexAppsToolsLoad::Invalid; + } + CachedCodexAppsToolsLoad::Hit(filter_disallowed_codex_apps_tools(cache.tools)) +} + +pub(crate) fn write_cached_codex_apps_tools( + cache_context: &CodexAppsToolsCacheContext, + tools: &[ToolInfo], +) { + let cache_path = cache_context.cache_path(); + if let Some(parent) = cache_path.parent() + && std::fs::create_dir_all(parent).is_err() + { + return; + } + let tools = filter_disallowed_codex_apps_tools(tools.to_vec()); + let Ok(bytes) = serde_json::to_vec_pretty(&CodexAppsToolsDiskCache { + schema_version: CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION, + tools, + }) else { + return; + }; + let _ = std::fs::write(cache_path, bytes); +} + +pub(crate) fn filter_disallowed_codex_apps_tools(tools: Vec) -> Vec { + tools + .into_iter() + .filter(|tool| { + tool.connector_id + .as_deref() + .is_none_or(is_connector_id_allowed) + }) + .collect() +} diff --git a/codex-rs/codex-mcp/src/client.rs b/codex-rs/codex-mcp/src/client.rs index 8ea17a5a45..62635a1018 100644 --- a/codex-rs/codex-mcp/src/client.rs +++ b/codex-rs/codex-mcp/src/client.rs @@ -8,11 +8,18 @@ use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; +use crate::apps::CachedCodexAppsToolsLoad; +use crate::apps::CodexAppsToolsCacheContext; +use crate::apps::filter_disallowed_codex_apps_tools; +use crate::apps::load_cached_codex_apps_tools; +use crate::apps::load_startup_cached_codex_apps_tools_snapshot; +use crate::apps::normalize_codex_apps_callable_name; +use crate::apps::normalize_codex_apps_callable_namespace; +use crate::apps::normalize_codex_apps_tool_title; +use crate::apps::write_cached_codex_apps_tools_if_needed; use crate::elicitation::ElicitationRequestManager; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::ToolPluginProvenance; -use crate::mcp_connection::CachedCodexAppsToolsLoad; -use crate::mcp_connection::CodexAppsToolsCacheContext; use crate::mcp_connection::DEFAULT_STARTUP_TIMEOUT; use crate::mcp_connection::DEFAULT_TOOL_TIMEOUT; use crate::mcp_connection::MCP_SANDBOX_STATE_META_CAPABILITY; @@ -20,17 +27,10 @@ use crate::mcp_connection::McpRuntimeEnvironment; use crate::mcp_connection::ToolFilter; use crate::mcp_connection::ToolInfo; use crate::mcp_connection::emit_duration; -use crate::mcp_connection::filter_disallowed_codex_apps_tools; use crate::mcp_connection::filter_tools; -use crate::mcp_connection::load_cached_codex_apps_tools; -use crate::mcp_connection::load_startup_cached_codex_apps_tools_snapshot; -use crate::mcp_connection::normalize_codex_apps_callable_name; -use crate::mcp_connection::normalize_codex_apps_callable_namespace; -use crate::mcp_connection::normalize_codex_apps_tool_title; use crate::mcp_connection::resolve_bearer_token; use crate::mcp_connection::tool_with_model_visible_input_schema; use crate::mcp_connection::validate_mcp_server_name; -use crate::mcp_connection::write_cached_codex_apps_tools_if_needed; use anyhow::Result; use anyhow::anyhow; use async_channel::Sender; diff --git a/codex-rs/codex-mcp/src/lib.rs b/codex-rs/codex-mcp/src/lib.rs index b603571ef3..f1a62a873b 100644 --- a/codex-rs/codex-mcp/src/lib.rs +++ b/codex-rs/codex-mcp/src/lib.rs @@ -8,8 +8,8 @@ pub use mcp::CODEX_APPS_MCP_SERVER_NAME; pub use mcp::McpConfig; pub use mcp::ToolPluginProvenance; -pub use mcp_connection::CodexAppsToolsCacheKey; -pub use mcp_connection::codex_apps_tools_cache_key; +pub use apps::CodexAppsToolsCacheKey; +pub use apps::codex_apps_tools_cache_key; pub use mcp::configured_mcp_servers; pub use mcp::effective_mcp_servers; @@ -33,11 +33,12 @@ pub use mcp::oauth_login_support; pub use mcp::resolve_oauth_scopes; pub use mcp::should_retry_without_scopes; +pub use apps::filter_non_codex_apps_mcp_tools_only; pub use mcp::mcp_permission_prompt_is_auto_approved; pub use mcp::qualified_mcp_tool_name_prefix; pub use mcp_connection::declared_openai_file_input_param_names; -pub use mcp_connection::filter_non_codex_apps_mcp_tools_only; +pub(crate) mod apps; pub(crate) mod client; pub(crate) mod elicitation; pub(crate) mod manager; diff --git a/codex-rs/codex-mcp/src/manager.rs b/codex-rs/codex-mcp/src/manager.rs index f8189e492c..de67ffd80e 100644 --- a/codex-rs/codex-mcp/src/manager.rs +++ b/codex-rs/codex-mcp/src/manager.rs @@ -5,6 +5,9 @@ use std::time::Duration; use std::time::Instant; use crate::McpAuthStatusEntry; +use crate::apps::CodexAppsToolsCacheContext; +use crate::apps::CodexAppsToolsCacheKey; +use crate::apps::write_cached_codex_apps_tools_if_needed; use crate::client::AsyncManagedClient; use crate::client::MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC; use crate::client::MCP_TOOLS_LIST_DURATION_METRIC; @@ -14,8 +17,6 @@ use crate::client::list_tools_for_client_uncached; use crate::elicitation::ElicitationRequestManager; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::ToolPluginProvenance; -use crate::mcp_connection::CodexAppsToolsCacheContext; -use crate::mcp_connection::CodexAppsToolsCacheKey; use crate::mcp_connection::McpRuntimeEnvironment; use crate::mcp_connection::ToolInfo; use crate::mcp_connection::emit_duration; @@ -26,7 +27,6 @@ use crate::mcp_connection::qualify_tools; use crate::mcp_connection::startup_outcome_error_message; use crate::mcp_connection::tool_with_model_visible_input_schema; use crate::mcp_connection::transport_origin; -use crate::mcp_connection::write_cached_codex_apps_tools_if_needed; use anyhow::Context; use anyhow::Result; use anyhow::anyhow; diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index a03165870e..d5af89387c 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -34,9 +34,9 @@ use rmcp::model::ReadResourceRequestParams; use rmcp::model::ReadResourceResult; use serde_json::Value; +use crate::apps::codex_apps_tools_cache_key; use crate::manager::McpConnectionManager; use crate::mcp_connection::McpRuntimeEnvironment; -use crate::mcp_connection::codex_apps_tools_cache_key; pub const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps"; const MCP_TOOL_NAME_PREFIX: &str = "mcp"; diff --git a/codex-rs/codex-mcp/src/mcp_connection.rs b/codex-rs/codex-mcp/src/mcp_connection.rs index c694f53a5c..99ad8766cb 100644 --- a/codex-rs/codex-mcp/src/mcp_connection.rs +++ b/codex-rs/codex-mcp/src/mcp_connection.rs @@ -8,11 +8,9 @@ use std::env; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use std::time::Instant; use crate::McpAuthStatusEntry; use crate::client::StartupOutcomeError; -use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; pub(crate) use crate::mcp_tool_names::qualify_tools; use anyhow::Result; use anyhow::anyhow; @@ -30,18 +28,10 @@ use serde::Deserialize; use serde::Serialize; use serde_json::Map; use serde_json::Value as JsonValue; -use sha1::Digest; -use sha1::Sha1; use url::Url; use codex_config::McpServerConfig; use codex_config::McpServerTransportConfig; -use codex_login::CodexAuth; -use codex_utils_plugins::mcp_connector::is_connector_id_allowed; -use codex_utils_plugins::mcp_connector::sanitize_name; - -/// Delimiter used to separate MCP tool-name parts. -const MCP_TOOL_NAME_DELIMITER: &str = "__"; /// Default timeout for initializing MCP server & initially listing tools. pub(crate) const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30); @@ -49,9 +39,8 @@ pub(crate) const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30); /// Default timeout for individual tool calls. pub(crate) const DEFAULT_TOOL_TIMEOUT: Duration = Duration::from_secs(120); -pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 2; -const CODEX_APPS_TOOLS_CACHE_DIR: &str = "cache/codex_apps_tools"; -const MCP_TOOLS_CACHE_WRITE_DURATION_METRIC: &str = "codex.mcp.tools.cache_write.duration_ms"; +pub(crate) const MCP_TOOLS_CACHE_WRITE_DURATION_METRIC: &str = + "codex.mcp.tools.cache_write.duration_ms"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolInfo { @@ -98,31 +87,6 @@ pub fn declared_openai_file_input_param_names( .collect() } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CodexAppsToolsCacheKey { - pub(crate) account_id: Option, - pub(crate) chatgpt_user_id: Option, - pub(crate) is_workspace_account: bool, -} - -pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCacheKey { - CodexAppsToolsCacheKey { - account_id: auth.and_then(CodexAuth::get_account_id), - chatgpt_user_id: auth.and_then(CodexAuth::get_chatgpt_user_id), - is_workspace_account: auth.is_some_and(CodexAuth::is_workspace_account), - } -} - -pub fn filter_non_codex_apps_mcp_tools_only( - mcp_tools: &HashMap, -) -> HashMap { - mcp_tools - .iter() - .filter(|(_, tool)| tool.server_name != CODEX_APPS_MCP_SERVER_NAME) - .map(|(name, tool)| (name.clone(), tool.clone())) - .collect() -} - /// MCP server capability indicating that Codex should include [`SandboxState`] /// in tool-call request `_meta` under this key. pub const MCP_SANDBOX_STATE_META_CAPABILITY: &str = "codex/sandbox-state-meta"; @@ -205,41 +169,6 @@ impl ToolFilter { } } -fn sha1_hex(s: &str) -> String { - let mut hasher = Sha1::new(); - hasher.update(s.as_bytes()); - let sha1 = hasher.finalize(); - format!("{sha1:x}") -} - -#[derive(Clone)] -pub(crate) struct CodexAppsToolsCacheContext { - pub(crate) codex_home: PathBuf, - pub(crate) user_key: CodexAppsToolsCacheKey, -} - -impl CodexAppsToolsCacheContext { - pub(crate) fn cache_path(&self) -> PathBuf { - let user_key_json = serde_json::to_string(&self.user_key).unwrap_or_default(); - let user_key_hash = sha1_hex(&user_key_json); - self.codex_home - .join(CODEX_APPS_TOOLS_CACHE_DIR) - .join(format!("{user_key_hash}.json")) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct CodexAppsToolsDiskCache { - schema_version: u8, - tools: Vec, -} - -pub(crate) enum CachedCodexAppsToolsLoad { - Hit(Vec), - Missing, - Invalid, -} - const META_OPENAI_FILE_PARAMS: &str = "openai/fileParams"; /// Returns the model-visible view of a tool while preserving the raw metadata @@ -326,86 +255,6 @@ pub(crate) fn filter_tools(tools: Vec, filter: &ToolFilter) -> Vec, - value: &str, -) -> String { - if server_name != CODEX_APPS_MCP_SERVER_NAME { - return value.to_string(); - } - - let Some(connector_name) = connector_name - .map(str::trim) - .filter(|name| !name.is_empty()) - else { - return value.to_string(); - }; - - let prefix = format!("{connector_name}_"); - if let Some(stripped) = value.strip_prefix(&prefix) - && !stripped.is_empty() - { - return stripped.to_string(); - } - - value.to_string() -} - -pub(crate) fn normalize_codex_apps_callable_name( - server_name: &str, - tool_name: &str, - connector_id: Option<&str>, - connector_name: Option<&str>, -) -> String { - if server_name != CODEX_APPS_MCP_SERVER_NAME { - return tool_name.to_string(); - } - - let tool_name = sanitize_name(tool_name); - - if let Some(connector_name) = connector_name - .map(str::trim) - .map(sanitize_name) - .filter(|name| !name.is_empty()) - && let Some(stripped) = tool_name.strip_prefix(&connector_name) - && !stripped.is_empty() - { - return stripped.to_string(); - } - - if let Some(connector_id) = connector_id - .map(str::trim) - .map(sanitize_name) - .filter(|name| !name.is_empty()) - && let Some(stripped) = tool_name.strip_prefix(&connector_id) - && !stripped.is_empty() - { - return stripped.to_string(); - } - - tool_name -} - -pub(crate) fn normalize_codex_apps_callable_namespace( - server_name: &str, - connector_name: Option<&str>, -) -> String { - if server_name == CODEX_APPS_MCP_SERVER_NAME - && let Some(connector_name) = connector_name - { - format!( - "mcp{}{}{}{}", - MCP_TOOL_NAME_DELIMITER, - server_name, - MCP_TOOL_NAME_DELIMITER, - sanitize_name(connector_name) - ) - } else { - format!("mcp{MCP_TOOL_NAME_DELIMITER}{server_name}{MCP_TOOL_NAME_DELIMITER}") - } -} - pub(crate) fn resolve_bearer_token( server_name: &str, bearer_token_env_var: Option<&str>, @@ -433,104 +282,6 @@ pub(crate) fn resolve_bearer_token( } } -pub(crate) fn write_cached_codex_apps_tools_if_needed( - server_name: &str, - cache_context: Option<&CodexAppsToolsCacheContext>, - tools: &[ToolInfo], -) { - if server_name != CODEX_APPS_MCP_SERVER_NAME { - return; - } - - if let Some(cache_context) = cache_context { - let cache_write_start = Instant::now(); - write_cached_codex_apps_tools(cache_context, tools); - emit_duration( - MCP_TOOLS_CACHE_WRITE_DURATION_METRIC, - cache_write_start.elapsed(), - &[], - ); - } -} - -pub(crate) fn load_startup_cached_codex_apps_tools_snapshot( - server_name: &str, - cache_context: Option<&CodexAppsToolsCacheContext>, -) -> Option> { - if server_name != CODEX_APPS_MCP_SERVER_NAME { - return None; - } - - let cache_context = cache_context?; - - match load_cached_codex_apps_tools(cache_context) { - CachedCodexAppsToolsLoad::Hit(tools) => Some(tools), - CachedCodexAppsToolsLoad::Missing | CachedCodexAppsToolsLoad::Invalid => None, - } -} - -#[cfg(test)] -pub(crate) fn read_cached_codex_apps_tools( - cache_context: &CodexAppsToolsCacheContext, -) -> Option> { - match load_cached_codex_apps_tools(cache_context) { - CachedCodexAppsToolsLoad::Hit(tools) => Some(tools), - CachedCodexAppsToolsLoad::Missing | CachedCodexAppsToolsLoad::Invalid => None, - } -} - -pub(crate) fn load_cached_codex_apps_tools( - cache_context: &CodexAppsToolsCacheContext, -) -> CachedCodexAppsToolsLoad { - let cache_path = cache_context.cache_path(); - let bytes = match std::fs::read(cache_path) { - Ok(bytes) => bytes, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return CachedCodexAppsToolsLoad::Missing; - } - Err(_) => return CachedCodexAppsToolsLoad::Invalid, - }; - let cache: CodexAppsToolsDiskCache = match serde_json::from_slice(&bytes) { - Ok(cache) => cache, - Err(_) => return CachedCodexAppsToolsLoad::Invalid, - }; - if cache.schema_version != CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION { - return CachedCodexAppsToolsLoad::Invalid; - } - CachedCodexAppsToolsLoad::Hit(filter_disallowed_codex_apps_tools(cache.tools)) -} - -pub(crate) fn write_cached_codex_apps_tools( - cache_context: &CodexAppsToolsCacheContext, - tools: &[ToolInfo], -) { - let cache_path = cache_context.cache_path(); - if let Some(parent) = cache_path.parent() - && std::fs::create_dir_all(parent).is_err() - { - return; - } - let tools = filter_disallowed_codex_apps_tools(tools.to_vec()); - let Ok(bytes) = serde_json::to_vec_pretty(&CodexAppsToolsDiskCache { - schema_version: CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION, - tools, - }) else { - return; - }; - let _ = std::fs::write(cache_path, bytes); -} - -pub(crate) fn filter_disallowed_codex_apps_tools(tools: Vec) -> Vec { - tools - .into_iter() - .filter(|tool| { - tool.connector_id - .as_deref() - .is_none_or(is_connector_id_allowed) - }) - .collect() -} - pub(crate) fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &str)]) { if let Some(metrics) = codex_otel::global() { let _ = metrics.record_duration(metric, duration, tags); diff --git a/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs b/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs index d3d3bf7c22..4e4ec55a27 100644 --- a/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs @@ -1,4 +1,9 @@ use super::*; +use crate::apps::CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION; +use crate::apps::CodexAppsToolsCacheContext; +use crate::apps::load_startup_cached_codex_apps_tools_snapshot; +use crate::apps::read_cached_codex_apps_tools; +use crate::apps::write_cached_codex_apps_tools; use crate::client::AsyncManagedClient; use crate::client::ManagedClient; use crate::client::StartupOutcomeError; @@ -6,16 +11,11 @@ use crate::client::elicitation_capability_for_server; use crate::declared_openai_file_input_param_names; use crate::elicitation::ElicitationRequestManager; use crate::elicitation::elicitation_is_rejected_by_policy; -use crate::mcp_connection::CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION; -use crate::mcp_connection::CodexAppsToolsCacheContext; use crate::mcp_connection::ToolFilter; use crate::mcp_connection::ToolInfo; use crate::mcp_connection::filter_tools; -use crate::mcp_connection::load_startup_cached_codex_apps_tools_snapshot; -use crate::mcp_connection::read_cached_codex_apps_tools; use crate::mcp_connection::tool_with_model_visible_input_schema; use crate::mcp_connection::transport_origin; -use crate::mcp_connection::write_cached_codex_apps_tools; use codex_config::Constrained; use codex_protocol::ToolName; use codex_protocol::protocol::GranularApprovalConfig;