Files
codex/codex-rs/codex-mcp/src/codex_apps.rs
T
Owen LinandGitHub 703793c22e feat(core, mcp): cache codex_apps tools in memory (#29003)
## 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).
2026-06-25 20:54:48 +00:00

66 lines
1.8 KiB
Rust

//! Codex Apps support for the host-owned apps MCP server.
//!
//! This module owns the normalization that turns ChatGPT-hosted app
//! connector/tool metadata into model-visible MCP callable names.
use codex_utils_plugins::mcp_connector::sanitize_name;
pub(crate) fn normalize_codex_apps_tool_title(connector_name: Option<&str>, value: &str) -> 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(
tool_name: &str,
connector_id: Option<&str>,
connector_name: Option<&str>,
) -> 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 let Some(connector_name) = connector_name {
format!("{}__{}", server_name, sanitize_name(connector_name))
} else {
server_name.to_string()
}
}