mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] Make MCP connection startup fallible (#27261)
## Why Required MCP server startup was enforced in `Session::new` after `McpConnectionManager` had already created the clients. That split let other manager construction paths bypass the same requirement and exposed manager internals solely so the session could validate them. Keeping required-server readiness in the constructor gives every caller one consistent startup contract. ## What changed - make `McpConnectionManager::new` return `anyhow::Result<Self>` and fail when an enabled, required server cannot initialize - pass the startup cancellation token into the constructor so required-server waits remain cancellable - propagate constructor failures through resource reads, connector discovery, and MCP status collection - preserve the active manager and cancellation token when a refreshed replacement fails - keep required-startup failure collection private and cover the constructor error contract directly ## Validation - updated the focused connection-manager test to assert the complete required-server startup error - local tests not run; relying on CI
This commit is contained in:
1 parent
00a25e1e0c
commit
a7b6baecc6
8 files changed
+129
-107
No files matched your search
@@ -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;
|
||||
@@ -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,
|
||||
@@ -113,7 +115,7 @@ 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,
|
||||
@@ -121,6 +123,7 @@ impl McpConnectionManager {
|
||||
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,
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
@@ -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(
|
||||
@@ -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,
|
||||
@@ -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> {
|
||||
|
||||
Reference in new issue
Block a user