From 3307ea8b6355ba15546647b02597876341c0489e Mon Sep 17 00:00:00 2001 From: jif Date: Wed, 15 Jul 2026 09:12:56 +0000 Subject: [PATCH] Allow MCP servers to opt out of tool catalog caching (#33297) ## What changed - Recognize the experimental `codex/tool-catalog-cache` server capability during MCP initialization. - When the server advertises `cacheable: false`, clear its shared tool catalog snapshot and prevent subsequent fetches from repopulating the cache. GitOrigin-RevId: bce98371a68881001d3ab1378fa1548575f2dadc --- codex-rs/codex-mcp/src/rmcp_client.rs | 17 +++++++++++++++++ codex-rs/codex-mcp/src/tool_catalog_cache.rs | 9 ++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/codex-rs/codex-mcp/src/rmcp_client.rs b/codex-rs/codex-mcp/src/rmcp_client.rs index 4ac96f867f..413b7eaa92 100644 --- a/codex-rs/codex-mcp/src/rmcp_client.rs +++ b/codex-rs/codex-mcp/src/rmcp_client.rs @@ -79,6 +79,10 @@ use tracing::warn; /// 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"; +/// Experimental MCP server capability for development and testing only; production servers should +/// not use it. Its `cacheable: false` property disables sharing tool definitions across connections. +const MCP_TOOL_CATALOG_CACHE_CAPABILITY: &str = "codex/tool-catalog-cache"; +const MCP_TOOL_CATALOG_CACHEABLE_PROPERTY: &str = "cacheable"; pub const OPENAI_FORM_CAPABILITY: &str = "openai/form"; pub(crate) const MCP_TOOLS_LIST_DURATION_METRIC: &str = "codex.mcp.tools.list.duration_ms"; @@ -887,6 +891,19 @@ async fn start_server_task( .await .map_err(StartupOutcomeError::from)?; + let server_disables_tool_catalog_cache = initialize_result + .capabilities + .experimental + .as_ref() + .and_then(|experimental| experimental.get(MCP_TOOL_CATALOG_CACHE_CAPABILITY)) + .and_then(|capability| capability.get(MCP_TOOL_CATALOG_CACHEABLE_PROPERTY)) + .and_then(serde_json::Value::as_bool) + == Some(false); + if server_disables_tool_catalog_cache + && let Some(cache_context) = tool_catalog_cache_context.as_ref() + { + cache_context.disable(); + } let server_supports_sandbox_state_meta_capability = initialize_result .capabilities .experimental diff --git a/codex-rs/codex-mcp/src/tool_catalog_cache.rs b/codex-rs/codex-mcp/src/tool_catalog_cache.rs index 4f2f57140c..15c3ae3d59 100644 --- a/codex-rs/codex-mcp/src/tool_catalog_cache.rs +++ b/codex-rs/codex-mcp/src/tool_catalog_cache.rs @@ -52,6 +52,7 @@ struct ToolCatalogCacheEntry { struct ToolCatalogCacheState { snapshot: Option, last_accepted_generation: u64, + disabled_by_server: bool, } struct ToolCatalogSnapshot { @@ -125,9 +126,15 @@ impl McpToolCatalogCacheContext { } } + pub(crate) fn disable(&self) { + let mut state = lock_unpoisoned(&self.entry.state); + state.disabled_by_server = true; + state.snapshot = None; + } + pub(crate) fn publish_if_newest(&self, ticket: McpToolCatalogFetchTicket, tools: &[ToolInfo]) { let mut state = lock_unpoisoned(&self.entry.state); - if ticket.generation <= state.last_accepted_generation { + if state.disabled_by_server || ticket.generation <= state.last_accepted_generation { return; }