Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 62 additions & 31 deletions codex-rs/codex-mcp/src/connection_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ use serde_json::Value as JsonValue;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use tracing::Instrument;
use tracing::info_span;
use tracing::instrument;
use tracing::trace;
use tracing::trace_span;
Expand Down Expand Up @@ -105,6 +106,7 @@ pub fn tool_is_model_visible(tool: &ToolInfo) -> bool {
pub struct McpConnectionManager {
clients: HashMap<String, AsyncManagedClient>,
server_metadata: HashMap<String, McpServerMetadata>,
required_servers: Vec<String>,
tool_plugin_provenance: Arc<ToolPluginProvenance>,
host_owned_codex_apps_enabled: bool,
prefix_mcp_tool_names: bool,
Expand All @@ -113,14 +115,15 @@ pub struct McpConnectionManager {
}

impl McpConnectionManager {
#[allow(clippy::new_ret_no_self, clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
pub async fn new(
mcp_servers: &HashMap<String, EffectiveMcpServer>,
store_mode: OAuthCredentialsStoreMode,
auth_entries: HashMap<String, McpAuthStatusEntry>,
approval_policy: &Constrained<AskForApproval>,
submit_id: String,
tx_event: Sender<Event>,
startup_cancellation_token: CancellationToken,
initial_permission_profile: PermissionProfile,
runtime_context: McpRuntimeContext,
codex_home: PathBuf,
Expand All @@ -131,8 +134,13 @@ impl McpConnectionManager {
tool_plugin_provenance: ToolPluginProvenance,
auth: Option<&CodexAuth>,
elicitation_reviewer: Option<ElicitationReviewerHandle>,
) -> (Self, CancellationToken) {
let cancel_token = CancellationToken::new();
) -> Self {
let mut required_servers = mcp_servers
.iter()
.filter(|(_, server)| server.enabled() && server.required())
.map(|(name, _)| name.clone())
.collect::<Vec<_>>();
required_servers.sort();
let mut clients = HashMap::new();
let mut server_metadata = HashMap::new();
let mut join_set = JoinSet::new();
Expand All @@ -152,7 +160,7 @@ impl McpConnectionManager {
.filter(|(_, server)| server.enabled())
{
server_metadata.insert(server_name.clone(), McpServerMetadata::from(&server));
let cancel_token = cancel_token.child_token();
let cancel_token = startup_cancellation_token.child_token();
let _ = emit_update(
startup_submit_id.as_str(),
&tx_event,
Expand Down Expand Up @@ -237,11 +245,12 @@ impl McpConnectionManager {
let manager = Self {
clients,
server_metadata,
required_servers,
tool_plugin_provenance,
host_owned_codex_apps_enabled,
prefix_mcp_tool_names,
elicitation_requests: elicitation_requests.clone(),
startup_cancellation_token: cancel_token.clone(),
startup_cancellation_token: startup_cancellation_token.clone(),
};
tokio::spawn(async move {
let outcomes = join_set.join_all().await;
Expand All @@ -265,7 +274,53 @@ impl McpConnectionManager {
})
.await;
});
(manager, cancel_token)
manager
}

/// Waits for every required server and reports their startup failures together.
///
/// Callers must make the manager reachable to request handlers before awaiting this method,
/// because server initialization may require client elicitation.
pub async fn validate_required_servers(&self) -> Result<()> {
let failures = async {
let mut failures = Vec::new();
for server_name in &self.required_servers {
let Some(async_managed_client) = self.clients.get(server_name).cloned() else {
failures.push(McpStartupFailure {
server: server_name.clone(),
error: format!("required MCP server `{server_name}` was not initialized"),
});
continue;
};

match async_managed_client.client().await {
Ok(_) => {}
Err(error) => failures.push(McpStartupFailure {
server: server_name.clone(),
error: startup_outcome_error_message(error),
}),
}
}
failures
}
.instrument(info_span!(
"session_init.required_mcp_wait",
otel.name = "session_init.required_mcp_wait",
session_init.required_mcp_server_count = self.required_servers.len(),
))
.await;
if failures.is_empty() {
return Ok(());
}

let details = failures
.iter()
.map(|failure| format!("{}: {}", failure.server, failure.error))
.collect::<Vec<_>>()
.join("; ");
Err(anyhow!(
"required MCP servers failed to initialize: {details}"
))
}

pub fn new_uninitialized_with_permission_profile(
Expand All @@ -276,6 +331,7 @@ impl McpConnectionManager {
Self {
clients: HashMap::new(),
server_metadata: HashMap::new(),
required_servers: Vec::new(),
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
host_owned_codex_apps_enabled: false,
prefix_mcp_tool_names,
Expand Down Expand Up @@ -374,31 +430,6 @@ impl McpConnectionManager {
}
}

pub async fn required_startup_failures(
&self,
required_servers: &[String],
) -> Vec<McpStartupFailure> {
let mut failures = Vec::new();
for server_name in required_servers {
let Some(async_managed_client) = self.clients.get(server_name).cloned() else {
failures.push(McpStartupFailure {
server: server_name.clone(),
error: format!("required MCP server `{server_name}` was not initialized"),
});
continue;
};

match async_managed_client.client().await {
Ok(_) => {}
Err(error) => failures.push(McpStartupFailure {
server: server_name.clone(),
error: startup_outcome_error_message(error),
}),
}
}
failures
}

/// Returns all tools with model-visible names normalized.
#[instrument(level = "trace", skip_all, fields(mcp_server_count = self.clients.len()))]
pub async fn list_all_tools(&self) -> Vec<ToolInfo> {
Expand Down
21 changes: 14 additions & 7 deletions codex-rs/codex-mcp/src/connection_manager_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1167,13 +1167,15 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
),
]);

let (manager, cancel_token) = McpConnectionManager::new(
let cancel_token = CancellationToken::new();
let manager = McpConnectionManager::new(
&mcp_servers,
OAuthCredentialsStoreMode::default(),
HashMap::new(),
&approval_policy,
String::new(),
tx_event,
cancel_token.clone(),
PermissionProfile::default(),
McpRuntimeContext::new(
Arc::new(EnvironmentManager::without_environments()),
Expand Down Expand Up @@ -1201,13 +1203,18 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
.wait_for_server_ready("stdio", Duration::from_millis(10))
.await
);
let failures = manager
.required_startup_failures(&["stdio".to_string()])
.await;
assert_eq!(failures.len(), 1);
assert_eq!(failures[0].server, "stdio");
let error = match manager
.clients
.get("stdio")
.expect("stdio client")
.client()
.await
{
Ok(_) => panic!("local stdio MCP startup should fail"),
Err(error) => error,
};
assert_eq!(
failures[0].error,
startup_outcome_error_message(error),
"local stdio MCP server `stdio` requires a local environment"
);
cancel_token.cancel();
Expand Down
9 changes: 7 additions & 2 deletions codex-rs/codex-mcp/src/mcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use rmcp::model::ElicitationCapability;
use rmcp::model::ReadResourceRequestParams;
use rmcp::model::ReadResourceResult;
use serde_json::Value;
use tokio_util::sync::CancellationToken;

use crate::codex_apps::codex_apps_tools_cache_key;
use crate::connection_manager::McpConnectionManager;
Expand Down Expand Up @@ -293,13 +294,15 @@ pub async fn read_mcp_resource(
.await;
let (tx_event, rx_event) = unbounded();
drop(rx_event);
let (manager, cancel_token) = McpConnectionManager::new(
let cancel_token = CancellationToken::new();
let manager = McpConnectionManager::new(
&mcp_servers,
config.mcp_oauth_credentials_store_mode,
auth_statuses,
&config.approval_policy,
String::new(),
tx_event,
cancel_token.clone(),
PermissionProfile::default(),
runtime_context,
config.codex_home.clone(),
Expand Down Expand Up @@ -363,13 +366,15 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
let (tx_event, rx_event) = unbounded();
drop(rx_event);

let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
let cancel_token = CancellationToken::new();
let mcp_connection_manager = McpConnectionManager::new(
&mcp_servers,
config.mcp_oauth_credentials_store_mode,
auth_status_entries.clone(),
&config.approval_policy,
submit_id,
tx_event,
cancel_token.clone(),
PermissionProfile::default(),
runtime_context,
config.codex_home.clone(),
Expand Down
5 changes: 4 additions & 1 deletion codex-rs/core/src/connectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use codex_protocol::models::PermissionProfile;
use codex_tools::DiscoverableTool;
use rmcp::model::ToolAnnotations;
use serde::Deserialize;
use tokio_util::sync::CancellationToken;
use tracing::warn;

use crate::config::Config;
Expand Down Expand Up @@ -285,13 +286,15 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
let (tx_event, rx_event) = unbounded();
drop(rx_event);

let (mut mcp_connection_manager, cancel_token) = McpConnectionManager::new(
let cancel_token = CancellationToken::new();
let mut mcp_connection_manager = McpConnectionManager::new(
&mcp_servers,
config.mcp_oauth_credentials_store_mode,
auth_status_entries,
&config.permissions.approval_policy,
INITIAL_SUBMIT_ID.to_owned(),
tx_event,
cancel_token.clone(),
PermissionProfile::default(),
// Connector discovery is threadless. Use an actually configured env if
// one exists, but do not reintroduce the old hidden-local fallback.
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/core/src/mcp_tool_call_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::tempdir;
use tokio_util::sync::CancellationToken;
use tracing::Instrument;
use tracing::Level;
use tracing_subscriber::fmt::format::FmtSpan;
Expand Down Expand Up @@ -1256,13 +1257,14 @@ fn codex_apps_auth_failure_metadata() -> McpToolApprovalMetadata {

async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: &TurnContext) {
let auth = session.services.auth_manager.auth().await;
let (manager, _cancel_token) = codex_mcp::McpConnectionManager::new(
let manager = codex_mcp::McpConnectionManager::new(
&HashMap::new(),
turn_context.config.mcp_oauth_credentials_store_mode,
HashMap::new(),
&turn_context.approval_policy,
turn_context.sub_id.clone(),
session.get_tx_event(),
CancellationToken::new(),
turn_context.permission_profile(),
codex_mcp::McpRuntimeContext::new(Arc::clone(&session.services.environment_manager), {
#[allow(deprecated)]
Expand Down
19 changes: 7 additions & 12 deletions codex-rs/core/src/session/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,18 +338,21 @@ impl Session {
turn_context.cwd.to_path_buf(),
),
};
{
let mcp_startup_cancellation_token = {
let mut guard = self.services.mcp_startup_cancellation_token.lock().await;
guard.cancel();
*guard = CancellationToken::new();
}
let (refreshed_manager, cancel_token) = McpConnectionManager::new(
let cancellation_token = CancellationToken::new();
*guard = cancellation_token.clone();
cancellation_token
};
let refreshed_manager = McpConnectionManager::new(
&mcp_servers,
store_mode,
auth_statuses,
&turn_context.approval_policy,
turn_context.sub_id.clone(),
self.get_tx_event(),
mcp_startup_cancellation_token,
turn_context.permission_profile(),
mcp_runtime_context,
config.codex_home.to_path_buf(),
Expand All @@ -366,14 +369,6 @@ impl Session {
let current_manager = self.services.mcp_connection_manager.read().await;
refreshed_manager.set_elicitations_auto_deny(current_manager.elicitations_auto_deny());
}
{
let mut guard = self.services.mcp_startup_cancellation_token.lock().await;
if guard.is_cancelled() {
cancel_token.cancel();
}
*guard = cancel_token;
}

let mut old_manager = {
let mut manager = self.services.mcp_connection_manager.write().await;
std::mem::replace(&mut *manager, refreshed_manager)
Expand Down
Loading
Loading