pub use auth::McpAuthStatusEntry; pub use auth::McpOAuthLoginConfig; pub use auth::McpOAuthLoginSupport; pub use auth::McpOAuthScopesSource; pub use auth::ResolvedMcpOAuthScopes; pub use auth::compute_auth_statuses; pub use auth::discover_supported_scopes; pub use auth::oauth_login_support; pub use auth::resolve_oauth_callback; pub use auth::resolve_oauth_scopes; pub use auth::should_retry_without_scopes; pub(crate) mod auth; use std::collections::HashMap; use std::collections::HashSet; use std::env; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use codex_config::ConfigLayerStack; use codex_config::Constrained; use codex_config::McpEnterpriseManagedAuthConfig; use codex_config::McpServerAuth; use codex_config::McpServerConfig; use codex_config::McpServerTransportConfig; use codex_config::types::AppToolApproval; use codex_config::types::ApprovalsReviewer; use codex_config::types::AuthKeyringBackendKind; use codex_config::types::OAuthCredentialsStoreMode; use codex_connectors::ConnectorRuntimeManager; use codex_connectors::ConnectorSnapshot; use codex_connectors::connector_runtime_context_key; use codex_login::CodexAuth; use codex_model_provider::CHATGPT_CODEX_BASE_URL; use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::mcp::McpServerInfo; use codex_protocol::mcp::Resource; use codex_protocol::mcp::ResourceTemplate; use codex_protocol::mcp::Tool; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::McpAuthStatus; use codex_rmcp_client::McpOAuthRefreshMode; use codex_utils_path_uri::PathUri; use rmcp::model::ElicitationCapability; use rmcp::model::ReadResourceRequestParams; use rmcp::model::ReadResourceResult; use serde_json::Value; use tokio_util::sync::CancellationToken; use crate::McpProtocolMode; use crate::McpServerSource; use crate::ResolvedMcpCatalog; use crate::connection_manager::McpConnectionSet; use crate::runtime::McpPublicationGate; use crate::runtime::McpRuntimeContext; use crate::runtime::McpRuntimeInput; use crate::runtime::McpStartupPolicy; use crate::server::EffectiveMcpServer; use crate::tools::ToolInfo; pub const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps"; const DEFAULT_CODEX_APPS_MCP_PRODUCT_SKU: &str = "codex"; const MCP_TOOL_NAME_PREFIX: &str = "mcp"; const MCP_TOOL_NAME_DELIMITER: &str = "__"; const CODEX_CONNECTORS_TOKEN_ENV_VAR: &str = "CODEX_CONNECTORS_TOKEN"; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum McpSnapshotDetail { #[default] Full, ToolsAndAuthOnly, } impl McpSnapshotDetail { fn include_resources(self) -> bool { matches!(self, Self::Full) } } pub fn qualified_mcp_tool_name_prefix(server_name: &str) -> String { sanitize_responses_api_tool_name(&format!( "{MCP_TOOL_NAME_PREFIX}{MCP_TOOL_NAME_DELIMITER}{server_name}{MCP_TOOL_NAME_DELIMITER}" )) } /// Returns true when MCP permission prompts should resolve as approved instead /// of being shown to the user. pub fn mcp_permission_prompt_is_auto_approved( approval_policy: AskForApproval, permission_profile: &PermissionProfile, context: McpPermissionPromptAutoApproveContext, ) -> bool { if context.tool_approval_mode == Some(AppToolApproval::Approve) { return true; } if approval_policy != AskForApproval::Never { return false; } match permission_profile { PermissionProfile::Disabled | PermissionProfile::External { .. } => true, PermissionProfile::Managed { file_system, .. } => { file_system.to_sandbox_policy().has_full_disk_write_access() } } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct McpPermissionPromptAutoApproveContext { pub tool_approval_mode: Option, } /// MCP runtime settings derived from `codex_core::config::Config`. /// /// Each published runtime and prepared call owns one immutable copy of these /// settings, so its connection, approval policy, and sandbox authority cannot /// change independently. Auth remains separate and is supplied explicitly to /// runtime entry points such as [`effective_mcp_servers`]. #[derive(Debug, Clone)] pub struct McpConfig { /// Base URL for ChatGPT-hosted app MCP servers, copied from the root config. pub chatgpt_base_url: String, /// Optional product SKU forwarded to the host-owned apps MCP server. pub apps_mcp_product_sku: Option, /// Codex home directory used for MCP OAuth state and app-tool cache files. pub codex_home: PathBuf, /// Trusted enterprise IdP inherited after normal catalog and policy resolution. pub mcp_enterprise_managed_auth: Option, pub xaa_enabled: bool, /// Preferred credential store for MCP OAuth tokens. pub mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode, /// OAuth refresh ownership selected for new MCP connections. pub oauth_refresh_mode: McpOAuthRefreshMode, /// Backend used when MCP OAuth storage is configured for keyring-backed persistence. pub auth_keyring_backend_kind: AuthKeyringBackendKind, /// Optional fixed localhost callback port for MCP OAuth login. pub mcp_oauth_callback_port: Option, /// Optional OAuth redirect URI override for MCP login. pub mcp_oauth_callback_url: Option, /// How long a tool catalog capture waits for optional MCP servers to initialize. /// /// A zero duration disables the shared grace and waits for each server's /// configured startup timeout instead. pub optional_mcp_startup_grace: Duration, /// Whether skill MCP dependency installation prompts are enabled. pub skill_mcp_dependency_install_enabled: bool, /// Approval policy used for MCP tool calls and MCP elicitation requests. pub approval_policy: Constrained, /// Permission profile captured with the connections and approval policy. pub permission_profile: PermissionProfile, /// Configuration layers used to evaluate Apps tool policy and reviewer selection. pub config_layer_stack: ConfigLayerStack, /// Default reviewer used when an Apps tool has no reviewer override. pub approvals_reviewer: ApprovalsReviewer, /// Working directories for the exact environment handles used by this runtime. pub environment_cwds: HashMap, /// Explicit server permissions; unresolved or unavailable servers have no entry. pub server_permission_profiles: HashMap, /// Optional path to `codex-linux-sandbox` for sandboxed MCP tool execution. pub codex_linux_sandbox_exe: Option, /// Whether to use legacy Landlock behavior in the MCP sandbox state. // TODO(anp): Reconcile this runtime-wide copy with TurnEnvironment::sandbox_context // for the environment that owns each MCP server. pub use_legacy_landlock: bool, /// Whether the app MCP integration is enabled by config. /// /// ChatGPT auth is checked separately before a materialized host-owned Apps /// server can be used. pub apps_enabled: bool, /// Whether model-visible MCP tool namespaces should keep the legacy /// `mcp__` prefix. pub prefix_mcp_tool_names: bool, /// MCP servers whose model-visible tool namespaces omit the `mcp__` prefix. pub non_prefixed_mcp_tool_servers: Vec, /// Protocol mode for servers other than the host-owned Codex Apps registration. pub protocol_mode: McpProtocolMode, /// Independent protocol mode for the trusted, HTTP Codex Apps registration. pub host_owned_apps_protocol_mode: McpProtocolMode, /// Client-side elicitation capabilities advertised during MCP initialization. pub client_elicitation_capability: ElicitationCapability, /// Resolved MCP registrations keyed by logical server name. pub mcp_server_catalog: ResolvedMcpCatalog, /// Plugin declarations used to attribute connector tools to plugin display names. /// MCP registrations retain their own package attribution in the catalog. pub connector_snapshot: ConnectorSnapshot, } /// Default amount of time a tool catalog capture waits for optional MCP servers. pub const DEFAULT_OPTIONAL_MCP_STARTUP_GRACE: Duration = Duration::from_secs(1); impl McpConfig { /// Resolves enabled runtime servers against the exact attachment permissions being published. pub fn set_server_permission_profiles( &mut self, servers: &HashMap, environment_profiles: impl IntoIterator, ) { let environment_profiles = environment_profiles.into_iter().collect::>(); self.server_permission_profiles = servers .iter() .filter(|(_, server)| server.enabled()) .filter_map(|(server_name, _)| { let server = self.mcp_server_catalog.server(server_name)?; let permission_profile = if server .source() .is_host_owned_apps(server_name, server.config()) { &self.permission_profile } else if let Some(permission_profile) = environment_profiles.get(&server.config().environment_id) { permission_profile } else if server.config().is_local_environment() || matches!(server.source(), McpServerSource::SelectedPlugin(_)) { &self.permission_profile } else { return None; }; Some((server_name.clone(), permission_profile.clone())) }) .collect(); } /// Returns this server's published permission authority. pub fn permission_profile_for_server(&self, server_name: &str) -> Option<&PermissionProfile> { self.server_permission_profiles.get(server_name) } /// Standalone discovery and resource reads must not inherit thread execution authority. pub fn for_threadless_operations(&self, servers: &HashMap) -> Self { let mut config = self.clone(); config.permission_profile = PermissionProfile::default(); config.server_permission_profiles = servers .iter() .filter(|(_, server)| server.enabled()) .map(|(name, _)| (name.clone(), PermissionProfile::default())) .collect(); config } } /// Plugin attribution and selection data derived from the current MCP configuration. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ToolPluginContext { plugin_display_names_by_connector_id: HashMap>, disabled_connector_ids: HashSet, plugin_display_names_by_mcp_server_name: HashMap>, plugin_ids_by_mcp_server_name: HashMap, selected_plugin_mcp_server_names: HashSet, } impl ToolPluginContext { pub(crate) fn allows_connector_id(&self, connector_id: Option<&str>) -> bool { connector_id.is_none_or(|id| !self.disabled_connector_ids.contains(id)) } pub fn plugin_display_names_for_connector_id(&self, connector_id: &str) -> &[String] { self.plugin_display_names_by_connector_id .get(connector_id) .map(Vec::as_slice) .unwrap_or(&[]) } pub fn plugin_display_names_for_mcp_server_name(&self, server_name: &str) -> &[String] { self.plugin_display_names_by_mcp_server_name .get(server_name) .map(Vec::as_slice) .unwrap_or(&[]) } pub fn plugin_id_for_mcp_server_name(&self, server_name: &str) -> Option<&str> { self.plugin_ids_by_mcp_server_name .get(server_name) .map(String::as_str) } pub(crate) fn is_selected_plugin_mcp_server(&self, server_name: &str) -> bool { self.selected_plugin_mcp_server_names.contains(server_name) } fn from_config(config: &McpConfig) -> Self { let mut tool_plugin_context = Self { disabled_connector_ids: config.connector_snapshot.disabled_connector_ids().clone(), ..Self::default() }; for connector_id in config.connector_snapshot.connector_ids() { tool_plugin_context .plugin_display_names_by_connector_id .insert( connector_id.0.clone(), config .connector_snapshot .plugin_display_names_for_connector_id(&connector_id.0) .to_vec(), ); } for (server_name, attribution) in config .mcp_server_catalog .plugin_attributions_by_server_name() { tool_plugin_context .plugin_display_names_by_mcp_server_name .insert( server_name.clone(), vec![attribution.display_name().to_string()], ); tool_plugin_context .plugin_ids_by_mcp_server_name .insert(server_name, attribution.plugin_id().to_string()); } tool_plugin_context.selected_plugin_mcp_server_names.extend( config .mcp_server_catalog .selected_plugin_server_names() .map(str::to_string), ); for plugin_names in tool_plugin_context .plugin_display_names_by_connector_id .values_mut() .chain( tool_plugin_context .plugin_display_names_by_mcp_server_name .values_mut(), ) { plugin_names.sort_unstable(); plugin_names.dedup(); } tool_plugin_context } } pub fn host_owned_codex_apps_enabled(config: &McpConfig, auth: Option<&CodexAuth>) -> bool { config.apps_enabled && auth.is_some_and(CodexAuth::uses_codex_backend) } pub fn configured_mcp_servers(config: &McpConfig) -> HashMap { config.mcp_server_catalog.configured_servers() } pub fn effective_mcp_servers( config: &McpConfig, auth: Option<&CodexAuth>, ) -> HashMap { effective_mcp_servers_from_configured(configured_mcp_servers(config), config, auth) } fn is_trusted_chatgpt_mcp_server( transport: &McpServerTransportConfig, chatgpt_base_url: &str, ) -> bool { let McpServerTransportConfig::StreamableHttp { url, .. } = transport else { return false; }; let Ok(server_url) = url::Url::parse(url) else { return false; }; if !matches!(server_url.scheme(), "http" | "https") { return false; } if url::Url::parse(CHATGPT_CODEX_BASE_URL) .ok() .is_some_and(|chatgpt_url| server_url.origin() == chatgpt_url.origin()) { return true; } url::Url::parse(chatgpt_base_url) .ok() .is_some_and(|staging_url| { staging_url.scheme() == "https" && staging_url.domain().is_some_and(|host| { host == "chatgpt-staging.com" || host.ends_with(".chatgpt-staging.com") }) && server_url.origin() == staging_url.origin() }) } /// Converts a materialized server map to its auth-gated runtime view. /// /// Compatibility built-ins and extension overlays must already be reflected in /// `configured_servers`; this function does not synthesize missing servers. pub fn effective_mcp_servers_from_configured( configured_servers: HashMap, config: &McpConfig, auth: Option<&CodexAuth>, ) -> HashMap { let mut servers = configured_servers .into_iter() .map(|(name, mut server)| { match server.auth.clone() { McpServerAuth::ChatGpt => { if !is_trusted_chatgpt_mcp_server(&server.transport, &config.chatgpt_base_url) { server.auth = McpServerAuth::OAuth; } } McpServerAuth::OAuth | McpServerAuth::EmaAuth => {} } let agent_plugin = config .mcp_server_catalog .server(&name) .is_some_and(|server| server.source().is_agent_plugin()); ( name, EffectiveMcpServer::configured(server).with_agent_plugin(agent_plugin), ) }) .collect::>(); if !host_owned_codex_apps_enabled(config, auth) { servers.remove(CODEX_APPS_MCP_SERVER_NAME); } servers } pub fn tool_plugin_context(config: &McpConfig) -> ToolPluginContext { ToolPluginContext::from_config(config) } pub async fn read_mcp_resource( config: &McpConfig, auth: Option<&CodexAuth>, runtime_context: McpRuntimeContext, codex_apps_tools_cache: ConnectorRuntimeManager, tool_catalog_cache: crate::McpToolCatalogCache, server: &str, params: ReadResourceRequestParams, ) -> anyhow::Result { let mut mcp_servers = effective_mcp_servers(config, auth); mcp_servers.retain(|name, _| name == server); let cancel_token = CancellationToken::new(); let runtime_config = config.for_threadless_operations(&mcp_servers); let manager = McpConnectionSet::new( /*previous*/ None, McpPublicationGate::already_published(), McpRuntimeInput { startup_policy: McpStartupPolicy::Eager, config: Arc::new(runtime_config), plugins_available: false, ready_selected_capability_roots: Vec::new(), mcp_servers, submit_id: String::new(), tx_event: None, startup_cancellation_token: cancel_token.clone(), runtime_context, codex_apps_tools_cache, tool_catalog_cache, codex_apps_tools_cache_key: connector_runtime_context_key(auth), client_mcp_extensions: ClientMcpExtensions::default(), auth: auth.cloned(), auth_manager: None, elicitation_reviewer: None, elicitation_lifecycle: None, }, crate::elicitation::ElicitationRequestRouter::default(), ) .await; let result = manager.read_resource(server, params).await; cancel_token.cancel(); result } #[derive(Debug, Clone)] pub struct McpServerStatusSnapshot { pub server_infos: HashMap, pub server_capabilities: HashMap, pub tools_by_server: HashMap>, pub tools_errors: HashMap, pub resources: HashMap>, pub resource_templates: HashMap>, pub auth_statuses: HashMap, pub server_names: Vec, } pub async fn collect_mcp_server_status_snapshot_with_detail( config: &McpConfig, auth: Option<&CodexAuth>, submit_id: String, runtime_context: McpRuntimeContext, codex_apps_tools_cache: ConnectorRuntimeManager, tool_catalog_cache: crate::McpToolCatalogCache, detail: McpSnapshotDetail, ) -> McpServerStatusSnapshot { let mcp_servers = effective_mcp_servers(config, auth); if mcp_servers.is_empty() { return McpServerStatusSnapshot { server_infos: HashMap::new(), server_capabilities: HashMap::new(), tools_by_server: HashMap::new(), tools_errors: HashMap::new(), resources: HashMap::new(), resource_templates: HashMap::new(), auth_statuses: HashMap::new(), server_names: Vec::new(), }; } let auth_status_entries = compute_auth_statuses( mcp_servers.iter(), config.mcp_oauth_credentials_store_mode, config.auth_keyring_backend_kind, auth, &runtime_context, ) .await; let server_names = mcp_servers.keys().cloned().collect(); let cancel_token = CancellationToken::new(); let runtime_config = config.for_threadless_operations(&mcp_servers); let mcp_connection_manager = McpConnectionSet::new( /*previous*/ None, McpPublicationGate::already_published(), McpRuntimeInput { startup_policy: McpStartupPolicy::Eager, config: Arc::new(runtime_config), plugins_available: false, ready_selected_capability_roots: Vec::new(), mcp_servers, submit_id, tx_event: None, startup_cancellation_token: cancel_token.clone(), runtime_context, codex_apps_tools_cache, tool_catalog_cache, codex_apps_tools_cache_key: connector_runtime_context_key(auth), client_mcp_extensions: ClientMcpExtensions::default(), auth: auth.cloned(), auth_manager: None, elicitation_reviewer: None, elicitation_lifecycle: None, }, crate::elicitation::ElicitationRequestRouter::default(), ) .await; let snapshot = collect_mcp_server_status_snapshot_from_manager( &mcp_connection_manager, auth_status_entries, server_names, detail, ) .await; cancel_token.cancel(); snapshot } /// The Responses API requires tool names to match `^[a-zA-Z0-9_-]+$`. /// MCP server/tool names are user-controlled, so sanitize the fully-qualified /// name we expose to the model by replacing any disallowed character with `_`. pub(crate) fn sanitize_responses_api_tool_name(name: &str) -> String { let mut sanitized = String::with_capacity(name.len()); for c in name.chars() { if c.is_ascii_alphanumeric() || c == '_' { sanitized.push(c); } else { sanitized.push('_'); } } if sanitized.is_empty() { "_".to_string() } else { sanitized } } fn codex_apps_mcp_bearer_token_env_var() -> Option { match env::var(CODEX_CONNECTORS_TOKEN_ENV_VAR) { Ok(value) if !value.trim().is_empty() => Some(CODEX_CONNECTORS_TOKEN_ENV_VAR.to_string()), Ok(_) => None, Err(env::VarError::NotPresent) => None, Err(env::VarError::NotUnicode(_)) => Some(CODEX_CONNECTORS_TOKEN_ENV_VAR.to_string()), } } fn normalize_codex_apps_base_url(base_url: &str) -> String { let mut base_url = base_url.trim_end_matches('/').to_string(); if (base_url.starts_with("https://chatgpt.com") || base_url.starts_with("https://chat.openai.com")) && !base_url.contains("/backend-api") { base_url = format!("{base_url}/backend-api"); } base_url } fn codex_apps_mcp_url_for_base_url(base_url: &str) -> String { let base_url = normalize_codex_apps_base_url(base_url); let base_url = if base_url.contains("/backend-api") || base_url.contains("/api/codex") { base_url } else { format!("{base_url}/api/codex") }; format!("{base_url}/ps/mcp") } pub fn codex_apps_mcp_server_config( chatgpt_base_url: &str, apps_mcp_product_sku: Option<&str>, originator: Option<&str>, ) -> McpServerConfig { mcp_server_config_for_url( codex_apps_mcp_url_for_base_url(chatgpt_base_url), apps_mcp_product_sku, originator, McpServerAuth::ChatGpt, ) } /// Builds the ChatGPT-hosted plugin runtime served by plugin-service. pub fn hosted_plugin_runtime_mcp_server_config( chatgpt_base_url: &str, apps_mcp_product_sku: Option<&str>, originator: Option<&str>, ) -> McpServerConfig { codex_apps_mcp_server_config(chatgpt_base_url, apps_mcp_product_sku, originator) } fn mcp_server_config_for_url( url: String, apps_mcp_product_sku: Option<&str>, originator: Option<&str>, auth_mode: McpServerAuth, ) -> McpServerConfig { let product_sku = apps_mcp_product_sku.unwrap_or(DEFAULT_CODEX_APPS_MCP_PRODUCT_SKU); let mut http_headers = HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())]); if let Some(originator) = originator { http_headers.insert("originator".to_string(), originator.to_string()); } let env_http_headers = None; McpServerConfig { transport: McpServerTransportConfig::StreamableHttp { url, bearer_token_env_var: codex_apps_mcp_bearer_token_env_var(), http_headers: Some(http_headers), env_http_headers, http_headers_helper: None, }, auth: auth_mode, environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: true, required: false, supports_parallel_tool_calls: false, omit_tools_from: None, disabled_reason: None, startup_timeout_sec: Some(Duration::from_secs(30)), tool_timeout_sec: None, default_tools_approval_mode: None, enabled_tools: None, disabled_tools: None, scopes: None, oauth: None, oauth_resource: None, tools: HashMap::new(), } } fn protocol_tool_from_rmcp_tool(name: &str, tool: &rmcp::model::Tool) -> Option { match serde_json::to_value(tool) { Ok(value) => match Tool::from_mcp_value(value) { Ok(tool) => Some(tool), Err(err) => { tracing::warn!("Failed to convert MCP tool '{name}': {err}"); None } }, Err(err) => { tracing::warn!("Failed to serialize MCP tool '{name}': {err}"); None } } } fn auth_statuses_from_entries( auth_status_entries: &HashMap, ) -> HashMap { auth_status_entries .iter() .map(|(name, entry)| (name.clone(), McpAuthStatus::from(entry.auth_state))) .collect::>() } fn convert_mcp_resources( resources: HashMap>, ) -> HashMap> { resources .into_iter() .map(|(name, resources)| { let resources = resources .into_iter() .filter_map(|resource| match serde_json::to_value(resource) { Ok(value) => match Resource::from_mcp_value(value.clone()) { Ok(resource) => Some(resource), Err(err) => { let (uri, resource_name) = match value { Value::Object(obj) => ( obj.get("uri") .and_then(|v| v.as_str().map(ToString::to_string)), obj.get("name") .and_then(|v| v.as_str().map(ToString::to_string)), ), _ => (None, None), }; tracing::warn!( "Failed to convert MCP resource (uri={uri:?}, name={resource_name:?}): {err}" ); None } }, Err(err) => { tracing::warn!("Failed to serialize MCP resource: {err}"); None } }) .collect::>(); (name, resources) }) .collect::>() } fn convert_mcp_resource_templates( resource_templates: HashMap>, ) -> HashMap> { resource_templates .into_iter() .map(|(name, templates)| { let templates = templates .into_iter() .filter_map(|template| match serde_json::to_value(template) { Ok(value) => match ResourceTemplate::from_mcp_value(value.clone()) { Ok(template) => Some(template), Err(err) => { let (uri_template, template_name) = match value { Value::Object(obj) => ( obj.get("uriTemplate") .or_else(|| obj.get("uri_template")) .and_then(|v| v.as_str().map(ToString::to_string)), obj.get("name") .and_then(|v| v.as_str().map(ToString::to_string)), ), _ => (None, None), }; tracing::warn!( "Failed to convert MCP resource template (uri_template={uri_template:?}, name={template_name:?}): {err}" ); None } }, Err(err) => { tracing::warn!("Failed to serialize MCP resource template: {err}"); None } }) .collect::>(); (name, templates) }) .collect::>() } async fn collect_mcp_server_status_snapshot_from_manager( mcp_connection_manager: &McpConnectionSet, auth_status_entries: HashMap, server_names: Vec, detail: McpSnapshotDetail, ) -> McpServerStatusSnapshot { let ((server_infos, (tools, tools_errors)), resources, resource_templates) = tokio::join!( async { let server_infos = mcp_connection_manager.list_available_server_infos().await; let tools = mcp_connection_manager.list_tools_with_errors().await; (server_infos, tools) }, async { if detail.include_resources() { mcp_connection_manager.list_all_resources(|_| true).await } else { HashMap::new() } }, async { if detail.include_resources() { mcp_connection_manager .list_all_resource_templates(|_| true) .await } else { HashMap::new() } }, ); let mut tools_by_server = HashMap::>::new(); for tool_info in tools { let raw_tool_name = tool_info.tool.name.to_string(); let Some(tool) = protocol_tool_from_rmcp_tool(&raw_tool_name, &tool_info.tool) else { continue; }; let tool_name = tool.name.clone(); tools_by_server .entry(tool_info.server_name) .or_default() .insert(tool_name, tool); } // Status-only discovery has no event channel. Report OAuth failures from the completed // connection attempt instead of retaining the credential-presence status read beforehand. let mut auth_statuses = auth_statuses_from_entries(&auth_status_entries); for server_name in mcp_connection_manager.authentication_failed_servers().await { if auth_statuses.get(&server_name) == Some(&McpAuthStatus::OAuth) { auth_statuses.insert(server_name, McpAuthStatus::NotLoggedIn); } } McpServerStatusSnapshot { server_capabilities: mcp_connection_manager.list_available_server_capabilities(), server_infos, tools_by_server, tools_errors, resources: convert_mcp_resources(resources), resource_templates: convert_mcp_resource_templates(resource_templates), auth_statuses, server_names, } } #[cfg(test)] #[path = "mod_tests.rs"] pub(crate) mod tests;