Codex Handbook
codex-mcp/src/connection_manager.rs 925 lines
//! Aggregates MCP server connections for Codex.//!//! [`McpConnectionManager`] owns the set of running async RMCP clients keyed by//! MCP server name. It coordinates startup status events, keeps server origin//! metadata, aggregates tools/resources/templates across servers, routes tool//! calls to the right client, and exposes the public manager API used by//! `codex-core`.use std::collections::HashMap;use std::path::PathBuf;use std::sync::Arc;use std::sync::atomic::Ordering;use std::time::Duration;use std::time::Instant;use crate::McpAuthStatusEntry;use crate::codex_apps::CodexAppsToolsCacheContext;use crate::codex_apps::CodexAppsToolsCacheKey;use crate::codex_apps::write_cached_codex_apps_tools_if_needed;use crate::elicitation::ElicitationRequestManager;use crate::elicitation::ElicitationReviewerHandle;use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;use crate::mcp::ToolPluginProvenance;use crate::rmcp_client::AsyncManagedClient;use crate::rmcp_client::DEFAULT_STARTUP_TIMEOUT;use crate::rmcp_client::MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC;use crate::rmcp_client::MCP_TOOLS_LIST_DURATION_METRIC;use crate::rmcp_client::ManagedClient;use crate::rmcp_client::StartupOutcomeError;use crate::rmcp_client::list_tools_for_client_uncached;use crate::runtime::McpRuntimeContext;use crate::runtime::emit_duration;use crate::server::EffectiveMcpServer;use crate::server::McpServerMetadata;use crate::tools::ToolInfo;use crate::tools::filter_tools;use crate::tools::normalize_tools_for_model_with_prefix;use crate::tools::tool_with_model_visible_input_schema;use anyhow::Context;use anyhow::Result;use anyhow::anyhow;use async_channel::Sender;use codex_config::Constrained;use codex_config::McpServerTransportConfig;use codex_config::types::AuthKeyringBackendKind;use codex_config::types::OAuthCredentialsStoreMode;use codex_login::CodexAuth;use codex_protocol::mcp::CallToolResult;use codex_protocol::mcp::McpServerInfo;use codex_protocol::models::PermissionProfile;use codex_protocol::protocol::AskForApproval;use codex_protocol::protocol::Event;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::McpStartupCompleteEvent;use codex_protocol::protocol::McpStartupFailure;use codex_protocol::protocol::McpStartupStatus;use codex_protocol::protocol::McpStartupUpdateEvent;use codex_rmcp_client::ElicitationResponse;use rmcp::model::ElicitationCapability;use rmcp::model::ListResourceTemplatesResult;use rmcp::model::ListResourcesResult;use rmcp::model::PaginatedRequestParams;use rmcp::model::ReadResourceRequestParams;use rmcp::model::ReadResourceResult;use rmcp::model::RequestId;use rmcp::model::Resource;use rmcp::model::ResourceTemplate;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;use tracing::warn;const MCP_UI_META_KEY: &str = "ui";const MCP_UI_VISIBILITY_META_KEY: &str = "visibility";const MCP_UI_MODEL_VISIBILITY: &str = "model";/// Returns whether a tool may be included in model-facing tool declarations.////// Tools without visibility metadata remain visible./// Tools with visibility metadata are hidden unless they explicitly include `model`.////// <https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx#resource-discovery>pub fn tool_is_model_visible(tool: &ToolInfo) -> bool {    let Some(visibility) = tool        .tool        .meta        .as_deref()        .and_then(|meta| meta.get(MCP_UI_META_KEY))        .and_then(JsonValue::as_object)        .and_then(|ui| ui.get(MCP_UI_VISIBILITY_META_KEY))        .and_then(JsonValue::as_array)    else {        return true;    };    visibility        .iter()        .any(|target| target.as_str() == Some(MCP_UI_MODEL_VISIBILITY))}/// A thin wrapper around a set of running [`RmcpClient`] instances.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,    elicitation_requests: ElicitationRequestManager,    startup_cancellation_token: CancellationToken,}impl McpConnectionManager {    #[allow(clippy::too_many_arguments)]    pub async fn new(        mcp_servers: &HashMap<String, EffectiveMcpServer>,        store_mode: OAuthCredentialsStoreMode,        keyring_backend_kind: AuthKeyringBackendKind,        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,        codex_apps_tools_cache_key: CodexAppsToolsCacheKey,        host_owned_codex_apps_enabled: bool,        prefix_mcp_tool_names: bool,        client_elicitation_capability: ElicitationCapability,        tool_plugin_provenance: ToolPluginProvenance,        auth: Option<&CodexAuth>,        elicitation_reviewer: Option<ElicitationReviewerHandle>,    ) -> 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();        let elicitation_requests = ElicitationRequestManager::new(            approval_policy.value(),            initial_permission_profile,            elicitation_reviewer,        );        let tool_plugin_provenance = Arc::new(tool_plugin_provenance);        let startup_submit_id = submit_id.clone();        let codex_apps_auth_provider = auth            .filter(|auth| auth.uses_codex_backend())            .map(codex_model_provider::auth_provider_from_auth);        let mcp_servers = mcp_servers.clone();        for (server_name, server) in mcp_servers            .into_iter()            .filter(|(_, server)| server.enabled())        {            server_metadata.insert(server_name.clone(), McpServerMetadata::from(&server));            let cancel_token = startup_cancellation_token.child_token();            let _ = emit_update(                startup_submit_id.as_str(),                &tx_event,                McpStartupUpdateEvent {                    server: server_name.clone(),                    status: McpStartupStatus::Starting,                },            )            .await;            let codex_apps_tools_cache_context = if server_name == CODEX_APPS_MCP_SERVER_NAME {                Some(CodexAppsToolsCacheContext {                    codex_home: codex_home.clone(),                    user_key: codex_apps_tools_cache_key.clone(),                })            } else {                None            };            let uses_env_bearer_token =                server                    .configured_config()                    .is_some_and(|config| match &config.transport {                        McpServerTransportConfig::StreamableHttp {                            bearer_token_env_var,                            ..                        } => bearer_token_env_var.is_some(),                        McpServerTransportConfig::Stdio { .. } => false,                    });            let runtime_auth_provider =                if server_name == CODEX_APPS_MCP_SERVER_NAME && !uses_env_bearer_token {                    codex_apps_auth_provider.clone()                } else {                    None                };            let async_managed_client = AsyncManagedClient::new(                server_name.clone(),                server,                store_mode,                keyring_backend_kind,                cancel_token.clone(),                tx_event.clone(),                elicitation_requests.clone(),                codex_apps_tools_cache_context,                Arc::clone(&tool_plugin_provenance),                runtime_context.clone(),                runtime_auth_provider,                client_elicitation_capability.clone(),            );            clients.insert(server_name.clone(), async_managed_client.clone());            let tx_event = tx_event.clone();            let submit_id = startup_submit_id.clone();            let auth_entry = auth_entries.get(&server_name).cloned();            join_set.spawn(async move {                let mut outcome = async_managed_client.client().await;                if cancel_token.is_cancelled() {                    outcome = Err(StartupOutcomeError::Cancelled);                }                let status = match &outcome {                    Ok(_) => McpStartupStatus::Ready,                    Err(StartupOutcomeError::Cancelled) => McpStartupStatus::Cancelled,                    Err(error) => {                        let error_str = mcp_init_error_display(                            server_name.as_str(),                            auth_entry.as_ref(),                            error,                        );                        McpStartupStatus::Failed { error: error_str }                    }                };                let _ = emit_update(                    submit_id.as_str(),                    &tx_event,                    McpStartupUpdateEvent {                        server: server_name.clone(),                        status,                    },                )                .await;                (server_name, outcome)            });        }        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: startup_cancellation_token.clone(),        };        tokio::spawn(async move {            let outcomes = join_set.join_all().await;            let mut summary = McpStartupCompleteEvent::default();            for (server_name, outcome) in outcomes {                match outcome {                    Ok(_) => summary.ready.push(server_name),                    Err(StartupOutcomeError::Cancelled) => summary.cancelled.push(server_name),                    Err(StartupOutcomeError::Failed { error }) => {                        summary.failed.push(McpStartupFailure {                            server: server_name,                            error,                        })                    }                }            }            let _ = tx_event                .send(Event {                    id: startup_submit_id,                    msg: EventMsg::McpStartupComplete(summary),                })                .await;        });        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(        approval_policy: &Constrained<AskForApproval>,        permission_profile: &PermissionProfile,        prefix_mcp_tool_names: bool,    ) -> Self {        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,            elicitation_requests: ElicitationRequestManager::new(                approval_policy.value(),                permission_profile.clone(),                /*reviewer*/ None,            ),            startup_cancellation_token: CancellationToken::new(),        }    }    pub fn has_servers(&self) -> bool {        !self.clients.is_empty()    }    pub(crate) fn contains_server(&self, server_name: &str) -> bool {        self.clients.contains_key(server_name)    }    /// Stop all MCP clients owned by this manager and terminate stdio server processes.    pub async fn shutdown(&self) {        self.startup_cancellation_token.cancel();        for client in self.clients.values() {            client.shutdown().await;        }    }    pub fn server_origin(&self, server_name: &str) -> Option<&str> {        self.server_metadata            .get(server_name)            .and_then(|metadata| metadata.origin.as_ref())            .map(super::server::McpServerOrigin::as_str)    }    pub fn server_pollutes_memory(&self, server_name: &str) -> bool {        self.server_metadata            .get(server_name)            .is_none_or(|metadata| metadata.pollutes_memory)    }    pub fn plugin_id_for_mcp_server_name(&self, server_name: &str) -> Option<&str> {        self.tool_plugin_provenance            .plugin_id_for_mcp_server_name(server_name)    }    pub fn is_selected_plugin_mcp_server(&self, server_name: &str) -> bool {        self.tool_plugin_provenance            .is_selected_plugin_mcp_server(server_name)    }    pub fn tool_approval_mode(        &self,        server_name: &str,        tool_name: &str,    ) -> codex_config::AppToolApproval {        self.server_metadata            .get(server_name)            .map(|metadata| metadata.tool_approval_mode(tool_name))            .unwrap_or_default()    }    pub fn is_host_owned_codex_apps_server(&self, server_name: &str) -> bool {        self.host_owned_codex_apps_enabled && server_name == CODEX_APPS_MCP_SERVER_NAME    }    pub fn set_approval_policy(&self, approval_policy: &Constrained<AskForApproval>) {        if let Ok(mut policy) = self.elicitation_requests.approval_policy.lock() {            *policy = approval_policy.value();        }    }    pub fn set_permission_profile(&self, permission_profile: PermissionProfile) {        if let Ok(mut profile) = self.elicitation_requests.permission_profile.lock() {            *profile = permission_profile;        }    }    pub fn elicitations_auto_deny(&self) -> bool {        self.elicitation_requests.auto_deny()    }    pub fn set_elicitations_auto_deny(&self, auto_deny: bool) {        self.elicitation_requests.set_auto_deny(auto_deny);    }    pub async fn resolve_elicitation(        &self,        server_name: String,        id: RequestId,        response: ElicitationResponse,    ) -> Result<()> {        self.elicitation_requests            .resolve(server_name, id, response)            .await    }    pub async fn wait_for_server_ready(&self, server_name: &str, timeout: Duration) -> bool {        let Some(async_managed_client) = self.clients.get(server_name) else {            return false;        };        match tokio::time::timeout(timeout, async_managed_client.client()).await {            Ok(Ok(_)) => true,            Ok(Err(_)) | Err(_) => false,        }    }    /// 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> {        let mut tools = Vec::new();        for (server_name, managed_client) in &self.clients {            let has_cached_tool_info_snapshot = managed_client.cached_tool_info_snapshot.is_some();            let startup_complete = managed_client                .startup_complete                .load(std::sync::atomic::Ordering::Acquire);            trace!(                server_name = %server_name,                has_cached_tool_info_snapshot,                startup_complete,                "waiting for MCP server tools while building tool list"            );            let Some(server_tools) = managed_client                .listed_tools()                .instrument(trace_span!(                    "list_tools_for_server",                    server_name = %server_name,                    has_cached_tool_info_snapshot,                    startup_complete                ))                .await            else {                continue;            };            trace!(                server_name = %server_name,                tool_count = server_tools.len(),                "listed MCP server tools while building tool list"            );            tools.extend(                server_tools                    .into_iter()                    .map(|tool| self.with_server_metadata(tool)),            );        }        normalize_tools_for_model_with_prefix(tools, self.prefix_mcp_tool_names)    }    /// Force-refresh codex apps tools by bypassing the in-process cache.    ///    /// On success, the refreshed tools replace the cache contents and the    /// latest filtered tools are returned directly to the caller. On    /// failure, the existing cache remains unchanged.    pub async fn hard_refresh_codex_apps_tools_cache(&self) -> Result<Vec<ToolInfo>> {        let managed_client = self            .clients            .get(CODEX_APPS_MCP_SERVER_NAME)            .ok_or_else(|| anyhow!("unknown MCP server '{CODEX_APPS_MCP_SERVER_NAME}'"))?            .client()            .await            .context("failed to get client")?;        let list_start = Instant::now();        let fetch_start = Instant::now();        let tools = list_tools_for_client_uncached(            CODEX_APPS_MCP_SERVER_NAME,            &managed_client.client,            managed_client.tool_timeout,            managed_client.server_instructions.as_deref(),        )        .await        .with_context(|| {            format!("failed to refresh tools for MCP server '{CODEX_APPS_MCP_SERVER_NAME}'")        })?;        emit_duration(            MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC,            fetch_start.elapsed(),            &[],        );        write_cached_codex_apps_tools_if_needed(            CODEX_APPS_MCP_SERVER_NAME,            managed_client.codex_apps_tools_cache_context.as_ref(),            &managed_client.server_info,            &tools,        );        emit_duration(            MCP_TOOLS_LIST_DURATION_METRIC,            list_start.elapsed(),            &[("cache", "miss")],        );        let tools = filter_tools(tools, &managed_client.tool_filter)            .into_iter()            .map(|mut tool| {                tool.tool = tool_with_model_visible_input_schema(&tool.tool);                self.with_server_metadata(tool)            });        Ok(normalize_tools_for_model_with_prefix(            tools,            self.prefix_mcp_tool_names,        ))    }    /// Returns a single map that contains all resources. Each key is the    /// server name and the value is a vector of resources.    pub async fn list_all_resources(&self) -> HashMap<String, Vec<Resource>> {        let mut join_set = JoinSet::new();        let clients_snapshot = &self.clients;        for (server_name, async_managed_client) in clients_snapshot {            let server_name = server_name.clone();            let Ok(managed_client) = async_managed_client.client().await else {                continue;            };            let timeout = managed_client.tool_timeout;            let client = managed_client.client.clone();            join_set.spawn(async move {                let mut collected: Vec<Resource> = Vec::new();                let mut cursor: Option<String> = None;                loop {                    let params = cursor.as_ref().map(|next| {                        PaginatedRequestParams::default().with_cursor(Some(next.clone()))                    });                    let response = match client.list_resources(params, timeout).await {                        Ok(result) => result,                        Err(err) => return (server_name, Err(err)),                    };                    collected.extend(response.resources);                    match response.next_cursor {                        Some(next) => {                            if cursor.as_ref() == Some(&next) {                                return (                                    server_name,                                    Err(anyhow!("resources/list returned duplicate cursor")),                                );                            }                            cursor = Some(next);                        }                        None => return (server_name, Ok(collected)),                    }                }            });        }        let mut aggregated: HashMap<String, Vec<Resource>> = HashMap::new();        while let Some(join_res) = join_set.join_next().await {            match join_res {                Ok((server_name, Ok(resources))) => {                    aggregated.insert(server_name, resources);                }                Ok((server_name, Err(err))) => {                    warn!("Failed to list resources for MCP server '{server_name}': {err:#}");                }                Err(err) => {                    warn!("Task panic when listing resources for MCP server: {err:#}");                }            }        }        aggregated    }    /// Returns a single map that contains all resource templates. Each key is the    /// server name and the value is a vector of resource templates.    pub async fn list_all_resource_templates(&self) -> HashMap<String, Vec<ResourceTemplate>> {        let mut join_set = JoinSet::new();        let clients_snapshot = &self.clients;        for (server_name, async_managed_client) in clients_snapshot {            let server_name_cloned = server_name.clone();            let Ok(managed_client) = async_managed_client.client().await else {                continue;            };            let client = managed_client.client.clone();            let timeout = managed_client.tool_timeout;            join_set.spawn(async move {                let mut collected: Vec<ResourceTemplate> = Vec::new();                let mut cursor: Option<String> = None;                loop {                    let params = cursor.as_ref().map(|next| {                        PaginatedRequestParams::default().with_cursor(Some(next.clone()))                    });                    let response = match client.list_resource_templates(params, timeout).await {                        Ok(result) => result,                        Err(err) => return (server_name_cloned, Err(err)),                    };                    collected.extend(response.resource_templates);                    match response.next_cursor {                        Some(next) => {                            if cursor.as_ref() == Some(&next) {                                return (                                    server_name_cloned,                                    Err(anyhow!(                                        "resources/templates/list returned duplicate cursor"                                    )),                                );                            }                            cursor = Some(next);                        }                        None => return (server_name_cloned, Ok(collected)),                    }                }            });        }        let mut aggregated: HashMap<String, Vec<ResourceTemplate>> = HashMap::new();        while let Some(join_res) = join_set.join_next().await {            match join_res {                Ok((server_name, Ok(templates))) => {                    aggregated.insert(server_name, templates);                }                Ok((server_name, Err(err))) => {                    warn!(                        "Failed to list resource templates for MCP server '{server_name}': {err:#}"                    );                }                Err(err) => {                    warn!("Task panic when listing resource templates for MCP server: {err:#}");                }            }        }        aggregated    }    /// Invoke the tool indicated by the (server, tool) pair.    pub async fn call_tool(        &self,        server: &str,        tool: &str,        arguments: Option<serde_json::Value>,        meta: Option<serde_json::Value>,    ) -> Result<CallToolResult> {        let client = self.client_by_name(server).await?;        if !client.tool_filter.allows(tool) {            return Err(anyhow!(                "tool '{tool}' is disabled for MCP server '{server}'"            ));        }        let result: rmcp::model::CallToolResult = client            .client            .call_tool(tool.to_string(), arguments, meta, client.tool_timeout)            .await            .with_context(|| format!("tool call failed for `{server}/{tool}`"))?;        let content = result            .content            .into_iter()            .map(|content| {                serde_json::to_value(content)                    .unwrap_or_else(|_| serde_json::Value::String("<content>".to_string()))            })            .collect();        Ok(CallToolResult {            content,            structured_content: result.structured_content,            is_error: result.is_error,            meta: result.meta.and_then(|meta| serde_json::to_value(meta).ok()),        })    }    pub async fn server_supports_sandbox_state_meta_capability(        &self,        server: &str,    ) -> Result<bool> {        Ok(self            .client_by_name(server)            .await?            .server_supports_sandbox_state_meta_capability)    }    /// List resources from the specified server.    pub async fn list_resources(        &self,        server: &str,        params: Option<PaginatedRequestParams>,    ) -> Result<ListResourcesResult> {        let managed = self.client_by_name(server).await?;        let timeout = managed.tool_timeout;        managed            .client            .list_resources(params, timeout)            .await            .with_context(|| format!("resources/list failed for `{server}`"))    }    /// List resource templates from the specified server.    pub async fn list_resource_templates(        &self,        server: &str,        params: Option<PaginatedRequestParams>,    ) -> Result<ListResourceTemplatesResult> {        let managed = self.client_by_name(server).await?;        let client = managed.client.clone();        let timeout = managed.tool_timeout;        client            .list_resource_templates(params, timeout)            .await            .with_context(|| format!("resources/templates/list failed for `{server}`"))    }    /// Read a resource from the specified server.    pub async fn read_resource(        &self,        server: &str,        params: ReadResourceRequestParams,    ) -> Result<ReadResourceResult> {        let managed = self.client_by_name(server).await?;        let client = managed.client.clone();        let timeout = managed.tool_timeout;        let uri = params.uri.clone();        client            .read_resource(params, timeout)            .await            .with_context(|| format!("resources/read failed for `{server}` ({uri})"))    }    /// Returns presentation metadata without waiting for uncached clients still initializing.    /// Cached values will be used if available and the server is still starting up.    pub(crate) async fn list_available_server_infos(&self) -> HashMap<String, McpServerInfo> {        let mut server_infos = HashMap::new();        for (server_name, client) in &self.clients {            if !client.startup_complete.load(Ordering::Acquire) {                if let Some(server_info) = client.cached_server_info.clone() {                    server_infos.insert(server_name.clone(), server_info);                }                continue;            }            match client.client().await {                Ok(managed_client) => {                    server_infos.insert(server_name.clone(), managed_client.server_info);                }                Err(_) => {                    if let Some(server_info) = client.cached_server_info.clone() {                        server_infos.insert(server_name.clone(), server_info);                    }                }            }        }        server_infos    }    fn with_server_metadata(&self, mut tool: ToolInfo) -> ToolInfo {        let Some(metadata) = self.server_metadata.get(&tool.server_name) else {            tool.supports_parallel_tool_calls = false;            tool.server_origin = None;            return tool;        };        tool.supports_parallel_tool_calls = metadata.supports_parallel_tool_calls;        tool.server_origin = metadata            .origin            .as_ref()            .map(|origin| origin.as_str().to_string());        tool    }    async fn client_by_name(&self, name: &str) -> Result<ManagedClient> {        self.clients            .get(name)            .ok_or_else(|| anyhow!("unknown MCP server '{name}'"))?            .client()            .await            .context("failed to get client")    }    #[cfg(test)]    fn new_uninitialized(        approval_policy: &Constrained<AskForApproval>,        permission_profile: &Constrained<PermissionProfile>,        prefix_mcp_tool_names: bool,    ) -> Self {        Self::new_uninitialized_with_permission_profile(            approval_policy,            permission_profile.get(),            prefix_mcp_tool_names,        )    }}impl Drop for McpConnectionManager {    fn drop(&mut self) {        self.startup_cancellation_token.cancel();        self.clients.clear();    }}async fn emit_update(    submit_id: &str,    tx_event: &Sender<Event>,    update: McpStartupUpdateEvent,) -> Result<(), async_channel::SendError<Event>> {    tx_event        .send(Event {            id: submit_id.to_string(),            msg: EventMsg::McpStartupUpdate(update),        })        .await}fn mcp_init_error_display(    server_name: &str,    entry: Option<&McpAuthStatusEntry>,    err: &StartupOutcomeError,) -> String {    if let Some(McpServerTransportConfig::StreamableHttp {        url,        bearer_token_env_var,        http_headers,        ..    }) = entry.and_then(|entry| entry.config.as_ref().map(|config| &config.transport))        && url == "https://api.githubcopilot.com/mcp/"        && bearer_token_env_var.is_none()        && http_headers.as_ref().map(HashMap::is_empty).unwrap_or(true)    {        format!(            "GitHub MCP does not support OAuth. Log in by adding a personal access token (https://github.com/settings/personal-access-tokens) to your environment and config.toml:\n[mcp_servers.{server_name}]\nbearer_token_env_var = CODEX_GITHUB_PERSONAL_ACCESS_TOKEN"        )    } else if is_mcp_client_auth_required_error(err) {        format!(            "The {server_name} MCP server is not logged in. Run `codex mcp login {server_name}`."        )    } else if is_mcp_client_startup_timeout_error(err) {        let startup_timeout_secs = match entry {            Some(entry) => match entry                .config                .as_ref()                .and_then(|config| config.startup_timeout_sec)            {                Some(timeout) => timeout,                None => DEFAULT_STARTUP_TIMEOUT,            },            None => DEFAULT_STARTUP_TIMEOUT,        }        .as_secs();        format!(            "MCP client for `{server_name}` timed out after {startup_timeout_secs} seconds. Add or adjust `startup_timeout_sec` in your config.toml:\n[mcp_servers.{server_name}]\nstartup_timeout_sec = XX"        )    } else {        format!("MCP client for `{server_name}` failed to start: {err:#}")    }}fn startup_outcome_error_message(error: StartupOutcomeError) -> String {    match error {        StartupOutcomeError::Cancelled => "MCP startup cancelled".to_string(),        StartupOutcomeError::Failed { error } => error,    }}fn is_mcp_client_auth_required_error(error: &StartupOutcomeError) -> bool {    match error {        StartupOutcomeError::Failed { error } => error.contains("Auth required"),        _ => false,    }}fn is_mcp_client_startup_timeout_error(error: &StartupOutcomeError) -> bool {    match error {        StartupOutcomeError::Failed { error } => {            error.contains("request timed out")                || error.contains("timed out handshaking with MCP server")        }        _ => false,    }}#[cfg(test)]#[path = "connection_manager_tests.rs"]mod tests;