Download codex-rs/codex-mcp/src/mcp/mod.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 32.1 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/codex-mcp/src/mcp/mod.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/codex-mcp/src/mcp/mod.rs
-
curl -L -o mod.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/codex-mcp/src/mcp/mod.rs
32.1 kB
| 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"; | |
| pub enum McpSnapshotDetail { | |
| 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() | |
| } | |
| } | |
| } | |
| pub struct McpPermissionPromptAutoApproveContext { | |
| pub tool_approval_mode: Option<AppToolApproval>, | |
| } | |
| /// 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`]. | |
| 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<String>, | |
| /// 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<McpEnterpriseManagedAuthConfig>, | |
| 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<u16>, | |
| /// Optional OAuth redirect URI override for MCP login. | |
| pub mcp_oauth_callback_url: Option<String>, | |
| /// 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<AskForApproval>, | |
| /// 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<String, PathUri>, | |
| /// Explicit server permissions; unresolved or unavailable servers have no entry. | |
| pub server_permission_profiles: HashMap<String, PermissionProfile>, | |
| /// Optional path to `codex-linux-sandbox` for sandboxed MCP tool execution. | |
| pub codex_linux_sandbox_exe: Option<PathBuf>, | |
| /// 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<String>, | |
| /// 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<String, EffectiveMcpServer>, | |
| environment_profiles: impl IntoIterator<Item = (String, PermissionProfile)>, | |
| ) { | |
| let environment_profiles = environment_profiles.into_iter().collect::<HashMap<_, _>>(); | |
| 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<String, EffectiveMcpServer>) -> 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. | |
| pub struct ToolPluginContext { | |
| plugin_display_names_by_connector_id: HashMap<String, Vec<String>>, | |
| disabled_connector_ids: HashSet<String>, | |
| plugin_display_names_by_mcp_server_name: HashMap<String, Vec<String>>, | |
| plugin_ids_by_mcp_server_name: HashMap<String, String>, | |
| selected_plugin_mcp_server_names: HashSet<String>, | |
| } | |
| 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<String, McpServerConfig> { | |
| config.mcp_server_catalog.configured_servers() | |
| } | |
| pub fn effective_mcp_servers( | |
| config: &McpConfig, | |
| auth: Option<&CodexAuth>, | |
| ) -> HashMap<String, EffectiveMcpServer> { | |
| 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<String, McpServerConfig>, | |
| config: &McpConfig, | |
| auth: Option<&CodexAuth>, | |
| ) -> HashMap<String, EffectiveMcpServer> { | |
| 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::<HashMap<_, _>>(); | |
| 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<ToolInfo>, | |
| tool_catalog_cache: crate::McpToolCatalogCache, | |
| server: &str, | |
| params: ReadResourceRequestParams, | |
| ) -> anyhow::Result<ReadResourceResult> { | |
| 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 | |
| } | |
| pub struct McpServerStatusSnapshot { | |
| pub server_infos: HashMap<String, McpServerInfo>, | |
| pub server_capabilities: HashMap<String, serde_json::Value>, | |
| pub tools_by_server: HashMap<String, HashMap<String, Tool>>, | |
| pub tools_errors: HashMap<String, String>, | |
| pub resources: HashMap<String, Vec<Resource>>, | |
| pub resource_templates: HashMap<String, Vec<ResourceTemplate>>, | |
| pub auth_statuses: HashMap<String, McpAuthStatus>, | |
| pub server_names: Vec<String>, | |
| } | |
| 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<ToolInfo>, | |
| 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<String> { | |
| 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<Tool> { | |
| 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<String, crate::mcp::auth::McpAuthStatusEntry>, | |
| ) -> HashMap<String, McpAuthStatus> { | |
| auth_status_entries | |
| .iter() | |
| .map(|(name, entry)| (name.clone(), McpAuthStatus::from(entry.auth_state))) | |
| .collect::<HashMap<_, _>>() | |
| } | |
| fn convert_mcp_resources( | |
| resources: HashMap<String, Vec<rmcp::model::Resource>>, | |
| ) -> HashMap<String, Vec<Resource>> { | |
| 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::<Vec<_>>(); | |
| (name, resources) | |
| }) | |
| .collect::<HashMap<_, _>>() | |
| } | |
| fn convert_mcp_resource_templates( | |
| resource_templates: HashMap<String, Vec<rmcp::model::ResourceTemplate>>, | |
| ) -> HashMap<String, Vec<ResourceTemplate>> { | |
| 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::<Vec<_>>(); | |
| (name, templates) | |
| }) | |
| .collect::<HashMap<_, _>>() | |
| } | |
| async fn collect_mcp_server_status_snapshot_from_manager( | |
| mcp_connection_manager: &McpConnectionSet, | |
| auth_status_entries: HashMap<String, crate::mcp::auth::McpAuthStatusEntry>, | |
| server_names: Vec<String>, | |
| 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::<String, HashMap<String, Tool>>::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, | |
| } | |
| } | |
| pub(crate) mod tests; | |