Allow cached catalogs to satisfy MCP startup readiness (#47935)

## Why

Required MCP servers can block startup while establishing a live connection even when a usable tool catalog is already cached.

## What changed

- Add per-server `startup_readiness = "catalog"` to expose cached tools while the connection starts, retaining `"connection"` as the default.
- Allow an eligible cached catalog to satisfy required-server startup validation and model binding capture. Explicit server or plugin requirements still wait for a live connection, and tool execution still requires the current connection.
- Check tool filters and model visibility when accepting cached catalogs, and preserve cached catalog revisions for binding reuse.
- Serialize the new setting, expose it in the configuration schema, and trace startup readiness at validation and model binding gates.

GitOrigin-RevId: a1cbdf1757ebb5824cffe2cc56408be49711c422
This commit is contained in:
Henry Levy
2026-09-24 20:38:50 +00:00
committed by copyberry
parent af09e926d6
commit b62fcd3c4b
34 changed files with 257 additions and 34 deletions
+1
View File
@@ -424,6 +424,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+1
View File
@@ -41,6 +41,7 @@ fn server(url: &str) -> McpServerConfig {
environment_id: DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: true,
startup_readiness: Default::default(),
supports_parallel_tool_calls: true,
omit_tools_from: None,
disabled_reason: None,
@@ -62,6 +62,7 @@ use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use codex_config::McpServerTransportConfig;
use codex_config::McpStartupReadiness;
use codex_diagnostics::Gauge;
use codex_diagnostics::GaugeGuard;
use codex_protocol::mcp::CallToolResult;
@@ -161,6 +162,7 @@ impl Drop for McpServerConnection {
struct McpServerView {
connection: Arc<McpServerConnection>,
protocol_mode: crate::McpProtocolMode,
startup_readiness: McpStartupReadiness,
metadata: McpServerMetadata,
tool_filter: ToolFilter,
tool_timeout: Option<Duration>,
@@ -168,6 +170,28 @@ struct McpServerView {
}
impl McpServerView {
fn allows_cached_startup(&self) -> bool {
self.startup_readiness == McpStartupReadiness::Catalog
|| self.connection.startup_is_dormant()
}
fn cached_startup_tools(&self, fallback: Option<Vec<ToolInfo>>) -> Option<Vec<ToolInfo>> {
self.connection
.client
.cached_tools_or(fallback)
.filter(|tools| self.accepts_cached_tools(tools))
}
fn accepts_cached_tools(&self, tools: &[ToolInfo]) -> bool {
self.connection.client.is_codex_apps_mcp_server
|| match self.startup_readiness {
McpStartupReadiness::Connection => !tools.is_empty(),
McpStartupReadiness::Catalog => tools.iter().any(|tool| {
self.tool_filter.allows(&tool.tool.name) && tool_is_model_visible(tool)
}),
}
}
async fn listed_tools(
&self,
tool_plugin_context: &ToolPluginContext,
@@ -496,6 +520,7 @@ impl McpConnectionSet {
McpServerView {
connection,
protocol_mode,
startup_readiness: configured_config.startup_readiness,
metadata,
tool_filter: configured_tool_filter,
tool_timeout: configured_tool_timeout,
@@ -633,6 +658,7 @@ impl McpConnectionSet {
_diagnostics_guard: LIVE_CONNECTIONS.track(),
}),
protocol_mode,
startup_readiness: configured_config.startup_readiness,
metadata,
tool_filter: configured_tool_filter,
tool_timeout: configured_tool_timeout,
@@ -1,3 +1,5 @@
use std::collections::HashSet;
use anyhow::Result;
use anyhow::anyhow;
use codex_protocol::protocol::McpStartupFailure;
@@ -8,12 +10,13 @@ use super::McpConnectionSet;
use crate::rmcp_client::StartupOutcomeError;
impl McpConnectionSet {
/// Waits for every required server and reports their startup failures together.
/// Waits for required servers unless their readiness policy accepts a cached catalog.
///
/// The manager must already be reachable through [`crate::McpRuntime`] so
/// startup-time elicitation can resolve while validation waits.
pub(crate) async fn validate_required_servers(&self) -> Result<()> {
let failures = async {
self.record_startup_readiness("required_startup", &[], &HashSet::new());
let mut failures = Vec::new();
for server_name in &self.required_servers {
let Some(view) = self.servers.get(server_name) else {
@@ -23,7 +26,8 @@ impl McpConnectionSet {
});
continue;
};
if view.connection.startup_is_dormant() && view.connection.client.has_cached_tools()
if view.allows_cached_startup()
&& view.cached_startup_tools(/*fallback*/ None).is_some()
{
continue;
}
@@ -7,6 +7,7 @@ use std::time::Instant;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use codex_config::McpStartupReadiness;
use codex_connectors::ConnectorRuntimeFetchSource;
use futures::future::join_all;
use tracing::Instrument;
@@ -59,14 +60,58 @@ pub fn tool_is_model_visible(tool: &ToolInfo) -> bool {
.any(|target| target.as_str() == Some(MCP_UI_MODEL_VISIBILITY))
}
/// Catalog identity within one published connection set, including dormant servers.
/// Catalog identity within one published connection set, including cached declarations.
#[derive(PartialEq, Eq)]
pub(crate) enum BindingCatalogRevision {
Ready(ClientToolCatalogRevision),
Dormant(u64),
Cached(u64),
}
impl McpConnectionSet {
/// Observes opted-in servers at startup gates without waiting or cloning catalogs.
pub(crate) fn record_startup_readiness(
&self,
phase: &'static str,
required_servers: &[String],
required_plugins: &HashSet<String>,
) {
if !tracing::enabled!(tracing::Level::INFO) {
return;
}
for (server_name, view) in &self.servers {
if view.startup_readiness != McpStartupReadiness::Catalog {
continue;
}
let client = &view.connection.client;
let startup_pending = !client.startup_complete.load(Ordering::Acquire);
let cache_eligible = startup_pending
&& client
.tool_catalog_cache_context
.as_ref()
.is_some_and(|cache| {
cache
.current_revision_if(|tools| view.accepts_cached_tools(tools))
.is_some()
});
let explicitly_required = required_servers.contains(server_name)
|| (self.is_selected_plugin_mcp_server(server_name)
&& self
.plugin_id_for_mcp_server_name(server_name)
.is_some_and(|plugin_id| required_plugins.contains(plugin_id)));
tracing::info!(
event.name = "mcp.startup_readiness",
server_name = %server_name,
phase,
startup_pending,
startup_readiness = "catalog",
cache_eligible,
required = self.required_servers.binary_search(server_name).is_ok(),
explicitly_required,
"MCP startup readiness observed"
);
}
}
pub(crate) async fn stable_catalog_revisions(
&self,
required_servers: &[String],
@@ -74,15 +119,17 @@ impl McpConnectionSet {
) -> Option<HashMap<String, BindingCatalogRevision>> {
let mut revisions = HashMap::new();
for (server_name, view) in &self.servers {
if !view
let startup_complete = view
.connection
.client
.startup_complete
.load(Ordering::Acquire)
.load(Ordering::Acquire);
if !startup_complete
|| (view.startup_readiness == McpStartupReadiness::Catalog
&& view.connection.client.ready_client().is_none())
{
// A cached catalog can remain stable without starting its server.
// Explicit requirements must still start the server during binding capture.
if !view.connection.startup_is_dormant()
// Explicit requirements still wait for a live connection during capture.
if !view.allows_cached_startup()
|| view.connection.client.cancel_token.is_cancelled()
|| required_servers
.iter()
@@ -99,10 +146,10 @@ impl McpConnectionSet {
.client
.tool_catalog_cache_context
.as_ref()?
.current_revision()?;
.current_revision_if(|tools| view.accepts_cached_tools(tools))?;
revisions.insert(
server_name.clone(),
BindingCatalogRevision::Dormant(revision),
BindingCatalogRevision::Cached(revision),
);
continue;
}
@@ -222,12 +269,10 @@ impl McpConnectionSet {
{
let required = self.required_servers.binary_search(server_name).is_ok();
// Keep the catalog that lets us skip startup even if it expires during the wait.
let cached_tools = view.connection.client.cached_tools().filter(|tools| {
view.connection.client.is_codex_apps_mcp_server || !tools.is_empty()
});
let cached_tools = view.cached_startup_tools(/*fallback*/ None);
let has_cached_tools = cached_tools.is_some();
let must_wait_for_startup = (required
&& (!view.connection.startup_is_dormant() || !has_cached_tools))
&& (!view.allows_cached_startup() || !has_cached_tools))
|| required_servers
.iter()
.any(|required| required == server_name)
@@ -237,6 +282,7 @@ impl McpConnectionSet {
.is_some_and(|plugin_id| required_plugins.contains(plugin_id)))
|| (server_name == CODEX_APPS_MCP_SERVER_NAME && !has_cached_tools);
if !must_wait_for_startup && has_cached_tools {
trace!(server_name = %server_name, "using cached MCP catalog without waiting for startup");
return (server_name, view, cached_tools);
}
if !must_wait_for_startup && optional_mcp_startup_grace.is_zero() {
@@ -283,14 +329,30 @@ impl McpConnectionSet {
}))
.await;
let server_results = join_all(server_snapshots.into_iter().map(|(server_name, view, cached_tools)| async move {
let (client, server_tools) = if !view
let startup_pending = !view
.connection
.client
.startup_complete
.load(Ordering::Acquire)
.load(Ordering::Acquire);
let accepts_cached_catalog = view.startup_readiness == McpStartupReadiness::Catalog;
let cached_tools = if startup_pending
|| (accepts_cached_catalog && view.connection.client.ready_client().is_none())
{
(None, view.connection.client.cached_tools_or(cached_tools)?)
view.cached_startup_tools(cached_tools)
} else {
None
};
let (client, server_tools) = if let Some(cached_tools) = cached_tools {
(None, cached_tools)
} else {
// A server may opt out of caching after the first pass. Required catalog
// readiness must then wait for discovery rather than silently omit its tools.
if startup_pending
&& !(accepts_cached_catalog
&& self.required_servers.binary_search(server_name).is_ok())
{
return None;
}
view.connection.client.reconnect_failed_startup().await;
let Ok(mut client) = view.connection.client().await else {
trace!(server_name = %server_name, "omitting MCP server without an exact ready client");
@@ -136,6 +136,7 @@ impl McpConnectionSet {
McpServerView {
tool_filter: ToolFilter::default(),
protocol_mode: crate::McpProtocolMode::Legacy,
startup_readiness: Default::default(),
connection: Arc::new(McpServerConnection {
identity: None,
client,
@@ -4668,6 +4669,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -4696,6 +4698,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -4807,6 +4810,7 @@ fn mcp_init_error_display_prompts_for_github_pat() {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -4967,6 +4971,7 @@ fn mcp_init_error_display_reports_generic_errors() {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -5046,6 +5051,7 @@ fn reusable_server_config(url: &str) -> McpServerConfig {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -5110,6 +5116,7 @@ async fn manager_with_reusable_ready_server(
"docs".to_string(),
McpServerView {
protocol_mode: crate::McpProtocolMode::Legacy,
startup_readiness: Default::default(),
connection: Arc::new(McpServerConnection {
identity: Some(reusable_server_identity("docs", config, runtime_context)),
client: create_ready_async_managed_client(tools).await,
@@ -5535,6 +5542,7 @@ async fn reconciliation_reuses_connection_without_relisting_regular_tools() -> a
"docs".to_string(),
McpServerView {
protocol_mode: crate::McpProtocolMode::Legacy,
startup_readiness: Default::default(),
connection: Arc::new(McpServerConnection {
identity: Some(reusable_server_identity("docs", &config, &runtime_context)),
client: AsyncManagedClient {
+1
View File
@@ -655,6 +655,7 @@ fn mcp_server_config_for_url(
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+2
View File
@@ -592,6 +592,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -620,6 +621,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -535,6 +535,7 @@ fn stdio_server(
environment_id: environment_id.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -571,6 +572,7 @@ fn declared_placement_preserves_local_plugin_normalization() {
environment_id: DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -891,6 +893,7 @@ fn local_environment_placement_preserves_http_env_references() {
environment_id: DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+6 -4
View File
@@ -417,12 +417,13 @@ impl McpRuntime {
required_servers: &[String],
required_plugins: &HashSet<String>,
) -> Option<Arc<McpBinding>> {
Self::binding_from_published_runtime(
self.current.load_full(),
let current = self.current.load_full();
current.connections.record_startup_readiness(
"model_binding",
required_servers,
required_plugins,
)
.await
);
Self::binding_from_published_runtime(current, required_servers, required_plugins).await
}
async fn binding_from_published_runtime(
@@ -948,6 +949,7 @@ mod tests {
environment_id: environment_id.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+10 -6
View File
@@ -116,18 +116,22 @@ impl Default for ToolCatalogCacheEntry {
impl McpToolCatalogCacheContext {
pub(crate) fn has_tools(&self) -> bool {
self.current_revision().is_some()
self.current_revision_if(|_| true).is_some()
}
/// Identifies the current usable catalog without cloning its tool definitions.
/// The entry is fixed for a connection; accepted publications advance its revision.
pub(crate) fn current_revision(&self) -> Option<u64> {
/// Checks eligibility and reads the revision under one lock without cloning tools.
/// The predicate borrows the current catalog while the cache entry is locked.
pub(crate) fn current_revision_if(
&self,
accepts_tools: impl FnOnce(&[ToolInfo]) -> bool,
) -> Option<u64> {
let state = lock_unpoisoned(&self.entry.state);
let snapshot = state.snapshot.as_ref()?;
(!state.disabled_by_server
&& !snapshot.tools.is_empty()
&& snapshot.published_at.elapsed() <= TOOL_CATALOG_CACHE_TTL)
.then_some(state.last_accepted_generation)
&& snapshot.published_at.elapsed() <= TOOL_CATALOG_CACHE_TTL
&& accepts_tools(&snapshot.tools))
.then_some(state.last_accepted_generation)
}
pub(crate) fn optional_startup_deadline(
+1
View File
@@ -176,6 +176,7 @@ pub use mcp_types::McpServerEnvVar;
pub use mcp_types::McpServerOAuthConfig;
pub use mcp_types::McpServerToolConfig;
pub use mcp_types::McpServerTransportConfig;
pub use mcp_types::McpStartupReadiness;
pub use mcp_types::RawMcpServerConfig;
pub use merge::ShellEnvironmentPolicyFilterRepresentation;
pub use merge::is_structured_feature_path;
@@ -17,6 +17,7 @@ fn stdio_server(command: &str, args: &[&str]) -> McpServerConfig {
environment_id: crate::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+26 -5
View File
@@ -213,10 +213,20 @@ pub enum McpServerAuth {
EmaAuth,
}
impl McpServerAuth {
fn is_default(&self) -> bool {
self == &Self::default()
}
/// Readiness needed before startup can expose this server's tools to the model.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum McpStartupReadiness {
/// Use the live connection to determine startup readiness.
#[default]
Connection,
/// Allow a valid cached tool catalog while the live connection starts.
/// Tool execution still requires the current connection.
Catalog,
}
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
value == &T::default()
}
#[derive(Serialize, Debug, Clone, PartialEq)]
@@ -225,7 +235,7 @@ pub struct McpServerConfig {
pub transport: McpServerTransportConfig,
/// Authentication flow, including an explicit no-fallback EMA mode.
#[serde(default, skip_serializing_if = "McpServerAuth::is_default")]
#[serde(default, skip_serializing_if = "is_default")]
pub auth: McpServerAuth,
/// Effective environment id for where Codex should start this MCP server.
@@ -236,9 +246,15 @@ pub struct McpServerConfig {
pub enabled: bool,
/// When `true`, `codex exec` exits with an error if this MCP server fails to initialize.
/// With `startup_readiness = "catalog"`, a valid cached catalog can satisfy startup;
/// connection failures are then reported when a tool is invoked.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub required: bool,
/// Whether startup requires a live connection or can use a valid cached tool catalog.
#[serde(default, skip_serializing_if = "is_default")]
pub startup_readiness: McpStartupReadiness,
/// When `true`, every tool from this server is advertised as safe for parallel tool calls.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub supports_parallel_tool_calls: bool,
@@ -391,6 +407,9 @@ pub struct RawMcpServerConfig {
pub enabled: Option<bool>,
#[serde(default)]
pub required: Option<bool>,
/// Whether startup requires a live connection or can use a valid cached tool catalog.
#[serde(default)]
pub startup_readiness: Option<McpStartupReadiness>,
#[serde(default)]
pub supports_parallel_tool_calls: Option<bool>,
#[serde(default)]
@@ -437,6 +456,7 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
tool_timeout_sec,
enabled,
required,
startup_readiness,
supports_parallel_tool_calls,
omit_tools_from,
default_tools_approval_mode,
@@ -556,6 +576,7 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
tool_timeout_sec,
enabled: enabled.unwrap_or_else(default_enabled),
required: required.unwrap_or_default(),
startup_readiness: startup_readiness.unwrap_or_default(),
supports_parallel_tool_calls: supports_parallel_tool_calls.unwrap_or_default(),
omit_tools_from,
disabled_reason: None,
+1
View File
@@ -640,6 +640,7 @@ fn deserialize_ignores_unknown_server_fields() {
environment_id: crate::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+1
View File
@@ -13,6 +13,7 @@ pub use crate::mcp_types::McpServerEnvVar;
pub use crate::mcp_types::McpServerOAuthConfig;
pub use crate::mcp_types::McpServerToolConfig;
pub use crate::mcp_types::McpServerTransportConfig;
pub use crate::mcp_types::McpStartupReadiness;
pub use crate::mcp_types::RawMcpServerConfig;
pub use crate::shell_environment_policy::ShellEnvironmentPolicyToml;
pub use codex_protocol::config_types::AltScreenMode;
@@ -981,6 +981,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
environment_id: "local".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -1083,6 +1084,7 @@ enabled = true
environment_id: "local".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -2186,6 +2188,7 @@ async fn load_plugins_uses_manifest_configured_component_paths() {
environment_id: "local".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -2525,6 +2528,7 @@ async fn load_plugins_ignores_manifest_component_paths_without_dot_slash() {
environment_id: "local".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -2780,6 +2784,7 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
environment_id: "local".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+28
View File
@@ -2393,6 +2393,25 @@
},
"type": "object"
},
"McpStartupReadiness": {
"description": "Readiness needed before startup can expose this server's tools to the model.",
"oneOf": [
{
"description": "Use the live connection to determine startup readiness.",
"enum": [
"connection"
],
"type": "string"
},
{
"description": "Allow a valid cached tool catalog while the live connection starts. Tool execution still requires the current connection.",
"enum": [
"catalog"
],
"type": "string"
}
]
},
"MemoriesToml": {
"additionalProperties": false,
"description": "Memories settings loaded from config.toml.",
@@ -3659,6 +3678,15 @@
},
"type": "array"
},
"startup_readiness": {
"allOf": [
{
"$ref": "#/definitions/McpStartupReadiness"
}
],
"default": null,
"description": "Whether startup requires a live connection or can use a valid cached tool catalog."
},
"startup_timeout_ms": {
"default": null,
"format": "uint64",
+17
View File
@@ -141,6 +141,7 @@ fn stdio_mcp_with_args(command: &str, args: &[&str]) -> McpServerConfig {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -169,6 +170,7 @@ fn http_mcp(url: &str) -> McpServerConfig {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -6889,6 +6891,7 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
environment_id: "remote".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7324,6 +7327,7 @@ async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7402,6 +7406,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7465,6 +7470,7 @@ async fn replace_mcp_servers_serializes_sourced_env_vars() -> anyhow::Result<()>
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7519,6 +7525,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7576,6 +7583,7 @@ async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7650,6 +7658,7 @@ async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyh
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7735,6 +7744,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7773,6 +7783,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7847,6 +7858,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7875,6 +7887,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -7965,6 +7978,7 @@ async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: false,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -8017,6 +8031,7 @@ async fn replace_mcp_servers_serializes_required_flag() -> anyhow::Result<()> {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: true,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -8069,6 +8084,7 @@ async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -8126,6 +8142,7 @@ async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyh
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -5,6 +5,7 @@ use codex_config::types::McpServerConfig;
use codex_config::types::McpServerEnvVar;
use codex_config::types::McpServerToolConfig;
use codex_config::types::McpServerTransportConfig;
use codex_config::types::McpStartupReadiness;
use codex_config::types::ToolSuggestDisabledTool;
use codex_config::types::ToolSuggestDiscoverableType;
use toml_edit::Array as TomlArray;
@@ -115,6 +116,10 @@ fn serialize_mcp_server_table(config: &McpServerConfig) -> anyhow::Result<TomlTa
if config.required {
entry["required"] = value(true);
}
match config.startup_readiness {
McpStartupReadiness::Connection => {}
McpStartupReadiness::Catalog => entry["startup_readiness"] = value("catalog"),
}
if config.supports_parallel_tool_calls {
entry["supports_parallel_tool_calls"] = value(true);
}
+7
View File
@@ -1080,6 +1080,7 @@ fn blocking_replace_mcp_servers_round_trips() {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: true,
omit_tools_from: None,
disabled_reason: None,
@@ -1113,6 +1114,7 @@ fn blocking_replace_mcp_servers_round_trips() {
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: false,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -1205,6 +1207,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides(output_token_
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -1281,6 +1284,7 @@ foo = { command = "cmd" }
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -1333,6 +1337,7 @@ foo = { command = "cmd" } # keep me
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: false,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -1384,6 +1389,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -1436,6 +1442,7 @@ foo = { command = "cmd" }
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: false,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -416,6 +416,7 @@ fn mcp_dependency_to_server_config(
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -455,6 +456,7 @@ fn mcp_dependency_to_server_config(
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+5 -1
View File
@@ -337,7 +337,11 @@ impl Session {
self.mcp_refresh.invalidate();
}
#[tracing::instrument(name = "mcp.runtime.resolve_for_step", skip_all)]
#[tracing::instrument(
name = "mcp.runtime.resolve_for_step",
skip_all,
fields(turn.id = %turn_context.sub_id)
)]
pub(crate) async fn mcp_runtime_for_step(
self: &Arc<Self>,
turn_context: &TurnContext,
+1
View File
@@ -9676,6 +9676,7 @@ async fn step_context_keeps_its_mcp_runtime_for_tools() -> anyhow::Result<()> {
environment_id: DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+1
View File
@@ -732,6 +732,7 @@ async fn run_code_mode_turn_with_rmcp_config(
environment_id: "local".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+1
View File
@@ -291,6 +291,7 @@ fn insert_rmcp_test_server(
environment_id,
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -45,6 +45,7 @@ async fn refresh_keeps_superseded_mcp_server_alive_for_in_flight_calls() -> anyh
environment_id: DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -135,6 +135,7 @@ async fn local_mcp_startup_and_refresh_use_configured_http_client() -> Result<()
environment_id: DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+1
View File
@@ -377,6 +377,7 @@ fn insert_mcp_server(
environment_id: options.environment_id,
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: options.supports_parallel_tool_calls,
omit_tools_from: None,
disabled_reason: None,
+3
View File
@@ -1326,6 +1326,7 @@ async fn tool_search_indexes_only_enabled_non_app_mcp_tools() -> Result<()> {
environment_id,
enabled: true,
required: false,
startup_readiness: Default::default(),
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
@@ -1459,6 +1460,7 @@ async fn tool_search_surfaced_mcp_tool_errors_are_returned_to_model() -> Result<
environment_id,
enabled: true,
required: false,
startup_readiness: Default::default(),
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
@@ -1608,6 +1610,7 @@ async fn tool_search_uses_non_app_mcp_server_instructions_as_namespace_descripti
environment_id,
enabled: true,
required: false,
startup_readiness: Default::default(),
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
@@ -882,6 +882,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result<
environment_id: "local".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -772,6 +772,7 @@ async fn token_budget_context_injects_plain_thread_hint_text() -> Result<()> {
environment_id: "local".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
+2
View File
@@ -413,6 +413,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()>
environment_id: "local".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -516,6 +517,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
environment_id: "local".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -195,6 +195,7 @@ async fn reads_declared_config_only_through_executor_file_system() {
environment_id: "executor-test".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -223,6 +224,7 @@ async fn reads_declared_config_only_through_executor_file_system() {
environment_id: "executor-test".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,
@@ -281,6 +283,7 @@ async fn reads_manifest_object_config_without_executor_file_system_access() {
environment_id: "executor-test".to_string(),
enabled: true,
required: false,
startup_readiness: Default::default(),
supports_parallel_tool_calls: false,
omit_tools_from: None,
disabled_reason: None,