From d1a791eb6896b431891b1ab3fec9269ca30da9ba Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Mon, 11 May 2026 11:37:48 -0700 Subject: [PATCH] update --- codex-rs/codex-mcp/src/connection_manager.rs | 3 ++ .../codex-mcp/src/connection_manager_tests.rs | 31 +++++++++++------ codex-rs/codex-mcp/src/mcp/mod.rs | 5 +++ codex-rs/codex-mcp/src/mcp/mod_tests.rs | 1 + codex-rs/codex-mcp/src/rmcp_client.rs | 15 +++------ codex-rs/core/src/config/config_tests.rs | 33 +++++++++++++++++++ codex-rs/core/src/config/mod.rs | 13 ++++++++ codex-rs/core/src/connectors.rs | 1 + codex-rs/core/src/mcp_tool_call_tests.rs | 1 + codex-rs/core/src/session/mcp.rs | 1 + codex-rs/core/src/session/mod.rs | 3 ++ codex-rs/core/src/session/session.rs | 9 +++++ 12 files changed, 96 insertions(+), 20 deletions(-) diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index e02b6094b398..0bd2f136233d 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -55,6 +55,7 @@ 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; @@ -178,6 +179,7 @@ impl McpConnectionManager { codex_home: PathBuf, codex_apps_tools_cache_key: CodexAppsToolsCacheKey, host_owned_codex_apps_enabled: bool, + client_elicitation_capability: ElicitationCapability, tool_plugin_provenance: ToolPluginProvenance, auth: Option<&CodexAuth>, elicitation_reviewer: Option, @@ -248,6 +250,7 @@ impl McpConnectionManager { runtime_environment.clone(), codex_home.clone(), runtime_auth_provider, + client_elicitation_capability.clone(), ); clients.insert(server_name.clone(), async_managed_client.clone()); let tx_event = tx_event.clone(); diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 61be90ed7e05..b2cf5f94f638 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -10,7 +10,6 @@ use crate::elicitation::elicitation_is_rejected_by_policy; use crate::rmcp_client::AsyncManagedClient; use crate::rmcp_client::ManagedClient; use crate::rmcp_client::StartupOutcomeError; -use crate::rmcp_client::elicitation_capability_for_server; use crate::tools::ToolFilter; use crate::tools::ToolInfo; use crate::tools::filter_tools; @@ -844,15 +843,27 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() { } #[test] -fn elicitation_capability_uses_2025_06_18_shape_for_all_servers() { - for server_name in [CODEX_APPS_MCP_SERVER_NAME, "custom_mcp"] { - let capability = elicitation_capability_for_server(server_name); - assert_eq!(capability, Some(ElicitationCapability::default())); - assert_eq!( - serde_json::to_value(capability).expect("serialize elicitation capability"), - serde_json::json!({}) - ); - } +fn elicitation_capability_uses_2025_06_18_shape_for_form_only_support() { + let capability = Some(ElicitationCapability::default()); + assert_eq!( + serde_json::to_value(capability).expect("serialize elicitation capability"), + serde_json::json!({}) + ); +} + +#[test] +fn elicitation_capability_advertises_url_support_when_enabled() { + let capability = Some(ElicitationCapability { + form: Some(rmcp::model::FormElicitationCapability::default()), + url: Some(rmcp::model::UrlElicitationCapability::default()), + }); + assert_eq!( + serde_json::to_value(capability).expect("serialize elicitation capability"), + serde_json::json!({ + "form": {}, + "url": {}, + }) + ); } #[test] diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 71f79b9b4a62..bb3bdf4199af 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -31,6 +31,7 @@ use codex_protocol::mcp::Tool; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::McpAuthStatus; +use rmcp::model::ElicitationCapability; use rmcp::model::ReadResourceRequestParams; use rmcp::model::ReadResourceResult; use serde_json::Value; @@ -129,6 +130,8 @@ pub struct McpConfig { /// ChatGPT auth is checked separately at runtime before the built-in apps /// MCP server is added. pub apps_enabled: bool, + /// Client-side elicitation capabilities advertised during MCP initialization. + pub client_elicitation_capability: ElicitationCapability, /// Config-backed MCP servers keyed by server name. /// /// Product-owned built-ins and runtime-only additions are merged later by @@ -281,6 +284,7 @@ pub async fn read_mcp_resource( config.codex_home.clone(), codex_apps_tools_cache_key(auth), host_owned_codex_apps_enabled, + config.client_elicitation_capability.clone(), tool_plugin_provenance(config), auth, /*elicitation_reviewer*/ None, @@ -349,6 +353,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( config.codex_home.clone(), codex_apps_tools_cache_key(auth), host_owned_codex_apps_enabled, + config.client_elicitation_capability.clone(), tool_plugin_provenance, auth, /*elicitation_reviewer*/ None, diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index 491341c3e922..3d2bdd45ab45 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -28,6 +28,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig { codex_linux_sandbox_exe: None, use_legacy_landlock: false, apps_enabled: false, + client_elicitation_capability: ElicitationCapability::default(), configured_mcp_servers: HashMap::new(), builtin_mcp_servers: Vec::new(), plugin_capability_summaries: Vec::new(), diff --git a/codex-rs/codex-mcp/src/rmcp_client.rs b/codex-rs/codex-mcp/src/rmcp_client.rs index c9a8ca8c339d..577bb28288df 100644 --- a/codex-rs/codex-mcp/src/rmcp_client.rs +++ b/codex-rs/codex-mcp/src/rmcp_client.rs @@ -148,6 +148,7 @@ impl AsyncManagedClient { runtime_environment: McpRuntimeEnvironment, codex_home: PathBuf, runtime_auth_provider: Option, + client_elicitation_capability: ElicitationCapability, ) -> Self { let tool_filter = server .configured_config() @@ -195,6 +196,7 @@ impl AsyncManagedClient { tx_event, elicitation_requests, codex_apps_tools_cache_context, + client_elicitation_capability, }, ) .await @@ -331,14 +333,6 @@ impl From for StartupOutcomeError { } } -pub(crate) fn elicitation_capability_for_server( - _server_name: &str, -) -> Option { - // https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#capabilities - // indicates this should be an empty object. - Some(ElicitationCapability::default()) -} - pub(crate) async fn list_tools_for_client_uncached( server_name: &str, client: &Arc, @@ -477,8 +471,8 @@ async fn start_server_task( tx_event, elicitation_requests, codex_apps_tools_cache_context, + client_elicitation_capability, } = params; - let elicitation = elicitation_capability_for_server(&server_name); let params = InitializeRequestParams { meta: None, capabilities: ClientCapabilities { @@ -486,7 +480,7 @@ async fn start_server_task( extensions: None, roots: None, sampling: None, - elicitation, + elicitation: Some(client_elicitation_capability), tasks: None, }, client_info: Implementation { @@ -562,6 +556,7 @@ struct StartServerTaskParams { tx_event: Sender, elicitation_requests: ElicitationRequestManager, codex_apps_tools_cache_context: Option, + client_elicitation_capability: ElicitationCapability, } async fn make_rmcp_client( diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index f8ef0371e520..46d0c63300fe 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -89,6 +89,9 @@ use core_test_support::PathExt; use core_test_support::TempDirExt; use core_test_support::test_absolute_path; use pretty_assertions::assert_eq; +use rmcp::model::ElicitationCapability; +use rmcp::model::FormElicitationCapability; +use rmcp::model::UrlElicitationCapability; use std::collections::BTreeMap; use std::collections::HashMap; @@ -4255,6 +4258,36 @@ async fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<( Ok(()) } +#[tokio::test] +async fn to_mcp_config_preserves_auth_elicitation_feature_from_config() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let mut config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides::default(), + codex_home.abs(), + ) + .await?; + let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); + + let mcp_config = config.to_mcp_config(&plugins_manager).await; + assert_eq!( + mcp_config.client_elicitation_capability, + ElicitationCapability::default() + ); + + let _ = config.features.enable(Feature::AuthElicitation); + let mcp_config = config.to_mcp_config(&plugins_manager).await; + assert_eq!( + mcp_config.client_elicitation_capability, + ElicitationCapability { + form: Some(FormElicitationCapability::default()), + url: Some(UrlElicitationCapability::default()), + } + ); + + Ok(()) +} + #[tokio::test] async fn to_mcp_config_includes_enabled_builtin_mcps() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index b1277eda4856..f899aad4c825 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -100,6 +100,9 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; +use rmcp::model::ElicitationCapability; +use rmcp::model::FormElicitationCapability; +use rmcp::model::UrlElicitationCapability; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; @@ -1133,6 +1136,16 @@ impl Config { codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(), use_legacy_landlock: self.features.use_legacy_landlock(), apps_enabled: self.features.enabled(Feature::Apps), + client_elicitation_capability: if self.features.enabled(Feature::AuthElicitation) { + ElicitationCapability { + form: Some(FormElicitationCapability::default()), + url: Some(UrlElicitationCapability::default()), + } + } else { + // https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#capabilities + // indicates this should be an empty object. + ElicitationCapability::default() + }, configured_mcp_servers, builtin_mcp_servers, plugin_capability_summaries: loaded_plugins.capability_summaries().to_vec(), diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 7a66e4ffa6bd..0a3b21173a1d 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -277,6 +277,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), host_owned_codex_apps_enabled, + mcp_config.client_elicitation_capability, ToolPluginProvenance::default(), auth.as_ref(), /*elicitation_reviewer*/ None, diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index a556e228ac95..326e0cfe3180 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1122,6 +1122,7 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: turn_context.config.codex_home.to_path_buf(), codex_mcp::codex_apps_tools_cache_key(auth.as_ref()), /*host_owned_codex_apps_enabled*/ true, + rmcp::model::ElicitationCapability::default(), codex_mcp::ToolPluginProvenance::default(), auth.as_ref(), /*elicitation_reviewer*/ None, diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index a7d7a965a243..102a9f2110d2 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -329,6 +329,7 @@ impl Session { config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), host_owned_codex_apps_enabled, + mcp_config.client_elicitation_capability, tool_plugin_provenance, auth.as_ref(), elicitation_reviewer, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index af36fda48738..5f7f3fd168c7 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -143,12 +143,15 @@ use codex_utils_output_truncation::TruncationPolicy; use futures::future::BoxFuture; use futures::future::Shared; use futures::prelude::*; +use rmcp::model::ElicitationCapability; +use rmcp::model::FormElicitationCapability; 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::UrlElicitationCapability; use serde_json::Value; use tokio::sync::Mutex; use tokio::sync::RwLock; diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 8c9ea1a123b7..32aaf8fb0b97 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -961,6 +961,14 @@ impl Session { let host_owned_codex_apps_enabled = config .features .apps_enabled_for_auth(auth.as_ref().is_some_and(|auth| auth.uses_codex_backend())); + let client_elicitation_capability = if config.features.enabled(Feature::AuthElicitation) { + ElicitationCapability { + form: Some(FormElicitationCapability::default()), + url: Some(UrlElicitationCapability::default()), + } + } else { + ElicitationCapability::default() + }; { let mut cancel_guard = sess.services.mcp_startup_cancellation_token.lock().await; cancel_guard.cancel(); @@ -1003,6 +1011,7 @@ impl Session { config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth), host_owned_codex_apps_enabled, + client_elicitation_capability, tool_plugin_provenance, auth, Some(sess.mcp_elicitation_reviewer()),