mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
## Why Regular chats start MCP servers eagerly even when cached tool definitions are available and the servers go unused. Allow those chats to use the cached catalog and defer eligible stdio servers until first use. ## What changed - Add the disabled-by-default `mcp_lazy_stdio_startup` feature and expose it through experimental feature configuration. - Defer cached stdio servers in regular chats when enabled, while keeping servers that require a live startup connection eager. Required servers with `startup_readiness = "catalog"` may defer; explicit reconnects remain eager. - Announce the full eager startup batch before terminal statuses and exclude dormant servers from the initial startup summary. Update TUI tracking so deferred servers can report startup independently in later rounds. ## Testing Add scenario coverage for a cached server staying dormant during unrelated work, starting on first use, and refreshing tool definitions and visibility. Cover required live startup, eager event ordering, experimental feature configuration, and TUI status handling. GitOrigin-RevId: 74075fcd4f26c8c79b5901b6a8cb03fdb9e35a7f
411 lines
16 KiB
Rust
411 lines
16 KiB
Rust
//! Thread MCP runtime projection and publication.
|
|
//!
|
|
//! This module owns the small correctness boundary between immutable session
|
|
//! inputs and the mutable [`codex_mcp::McpRuntime`]. Background scheduling
|
|
//! belongs elsewhere.
|
|
|
|
use super::session::SessionConfiguration;
|
|
use super::*;
|
|
use crate::mcp::McpRuntimeProjection;
|
|
use codex_config::McpServerDisabledReason;
|
|
use codex_config::McpServerTransportConfig;
|
|
use codex_file_system::WindowsSandboxSelection;
|
|
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
|
use codex_mcp::ElicitationReviewerHandle;
|
|
use codex_mcp::McpServerRegistration;
|
|
use codex_mcp::McpServerSource;
|
|
use codex_mcp::McpStartupPolicy;
|
|
use codex_mcp::PreparedMcpCall;
|
|
use codex_mcp::ToolInfo;
|
|
use codex_otel::auth_storage::AuthStorageOriginator;
|
|
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
|
use std::collections::HashSet;
|
|
|
|
pub(super) struct McpDesiredState {
|
|
pub(super) config: Arc<Config>,
|
|
pub(super) auth: Option<CodexAuth>,
|
|
pub(super) submit_id: String,
|
|
pub(super) originator: String,
|
|
pub(super) session_source: SessionSource,
|
|
pub(super) environments: TurnEnvironmentSnapshot,
|
|
pub(super) local_process_cwd: PathBuf,
|
|
pub(super) disabled_plugin_ids: Vec<String>,
|
|
}
|
|
|
|
impl Session {
|
|
pub(super) fn mcp_inputs_differ(
|
|
&self,
|
|
current: &SessionConfiguration,
|
|
next: &SessionConfiguration,
|
|
) -> bool {
|
|
current.cwd() != next.cwd()
|
|
|| current.runtime_workspace_roots != next.runtime_workspace_roots
|
|
|| current.step_settings.approval_policy.value()
|
|
!= next.step_settings.approval_policy.value()
|
|
|| current.step_settings.approvals_reviewer != next.step_settings.approvals_reviewer
|
|
|| current.permission_profile() != next.permission_profile()
|
|
|| current.windows_sandbox_level != next.windows_sandbox_level
|
|
}
|
|
|
|
/// Waits on this session's refreshed server before tool execution is admitted.
|
|
pub(crate) async fn wait_for_mcp_server(self: &Arc<Self>, server: &str) {
|
|
self.refresh_mcp_if_dirty().await;
|
|
self.services
|
|
.mcp_runtime
|
|
.wait_for_server_startup(server)
|
|
.await;
|
|
}
|
|
|
|
/// Captures this session's current MCP client and catalog for one tool call.
|
|
pub(crate) async fn prepare_mcp_call(
|
|
self: &Arc<Self>,
|
|
advertised_tool: &ToolInfo,
|
|
) -> anyhow::Result<PreparedMcpCall> {
|
|
self.refresh_mcp_if_dirty().await;
|
|
self.services
|
|
.mcp_runtime
|
|
.prepare_call(advertised_tool)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn latest_mcp_desired_state(
|
|
&self,
|
|
auth: Option<CodexAuth>,
|
|
environments: TurnEnvironmentSnapshot,
|
|
) -> McpDesiredState {
|
|
let (session_configuration, disabled_plugin_ids) = {
|
|
let state = self.state.lock().await;
|
|
(
|
|
state.session_configuration.clone(),
|
|
state.active_disabled_plugin_ids.clone(),
|
|
)
|
|
};
|
|
let config =
|
|
self.build_per_turn_config(&session_configuration, environments.local_selection());
|
|
let local_process_cwd = config.cwd.to_path_buf();
|
|
|
|
McpDesiredState {
|
|
config: Arc::new(config),
|
|
auth,
|
|
submit_id: self.next_internal_sub_id(),
|
|
originator: session_configuration.originator,
|
|
session_source: session_configuration.session_source,
|
|
environments,
|
|
local_process_cwd,
|
|
disabled_plugin_ids,
|
|
}
|
|
}
|
|
|
|
pub(super) async fn install_initial_mcp_runtime(
|
|
self: &Arc<Self>,
|
|
session_configuration: &SessionConfiguration,
|
|
auth: Option<CodexAuth>,
|
|
mcp_projection: McpRuntimeProjection,
|
|
resolved_environments: &TurnEnvironmentSnapshot,
|
|
) -> anyhow::Result<()> {
|
|
let config = self.build_per_turn_config(
|
|
session_configuration,
|
|
resolved_environments.local_selection(),
|
|
);
|
|
let local_process_cwd = config.cwd.to_path_buf();
|
|
let desired = McpDesiredState {
|
|
config: Arc::new(config),
|
|
auth,
|
|
submit_id: INITIAL_SUBMIT_ID.to_owned(),
|
|
originator: session_configuration.originator.clone(),
|
|
session_source: session_configuration.session_source.clone(),
|
|
environments: resolved_environments.clone(),
|
|
local_process_cwd,
|
|
disabled_plugin_ids: session_configuration.disabled_plugin_ids.clone(),
|
|
};
|
|
self.publish_mcp_runtime(
|
|
&desired,
|
|
mcp_projection,
|
|
/*ready_selected_capability_roots*/ &[],
|
|
Some(self.mcp_elicitation_reviewer()),
|
|
)
|
|
.instrument(info_span!(
|
|
"session_init.mcp_manager_init",
|
|
otel.name = "session_init.mcp_manager_init",
|
|
))
|
|
.await;
|
|
|
|
self.services.mcp_runtime.validate_required_servers().await
|
|
}
|
|
|
|
/// Adds effective executor-owned configuration from this exact thread snapshot.
|
|
pub(super) fn project_selected_environment_mcp_servers<'a>(
|
|
&'a self,
|
|
config: &'a Config,
|
|
environments: &'a TurnEnvironmentSnapshot,
|
|
mut projection: McpRuntimeProjection,
|
|
) -> BoxFuture<'a, McpRuntimeProjection> {
|
|
Box::pin(async move {
|
|
if self.isolation == codex_extension_api::SessionIsolation::Isolated
|
|
|| self
|
|
.services
|
|
.mcp_thread_init
|
|
.get::<crate::mcp::McpServersDisabled>()
|
|
.is_some()
|
|
{
|
|
return projection;
|
|
}
|
|
|
|
let mut catalog = None;
|
|
let mut registered = HashSet::new();
|
|
for selected in environments.turn_environments() {
|
|
let environment = selected.environment();
|
|
if !environment.is_remote() {
|
|
continue;
|
|
}
|
|
|
|
let environment_id = &selected.selection.environment_id;
|
|
let discovery = environment
|
|
.discover_http_mcp_servers(selected.cwd().clone())
|
|
.await;
|
|
let outcome = if discovery.is_ok() {
|
|
"success"
|
|
} else {
|
|
"error"
|
|
};
|
|
// Count completed discovery attempts, including refreshes, before host policy
|
|
// or MCP startup determines whether the server's tools become available.
|
|
self.services.session_telemetry.counter(
|
|
"codex.mcp.executor_discovery",
|
|
/*inc*/ 1,
|
|
&[("outcome", outcome)],
|
|
);
|
|
let servers = match discovery {
|
|
Ok(servers) => servers,
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
environment_id,
|
|
%error,
|
|
"failed to discover executor-local MCP servers"
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
for (name, mut server) in servers {
|
|
let outcome = if server.enabled {
|
|
"found"
|
|
} else {
|
|
"unavailable"
|
|
};
|
|
self.services.session_telemetry.counter(
|
|
"codex.mcp.executor_discovery.server",
|
|
/*inc*/ 1,
|
|
&[("server_name", name.as_str()), ("outcome", outcome)],
|
|
);
|
|
if name == CODEX_APPS_MCP_SERVER_NAME
|
|
|| !server.is_local_environment()
|
|
|| projection
|
|
.config
|
|
.mcp_server_catalog
|
|
.server(&name)
|
|
.is_some_and(|existing| {
|
|
existing.config().environment_id != *environment_id
|
|
|| !matches!(
|
|
existing.source(),
|
|
McpServerSource::Plugin(_)
|
|
| McpServerSource::SelectedPlugin(_)
|
|
)
|
|
})
|
|
|| registered.contains(&name)
|
|
{
|
|
continue;
|
|
}
|
|
let McpServerTransportConfig::StreamableHttp {
|
|
env_http_headers,
|
|
http_headers_helper,
|
|
..
|
|
} = &server.transport
|
|
else {
|
|
continue;
|
|
};
|
|
if http_headers_helper.is_some()
|
|
|| env_http_headers
|
|
.as_ref()
|
|
.is_some_and(|headers| !headers.is_empty())
|
|
{
|
|
tracing::warn!(
|
|
environment_id,
|
|
server = name,
|
|
"executor-local HTTP header helpers are not supported"
|
|
);
|
|
continue;
|
|
}
|
|
server.environment_id = environment_id.clone();
|
|
if let Some(requirements) = config
|
|
.config_layer_stack
|
|
.requirements()
|
|
.mcp_servers
|
|
.as_ref()
|
|
&& !requirements
|
|
.value
|
|
.get(&name)
|
|
.is_some_and(|requirement| server.matches_requirement(requirement))
|
|
{
|
|
server.enabled = false;
|
|
server.disabled_reason = Some(McpServerDisabledReason::Requirements {
|
|
source: requirements.source.clone(),
|
|
});
|
|
}
|
|
registered.insert(name.clone());
|
|
catalog
|
|
.get_or_insert_with(|| projection.config.mcp_server_catalog.to_builder())
|
|
.register(McpServerRegistration::from_executor_config(name, server));
|
|
}
|
|
}
|
|
|
|
if let Some(catalog) = catalog {
|
|
let selections = environments.all_selections();
|
|
let environment_scope = McpEnvironmentScope::Selected(&selections);
|
|
projection.config.mcp_server_catalog =
|
|
catalog.build_with_environment_authority(|environment_id| {
|
|
environment_scope.authority_for(environment_id)
|
|
});
|
|
}
|
|
projection
|
|
})
|
|
}
|
|
|
|
#[tracing::instrument(name = "mcp.runtime.refresh", skip_all)]
|
|
pub(super) async fn publish_mcp_runtime(
|
|
&self,
|
|
desired: &McpDesiredState,
|
|
mcp_projection: McpRuntimeProjection,
|
|
ready_selected_capability_roots: &[SelectedCapabilityRoot],
|
|
elicitation_reviewer: Option<ElicitationReviewerHandle>,
|
|
) {
|
|
let mcp_projection = self
|
|
.project_selected_environment_mcp_servers(
|
|
&desired.config,
|
|
&desired.environments,
|
|
mcp_projection,
|
|
)
|
|
.await;
|
|
let selected_plugins = mcp_projection.selected_plugins.clone();
|
|
let input = self.build_mcp_runtime_input(
|
|
desired,
|
|
mcp_projection,
|
|
ready_selected_capability_roots,
|
|
elicitation_reviewer,
|
|
);
|
|
AuthStorageOriginator::from_client_name(&desired.originator)
|
|
.scope(self.services.mcp_runtime.replace(input))
|
|
.await;
|
|
self.services.thread_extension_data.insert(selected_plugins);
|
|
}
|
|
|
|
pub(super) fn build_mcp_runtime_input(
|
|
&self,
|
|
desired: &McpDesiredState,
|
|
mcp_projection: McpRuntimeProjection,
|
|
ready_selected_capability_roots: &[SelectedCapabilityRoot],
|
|
elicitation_reviewer: Option<ElicitationReviewerHandle>,
|
|
) -> McpRuntimeInput {
|
|
let auth = desired.auth.clone();
|
|
let McpRuntimeProjection {
|
|
mut config,
|
|
plugins_available,
|
|
selected_plugins: _,
|
|
} = mcp_projection;
|
|
config.approval_policy = desired.config.permissions.approval_policy.clone();
|
|
config.permission_profile = desired.config.permissions.effective_permission_profile();
|
|
config.host_cwd = desired.config.cwd.clone();
|
|
config.approvals_reviewer = desired.config.approvals_reviewer;
|
|
config.environment_cwds = desired
|
|
.environments
|
|
.turn_environments()
|
|
.map(|environment| {
|
|
(
|
|
environment.selection.environment_id.clone(),
|
|
environment.cwd().clone(),
|
|
)
|
|
})
|
|
.collect();
|
|
config
|
|
.environment_cwds
|
|
.entry(codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string())
|
|
.or_insert_with(|| PathUri::from_abs_path(&desired.config.cwd));
|
|
config.environment_use_mxc = desired
|
|
.environments
|
|
.turn_environments()
|
|
.map(|environment| {
|
|
(
|
|
environment.selection.environment_id.clone(),
|
|
environment.windows_sandbox_selection_for_turn_metadata()
|
|
== WindowsSandboxSelection::Mxc,
|
|
)
|
|
})
|
|
.collect();
|
|
config
|
|
.environment_use_mxc
|
|
.entry(codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string())
|
|
.or_insert(
|
|
cfg!(windows)
|
|
&& desired.config.effective_local_windows_sandbox_type()
|
|
== SandboxType::WindowsMxc,
|
|
);
|
|
let mcp_servers = effective_mcp_servers(&config, auth.as_ref());
|
|
config.set_server_permissions(
|
|
&mcp_servers,
|
|
desired.environments.turn_environments().map(|environment| {
|
|
// Freeze the executor paths used to interpret this profile with the MCP call.
|
|
let mut sandbox = environment.sandbox_context(/*additional_permissions*/ None);
|
|
sandbox.permissions = sandbox
|
|
.permissions
|
|
.materialize_project_roots_with_path_uris(&sandbox.workspace_roots);
|
|
(
|
|
environment.selection.environment_id.clone(),
|
|
Arc::new(sandbox),
|
|
)
|
|
}),
|
|
);
|
|
let mcp_config = Arc::new(config);
|
|
let runtime_context =
|
|
self.mcp_runtime_context(&desired.environments, &desired.local_process_cwd);
|
|
McpRuntimeInput {
|
|
network_policy_metadata: None,
|
|
runtime_auth: desired
|
|
.config
|
|
.features
|
|
.enabled(Feature::RuntimeAgentInstances)
|
|
.then(|| {
|
|
(
|
|
Arc::clone(&self.services.runtime_auth),
|
|
desired.config.runtime_auth_mode,
|
|
)
|
|
}),
|
|
startup_policy: if matches!(desired.session_source, SessionSource::SubAgent(_)) {
|
|
McpStartupPolicy::DeferCachedServers
|
|
} else if desired
|
|
.config
|
|
.features
|
|
.enabled(Feature::McpLazyStdioStartup)
|
|
{
|
|
McpStartupPolicy::DeferCachedStdio
|
|
} else {
|
|
McpStartupPolicy::Eager
|
|
},
|
|
config: mcp_config,
|
|
plugins_available,
|
|
ready_selected_capability_roots: ready_selected_capability_roots.to_vec(),
|
|
mcp_servers,
|
|
submit_id: desired.submit_id.clone(),
|
|
tx_event: Some(self.get_tx_event()),
|
|
startup_cancellation_token: CancellationToken::new(),
|
|
runtime_context,
|
|
codex_apps_tools_cache: self.services.mcp_manager.codex_apps_tools_cache(),
|
|
tool_catalog_cache: self.services.mcp_manager.tool_catalog_cache(),
|
|
codex_apps_tools_cache_key: connector_runtime_context_key(auth.as_ref()),
|
|
client_mcp_extensions: self.services.client_mcp_extensions.for_mcp_servers(),
|
|
auth,
|
|
auth_manager: Some(Arc::clone(&self.services.auth_manager)),
|
|
elicitation_reviewer,
|
|
elicitation_lifecycle: Some(self.mcp_elicitation_lifecycle()),
|
|
}
|
|
}
|
|
}
|