From acdbd8edc508c19f6bfe58d464eeb1f459fb7585 Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Wed, 4 Feb 2026 13:51:57 -0800 Subject: [PATCH] [apps] Cache MCP actions from apps. (#10662) MCP actions take a long time to load for users with lots of apps installed. Adding a cache for these actions with 1hr expiration, given that they are almost always aren't going to change unless people install another app, which means they also need to restart codex to pick it up. --- codex-rs/core/src/mcp_connection_manager.rs | 83 +++++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 456114c30..d6447738c 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -12,7 +12,10 @@ use std::env; use std::ffi::OsString; use std::path::PathBuf; use std::sync::Arc; +use std::sync::LazyLock; +use std::sync::Mutex as StdMutex; use std::time::Duration; +use std::time::Instant; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::auth::McpAuthStatusEntry; @@ -83,6 +86,8 @@ pub const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(10); /// Default timeout for individual tool calls. const DEFAULT_TOOL_TIMEOUT: Duration = Duration::from_secs(60); +const CODEX_APPS_TOOLS_CACHE_TTL: Duration = Duration::from_secs(3600); + /// The Responses API requires tool names to match `^[a-zA-Z0-9_-]+$`. /// MCP server/tool names are user-controlled, so sanitize the fully-qualified /// name we expose to the model by replacing any disallowed character with `_`. @@ -161,6 +166,15 @@ pub(crate) struct ToolInfo { pub(crate) connector_name: Option, } +#[derive(Clone)] +struct CachedCodexAppsTools { + expires_at: Instant, + tools: Vec, +} + +static CODEX_APPS_TOOLS_CACHE: LazyLock>> = + LazyLock::new(|| StdMutex::new(None)); + type ResponderMap = HashMap<(String, RequestId), oneshot::Sender>; #[derive(Clone, Default)] @@ -465,13 +479,28 @@ impl McpConnectionManager { #[instrument(level = "trace", skip_all)] pub async fn list_all_tools(&self) -> HashMap { let mut tools = HashMap::new(); - for managed_client in self.clients.values() { + for (server_name, managed_client) in &self.clients { let client = managed_client.client().await.ok(); if let Some(client) = client { - tools.extend(qualify_tools(filter_tools( - client.tools, - client.tool_filter, - ))); + let rmcp_client = client.client; + let tool_timeout = client.tool_timeout; + let tool_filter = client.tool_filter; + let mut server_tools = client.tools; + + if server_name == CODEX_APPS_MCP_SERVER_NAME { + match list_tools_for_client(server_name, &rmcp_client, tool_timeout).await { + Ok(fresh_or_cached_tools) => { + server_tools = fresh_or_cached_tools; + } + Err(err) => { + warn!( + "Failed to refresh tools for MCP server '{server_name}', using startup snapshot: {err:#}" + ); + } + } + } + + tools.extend(qualify_tools(filter_tools(server_tools, tool_filter))); } } tools @@ -965,6 +994,50 @@ async fn list_tools_for_client( server_name: &str, client: &Arc, timeout: Option, +) -> Result> { + if server_name == CODEX_APPS_MCP_SERVER_NAME + && let Some(cached_tools) = read_cached_codex_apps_tools() + { + return Ok(cached_tools); + } + + let tools = list_tools_for_client_uncached(server_name, client, timeout).await?; + if server_name == CODEX_APPS_MCP_SERVER_NAME { + write_cached_codex_apps_tools(&tools); + } + Ok(tools) +} + +fn read_cached_codex_apps_tools() -> Option> { + let mut cache_guard = CODEX_APPS_TOOLS_CACHE + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let now = Instant::now(); + + if let Some(cached) = cache_guard.as_ref() + && now < cached.expires_at + { + return Some(cached.tools.clone()); + } + + *cache_guard = None; + None +} + +fn write_cached_codex_apps_tools(tools: &[ToolInfo]) { + let mut cache_guard = CODEX_APPS_TOOLS_CACHE + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + *cache_guard = Some(CachedCodexAppsTools { + expires_at: Instant::now() + CODEX_APPS_TOOLS_CACHE_TTL, + tools: tools.to_vec(), + }); +} + +async fn list_tools_for_client_uncached( + server_name: &str, + client: &Arc, + timeout: Option, ) -> Result> { let resp = client.list_tools_with_connector_ids(None, timeout).await?; Ok(resp