Skip to content

Commit acdbd8e

Browse files
authored
[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.
1 parent d589ee0 commit acdbd8e

File tree

1 file changed

+78
-5
lines changed

1 file changed

+78
-5
lines changed

codex-rs/core/src/mcp_connection_manager.rs

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use std::env;
1212
use std::ffi::OsString;
1313
use std::path::PathBuf;
1414
use std::sync::Arc;
15+
use std::sync::LazyLock;
16+
use std::sync::Mutex as StdMutex;
1517
use std::time::Duration;
18+
use std::time::Instant;
1619

1720
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
1821
use crate::mcp::auth::McpAuthStatusEntry;
@@ -83,6 +86,8 @@ pub const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(10);
8386
/// Default timeout for individual tool calls.
8487
const DEFAULT_TOOL_TIMEOUT: Duration = Duration::from_secs(60);
8588

89+
const CODEX_APPS_TOOLS_CACHE_TTL: Duration = Duration::from_secs(3600);
90+
8691
/// The Responses API requires tool names to match `^[a-zA-Z0-9_-]+$`.
8792
/// MCP server/tool names are user-controlled, so sanitize the fully-qualified
8893
/// name we expose to the model by replacing any disallowed character with `_`.
@@ -161,6 +166,15 @@ pub(crate) struct ToolInfo {
161166
pub(crate) connector_name: Option<String>,
162167
}
163168

169+
#[derive(Clone)]
170+
struct CachedCodexAppsTools {
171+
expires_at: Instant,
172+
tools: Vec<ToolInfo>,
173+
}
174+
175+
static CODEX_APPS_TOOLS_CACHE: LazyLock<StdMutex<Option<CachedCodexAppsTools>>> =
176+
LazyLock::new(|| StdMutex::new(None));
177+
164178
type ResponderMap = HashMap<(String, RequestId), oneshot::Sender<ElicitationResponse>>;
165179

166180
#[derive(Clone, Default)]
@@ -465,13 +479,28 @@ impl McpConnectionManager {
465479
#[instrument(level = "trace", skip_all)]
466480
pub async fn list_all_tools(&self) -> HashMap<String, ToolInfo> {
467481
let mut tools = HashMap::new();
468-
for managed_client in self.clients.values() {
482+
for (server_name, managed_client) in &self.clients {
469483
let client = managed_client.client().await.ok();
470484
if let Some(client) = client {
471-
tools.extend(qualify_tools(filter_tools(
472-
client.tools,
473-
client.tool_filter,
474-
)));
485+
let rmcp_client = client.client;
486+
let tool_timeout = client.tool_timeout;
487+
let tool_filter = client.tool_filter;
488+
let mut server_tools = client.tools;
489+
490+
if server_name == CODEX_APPS_MCP_SERVER_NAME {
491+
match list_tools_for_client(server_name, &rmcp_client, tool_timeout).await {
492+
Ok(fresh_or_cached_tools) => {
493+
server_tools = fresh_or_cached_tools;
494+
}
495+
Err(err) => {
496+
warn!(
497+
"Failed to refresh tools for MCP server '{server_name}', using startup snapshot: {err:#}"
498+
);
499+
}
500+
}
501+
}
502+
503+
tools.extend(qualify_tools(filter_tools(server_tools, tool_filter)));
475504
}
476505
}
477506
tools
@@ -965,6 +994,50 @@ async fn list_tools_for_client(
965994
server_name: &str,
966995
client: &Arc<RmcpClient>,
967996
timeout: Option<Duration>,
997+
) -> Result<Vec<ToolInfo>> {
998+
if server_name == CODEX_APPS_MCP_SERVER_NAME
999+
&& let Some(cached_tools) = read_cached_codex_apps_tools()
1000+
{
1001+
return Ok(cached_tools);
1002+
}
1003+
1004+
let tools = list_tools_for_client_uncached(server_name, client, timeout).await?;
1005+
if server_name == CODEX_APPS_MCP_SERVER_NAME {
1006+
write_cached_codex_apps_tools(&tools);
1007+
}
1008+
Ok(tools)
1009+
}
1010+
1011+
fn read_cached_codex_apps_tools() -> Option<Vec<ToolInfo>> {
1012+
let mut cache_guard = CODEX_APPS_TOOLS_CACHE
1013+
.lock()
1014+
.unwrap_or_else(std::sync::PoisonError::into_inner);
1015+
let now = Instant::now();
1016+
1017+
if let Some(cached) = cache_guard.as_ref()
1018+
&& now < cached.expires_at
1019+
{
1020+
return Some(cached.tools.clone());
1021+
}
1022+
1023+
*cache_guard = None;
1024+
None
1025+
}
1026+
1027+
fn write_cached_codex_apps_tools(tools: &[ToolInfo]) {
1028+
let mut cache_guard = CODEX_APPS_TOOLS_CACHE
1029+
.lock()
1030+
.unwrap_or_else(std::sync::PoisonError::into_inner);
1031+
*cache_guard = Some(CachedCodexAppsTools {
1032+
expires_at: Instant::now() + CODEX_APPS_TOOLS_CACHE_TTL,
1033+
tools: tools.to_vec(),
1034+
});
1035+
}
1036+
1037+
async fn list_tools_for_client_uncached(
1038+
server_name: &str,
1039+
client: &Arc<RmcpClient>,
1040+
timeout: Option<Duration>,
9681041
) -> Result<Vec<ToolInfo>> {
9691042
let resp = client.list_tools_with_connector_ids(None, timeout).await?;
9701043
Ok(resp

0 commit comments

Comments
 (0)