mirror of
https://github.com/openai/codex.git
synced 2026-11-03 00:21:31 +00:00
## Description This makes Codex Apps tool reads use a shared in-memory snapshot instead of rereading the disk cache every time `list_all_tools()` runs. Disk still seeds the cache on startup and gets updated after successful fetches, but it is no longer the live read path. The core change is that `McpManager` now owns a process-scoped `CodexAppsToolsCache`. Codex threads in the same app-server process now share this Codex Apps in-memory tools snapshot. The snapshot is keyed by the Codex home plus the Codex Apps identity: the active Codex auth user/workspace and the effective Codex Apps MCP source config. There's already code to hard-refresh the cache, so we respect it in this PR. ## Local benchmark I ran a local steady-state microbenchmark of the exact repeated Codex Apps cached-tools read this PR removes, using the same real local cache payload in both trees: `3,678,138` bytes and `381` tools. The cache file was already warm in the OS page cache, so this measures same-process reread/deserialization work rather than cold-disk latency or full turn latency. Each run is 25 iterations (mimicking a turn that makes 25 inference calls). | Version | Run 1 | Run 2 | Avg | |---|---:|---:|---:| | `origin/main` disk read + JSON deserialize + `filter_tools` | `50.755 ms` | `52.894 ms` | `51.825 ms` | | This branch in-memory `current_tools` + `filter_tools` | `0.740 ms` | `0.778 ms` | `0.759 ms` | That removes about `51 ms` from each repeated Codex Apps cached-tools read on this machine, roughly `68x` faster for that subpath. It is useful evidence for the hot path this PR changes, but not a claim that every production turn gets `51 ms` faster; end-to-end impact also depends on the rest of `list_all_tools()` and tool-payload construction. This is on my M2 Max macbook, so with a slower disk this would be much worse (and indeed we did see this really blew up turn runtime with a slow disk).
214 lines
7.5 KiB
Rust
214 lines
7.5 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use crate::config::Config;
|
|
use codex_config::McpServerConfig;
|
|
use codex_core_plugins::PluginsManager;
|
|
use codex_extension_api::ExtensionDataInit;
|
|
use codex_extension_api::ExtensionRegistry;
|
|
use codex_extension_api::McpServerContribution;
|
|
use codex_extension_api::McpServerContributionContext;
|
|
use codex_login::CodexAuth;
|
|
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
|
use codex_mcp::CodexAppsToolsCache;
|
|
use codex_mcp::EffectiveMcpServer;
|
|
use codex_mcp::McpConfig;
|
|
use codex_mcp::McpPluginAttribution;
|
|
use codex_mcp::McpServerRegistration;
|
|
use codex_mcp::codex_apps_mcp_server_config;
|
|
use codex_mcp::configured_mcp_servers;
|
|
use codex_mcp::effective_mcp_servers;
|
|
|
|
const LEGACY_CODEX_APPS_REGISTRATION_ID: &str = "legacy_codex_apps";
|
|
|
|
enum OrderedMcpOverlay {
|
|
Set {
|
|
contributor_id: &'static str,
|
|
contribution_order: usize,
|
|
name: String,
|
|
config: Box<McpServerConfig>,
|
|
},
|
|
Remove {
|
|
contributor_id: &'static str,
|
|
contribution_order: usize,
|
|
name: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct McpManager {
|
|
plugins_manager: Arc<PluginsManager>,
|
|
extensions: Arc<ExtensionRegistry<Config>>,
|
|
codex_apps_tools_cache: CodexAppsToolsCache,
|
|
}
|
|
|
|
impl McpManager {
|
|
pub fn new(plugins_manager: Arc<PluginsManager>) -> Self {
|
|
Self::new_with_extensions(
|
|
plugins_manager,
|
|
codex_extension_api::empty_extension_registry(),
|
|
)
|
|
}
|
|
|
|
/// Creates a manager that resolves host-installed MCP contributions.
|
|
pub fn new_with_extensions(
|
|
plugins_manager: Arc<PluginsManager>,
|
|
extensions: Arc<ExtensionRegistry<Config>>,
|
|
) -> Self {
|
|
Self {
|
|
plugins_manager,
|
|
extensions,
|
|
codex_apps_tools_cache: CodexAppsToolsCache::default(),
|
|
}
|
|
}
|
|
|
|
pub fn codex_apps_tools_cache(&self) -> CodexAppsToolsCache {
|
|
self.codex_apps_tools_cache.clone()
|
|
}
|
|
|
|
/// Returns the MCP config after applying compatibility built-ins and
|
|
/// runtime-only extension overlays.
|
|
pub async fn runtime_config(&self, config: &Config) -> McpConfig {
|
|
self.runtime_config_with_context(config, /*thread_init*/ None)
|
|
.await
|
|
}
|
|
|
|
pub(crate) async fn runtime_config_for_thread(
|
|
&self,
|
|
config: &Config,
|
|
thread_init: &ExtensionDataInit,
|
|
) -> McpConfig {
|
|
self.runtime_config_with_context(config, Some(thread_init))
|
|
.await
|
|
}
|
|
|
|
async fn runtime_config_with_context(
|
|
&self,
|
|
config: &Config,
|
|
thread_init: Option<&ExtensionDataInit>,
|
|
) -> McpConfig {
|
|
let context = match thread_init {
|
|
Some(thread_init) => McpServerContributionContext::for_thread(config, thread_init),
|
|
None => McpServerContributionContext::global(config),
|
|
};
|
|
let mut selected_plugin_registrations = Vec::new();
|
|
let mut overlays = Vec::new();
|
|
// A contributor can emit multiple ordered actions, so order each action globally rather
|
|
// than enumerating contributors.
|
|
let mut contribution_order = 0;
|
|
for contributor in self.extensions.mcp_server_contributors() {
|
|
for contribution in contributor.contribute(context).await {
|
|
match contribution {
|
|
McpServerContribution::Set { name, config } => {
|
|
overlays.push(OrderedMcpOverlay::Set {
|
|
contributor_id: contributor.id(),
|
|
contribution_order,
|
|
name,
|
|
config,
|
|
});
|
|
}
|
|
McpServerContribution::SelectedPlugin {
|
|
name,
|
|
plugin_id,
|
|
plugin_display_name,
|
|
selection_order,
|
|
config,
|
|
} => selected_plugin_registrations.push(
|
|
McpServerRegistration::from_selected_plugin(
|
|
name,
|
|
McpPluginAttribution::new(plugin_id, plugin_display_name),
|
|
selection_order,
|
|
*config,
|
|
),
|
|
),
|
|
McpServerContribution::Remove { name } => {
|
|
overlays.push(OrderedMcpOverlay::Remove {
|
|
contributor_id: contributor.id(),
|
|
contribution_order,
|
|
name,
|
|
});
|
|
}
|
|
}
|
|
contribution_order += 1;
|
|
}
|
|
}
|
|
|
|
let mut mcp_config = config
|
|
.to_mcp_config_with_plugin_registrations(
|
|
self.plugins_manager.as_ref(),
|
|
selected_plugin_registrations,
|
|
)
|
|
.await;
|
|
let mut catalog = mcp_config.mcp_server_catalog.to_builder();
|
|
if mcp_config.apps_enabled {
|
|
catalog.register(McpServerRegistration::from_compatibility(
|
|
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
LEGACY_CODEX_APPS_REGISTRATION_ID,
|
|
codex_apps_mcp_server_config(
|
|
&mcp_config.chatgpt_base_url,
|
|
mcp_config.apps_mcp_product_sku.as_deref(),
|
|
),
|
|
));
|
|
} else {
|
|
catalog.remove_compatibility(
|
|
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
LEGACY_CODEX_APPS_REGISTRATION_ID,
|
|
);
|
|
}
|
|
|
|
for overlay in overlays {
|
|
match overlay {
|
|
OrderedMcpOverlay::Set {
|
|
contributor_id,
|
|
contribution_order,
|
|
name,
|
|
config,
|
|
} => catalog.register(McpServerRegistration::from_extension(
|
|
name,
|
|
contributor_id,
|
|
contribution_order,
|
|
*config,
|
|
)),
|
|
OrderedMcpOverlay::Remove {
|
|
contributor_id,
|
|
contribution_order,
|
|
name,
|
|
} => catalog.remove_extension(name, contributor_id, contribution_order),
|
|
}
|
|
}
|
|
let catalog = catalog.build();
|
|
for conflict in catalog.conflicts() {
|
|
tracing::warn!(
|
|
server = conflict.name,
|
|
outcome = ?conflict.outcome,
|
|
contenders = ?conflict.contenders,
|
|
"conflicting MCP server actions; using resolved catalog outcome"
|
|
);
|
|
}
|
|
mcp_config.mcp_server_catalog = catalog;
|
|
mcp_config
|
|
}
|
|
|
|
/// Returns config- and plugin-backed servers without runtime contributions.
|
|
pub async fn configured_servers(&self, config: &Config) -> HashMap<String, McpServerConfig> {
|
|
let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
|
configured_mcp_servers(&mcp_config)
|
|
}
|
|
|
|
/// Returns configured and host-contributed servers before auth gating.
|
|
pub async fn runtime_servers(&self, config: &Config) -> HashMap<String, McpServerConfig> {
|
|
let mcp_config = self.runtime_config(config).await;
|
|
configured_mcp_servers(&mcp_config)
|
|
}
|
|
|
|
/// Returns runtime servers after auth gating and compatibility built-ins.
|
|
pub async fn effective_servers(
|
|
&self,
|
|
config: &Config,
|
|
auth: Option<&CodexAuth>,
|
|
) -> HashMap<String, EffectiveMcpServer> {
|
|
let mcp_config = self.runtime_config(config).await;
|
|
effective_mcp_servers(&mcp_config, auth)
|
|
}
|
|
}
|