diff --git a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs index 3b5afec3f..f3a387245 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs @@ -10,6 +10,8 @@ use app_test_support::create_mock_responses_server_sequence; use app_test_support::to_response; use app_test_support::write_mock_responses_config_toml; use axum::Router; +use codex_app_server_protocol::CapabilityRootLocation; +use codex_app_server_protocol::EnvironmentAddResponse; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; @@ -22,15 +24,21 @@ use codex_app_server_protocol::McpServerToolCallParams; use codex_app_server_protocol::McpServerToolCallResponse; use codex_app_server_protocol::McpToolCallStatus; use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::SelectedCapabilityRoot; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnEnvironmentParams; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput as V2UserInput; +use codex_features::Feature; +use codex_utils_path_uri::PathUri; use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP; use core_test_support::responses; +use futures::SinkExt; +use futures::StreamExt; use pretty_assertions::assert_eq; use rmcp::handler::server::ServerHandler; use rmcp::model::BooleanSchema; @@ -56,8 +64,13 @@ use rmcp::transport::streamable_http_server::session::local::LocalSessionManager use serde_json::json; use tempfile::TempDir; use tokio::net::TcpListener; +use tokio::net::TcpStream; +use tokio::sync::oneshot; use tokio::task::JoinHandle; use tokio::time::timeout; +use tokio_tungstenite::WebSocketStream; +use tokio_tungstenite::accept_async; +use tokio_tungstenite::tungstenite::Message; const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); const TEST_SERVER_NAME: &str = "tool_server"; @@ -68,6 +81,7 @@ const ELICITATION_MESSAGE: &str = "Allow this request?"; const URL_ELICITATION_TRIGGER_MESSAGE: &str = "auth"; const URL_ELICITATION_MESSAGE: &str = "Sign in to GitHub to continue."; const URL_ELICITATION_URL: &str = "https://github.example/login/device"; +const LATE_ENVIRONMENT_ID: &str = "late-environment"; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn mcp_server_tool_call_returns_tool_result() -> Result<()> { @@ -297,6 +311,145 @@ url = "{mcp_server_url}/mcp" Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn mcp_server_elicitation_survives_environment_runtime_refresh() -> Result<()> { + let responses_server = responses::start_mock_server().await; + let (mcp_server_url, mcp_server_handle) = start_mcp_server().await?; + let exec_listener = TcpListener::bind("127.0.0.1:0").await?; + let exec_server_url = format!("ws://{}", exec_listener.local_addr()?); + let codex_home = TempDir::new()?; + write_mock_responses_config_toml( + codex_home.path(), + &responses_server.uri(), + &BTreeMap::from([(Feature::DeferredExecutor, true)]), + /*auto_compact_limit*/ 1024, + /*requires_openai_auth*/ None, + "mock_provider", + "compact", + )?; + let config_path = codex_home.path().join("config.toml"); + let mut config_toml = std::fs::read_to_string(&config_path)?; + config_toml.push_str(&format!( + r#" +[mcp_servers.{TEST_SERVER_NAME}] +url = "{mcp_server_url}/mcp" +"# + )); + std::fs::write(config_path, config_toml)?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + let add_environment_id = mcp + .send_raw_request( + "environment/add", + Some(json!({ + "environmentId": LATE_ENVIRONMENT_ID, + "execServerUrl": exec_server_url, + "connectTimeoutMs": 10_000, + })), + ) + .await?; + let add_environment_response = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(add_environment_id)), + ) + .await??; + let _: EnvironmentAddResponse = to_response(add_environment_response)?; + + let capability_root = TempDir::new()?; + let thread_start_id = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + approval_policy: Some(codex_app_server_protocol::AskForApproval::UnlessTrusted), + environments: Some(vec![TurnEnvironmentParams { + environment_id: LATE_ENVIRONMENT_ID.to_string(), + cwd: codex_utils_absolute_path::AbsolutePathBuf::try_from( + capability_root.path().to_path_buf(), + )? + .into(), + }]), + selected_capability_roots: Some(vec![SelectedCapabilityRoot { + id: "late-plugin@1".to_string(), + location: CapabilityRootLocation::Environment { + environment_id: LATE_ENVIRONMENT_ID.to_string(), + path: PathUri::from_host_native_path(capability_root.path())?, + }, + }]), + ..Default::default() + }) + .await?; + let thread_start_response = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response(thread_start_response)?; + + let tool_call_request_id = mcp + .send_mcp_server_tool_call_request(McpServerToolCallParams { + thread_id: thread.id.clone(), + server: TEST_SERVER_NAME.to_string(), + tool: TEST_TOOL_NAME.to_string(), + arguments: Some(json!({"message": ELICITATION_TRIGGER_MESSAGE})), + meta: None, + }) + .await?; + let server_request = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_request_message(), + ) + .await??; + let ServerRequest::McpServerElicitationRequest { request_id, .. } = server_request else { + panic!("expected MCP elicitation request, got: {server_request:?}"); + }; + + let (filesystem_request_tx, filesystem_request_rx) = oneshot::channel(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let exec_server_handle = tokio::spawn(serve_environment_until_shutdown( + exec_listener, + filesystem_request_tx, + shutdown_rx, + )); + let mut filesystem_request_rx = filesystem_request_rx; + timeout(DEFAULT_READ_TIMEOUT, async { + loop { + let status_request_id = mcp + .send_raw_request("mcpServerStatus/list", Some(json!({"threadId": thread.id}))) + .await?; + mcp.read_stream_until_response_message(RequestId::Integer(status_request_id)) + .await?; + if filesystem_request_rx.try_recv().is_ok() { + return Ok::<_, anyhow::Error>(()); + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await??; + + mcp.send_response( + request_id, + serde_json::to_value(McpServerElicitationRequestResponse { + action: McpServerElicitationAction::Accept, + content: Some(json!({"confirmed": true})), + meta: None, + })?, + ) + .await?; + let tool_call_response = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(tool_call_request_id)), + ) + .await??; + let response: McpServerToolCallResponse = to_response(tool_call_response)?; + assert_eq!(response.content[0].get("text"), Some(&json!("accepted"))); + + let _ = shutdown_tx.send(()); + exec_server_handle.await??; + mcp_server_handle.abort(); + let _ = mcp_server_handle.await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn mcp_server_tool_call_forwards_url_elicitation() -> Result<()> { let responses_server = responses::start_mock_server().await; @@ -683,6 +836,86 @@ async fn start_mcp_server() -> Result<(String, JoinHandle<()>)> { Ok((format!("http://{addr}"), handle)) } +async fn serve_environment_until_shutdown( + listener: TcpListener, + filesystem_request_tx: oneshot::Sender<()>, + mut shutdown_rx: oneshot::Receiver<()>, +) -> Result<()> { + let (stream, _) = listener.accept().await?; + let mut websocket = accept_async(stream).await?; + + let initialize = read_exec_server_json(&mut websocket).await?; + assert_eq!(initialize["method"], "initialize"); + websocket + .send(Message::Text( + json!({ + "id": initialize["id"], + "result": {"sessionId": "test-session"}, + }) + .to_string() + .into(), + )) + .await?; + let initialized = read_exec_server_json(&mut websocket).await?; + assert_eq!(initialized["method"], "initialized"); + let environment_info = read_exec_server_json(&mut websocket).await?; + assert_eq!(environment_info["method"], "environment/info"); + websocket + .send(Message::Text( + json!({ + "id": environment_info["id"], + "result": {"shell": {"name": "zsh", "path": "/bin/zsh"}}, + }) + .to_string() + .into(), + )) + .await?; + + let mut filesystem_request_tx = Some(filesystem_request_tx); + loop { + let request = tokio::select! { + request = read_exec_server_json(&mut websocket) => request?, + _ = &mut shutdown_rx => return Ok(()), + }; + if request["method"] + .as_str() + .is_some_and(|method| method.starts_with("fs/")) + && let Some(tx) = filesystem_request_tx.take() + { + let _ = tx.send(()); + } + if request.get("id").is_some() { + websocket + .send(Message::Text( + json!({ + "id": request["id"], + "error": {"code": -32004, "message": "not found"}, + }) + .to_string() + .into(), + )) + .await?; + } + } +} + +async fn read_exec_server_json( + websocket: &mut WebSocketStream, +) -> Result { + loop { + match websocket + .next() + .await + .ok_or_else(|| anyhow::anyhow!("exec-server websocket closed"))?? + { + Message::Text(text) => return Ok(serde_json::from_str(text.as_ref())?), + Message::Binary(bytes) => return Ok(serde_json::from_slice(bytes.as_ref())?), + Message::Ping(_) | Message::Pong(_) => {} + message => anyhow::bail!("expected JSON-RPC message, got {message:?}"), + } + } +} + async fn wait_for_mcp_tool_call_completed( mcp: &mut TestAppServer, call_id: &str, diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index 5902bc960..3107f678b 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -18,6 +18,7 @@ use crate::codex_apps_cache::CodexAppsToolsCache; use crate::codex_apps_cache::CodexAppsToolsCacheKey; use crate::codex_apps_cache::CodexAppsToolsFetchSource; use crate::elicitation::ElicitationRequestManager; +use crate::elicitation::ElicitationRequestRouter; use crate::elicitation::ElicitationReviewerHandle; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::ToolPluginProvenance; @@ -141,6 +142,7 @@ impl McpConnectionManager { tool_plugin_provenance: ToolPluginProvenance, auth: Option<&CodexAuth>, elicitation_reviewer: Option, + elicitation_router: ElicitationRequestRouter, ) -> Self { let mut required_servers = mcp_servers .iter() @@ -155,6 +157,7 @@ impl McpConnectionManager { approval_policy.value(), initial_permission_profile, elicitation_reviewer, + elicitation_router, ); let tool_plugin_provenance = Arc::new(tool_plugin_provenance); let startup_submit_id = submit_id.clone(); @@ -352,6 +355,7 @@ impl McpConnectionManager { approval_policy.value(), permission_profile.clone(), /*reviewer*/ None, + ElicitationRequestRouter::default(), ), startup_cancellation_token: CancellationToken::new(), } @@ -444,6 +448,10 @@ impl McpConnectionManager { self.elicitation_requests.set_auto_deny(auto_deny); } + pub fn elicitation_router(&self) -> ElicitationRequestRouter { + self.elicitation_requests.router() + } + pub async fn resolve_elicitation( &self, server_name: String, diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index e11fcb591..d2869fe5c 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -3,6 +3,7 @@ use crate::codex_apps_cache::CodexAppsToolsCache; use crate::codex_apps_cache::CodexAppsToolsCacheContext; use crate::declared_openai_file_input_param_names; use crate::elicitation::ElicitationRequestManager; +use crate::elicitation::ElicitationRequestRouter; use crate::elicitation::elicitation_is_rejected_by_policy; use crate::rmcp_client::AsyncManagedClient; use crate::rmcp_client::ManagedClient; @@ -274,6 +275,7 @@ async fn disabled_permissions_auto_accept_elicitation_with_empty_form_schema() { AskForApproval::Never, PermissionProfile::Disabled, /*reviewer*/ None, + ElicitationRequestRouter::default(), ); let (tx_event, _rx_event) = async_channel::bounded(1); let sender = manager.make_sender("server".to_string(), tx_event); @@ -309,6 +311,7 @@ async fn disabled_permissions_do_not_auto_accept_elicitation_with_requested_fiel AskForApproval::Never, PermissionProfile::Disabled, /*reviewer*/ None, + ElicitationRequestRouter::default(), ); let (tx_event, _rx_event) = async_channel::bounded(1); let sender = manager.make_sender("server".to_string(), tx_event); @@ -342,6 +345,100 @@ async fn disabled_permissions_do_not_auto_accept_elicitation_with_requested_fiel ); } +#[tokio::test] +async fn shared_elicitation_router_targets_the_exact_pending_request() { + let router = ElicitationRequestRouter::default(); + let manager_a = ElicitationRequestManager::new( + AskForApproval::OnRequest, + PermissionProfile::default(), + /*reviewer*/ None, + router.clone(), + ); + let manager_b = ElicitationRequestManager::new( + AskForApproval::OnRequest, + PermissionProfile::default(), + /*reviewer*/ None, + router, + ); + let (tx_event, rx_event) = async_channel::bounded(2); + let sender_a = manager_a.make_sender("server".to_string(), tx_event.clone()); + let sender_b = manager_b.make_sender("server".to_string(), tx_event); + let elicitation = codex_rmcp_client::Elicitation::Mcp( + CreateElicitationRequestParams::FormElicitationParams { + meta: None, + message: "Which runtime?".to_string(), + requested_schema: rmcp::model::ElicitationSchema::builder() + .required_property( + "runtime", + rmcp::model::PrimitiveSchema::String(rmcp::model::StringSchema::new()), + ) + .build() + .expect("schema should build"), + }, + ); + + let pending_a = tokio::spawn(sender_a(NumberOrString::Number(1), elicitation.clone())); + let EventMsg::ElicitationRequest(request_a) = rx_event.recv().await.expect("request A").msg + else { + panic!("expected elicitation request"); + }; + let pending_b = tokio::spawn(sender_b(NumberOrString::Number(1), elicitation)); + let EventMsg::ElicitationRequest(request_b) = rx_event.recv().await.expect("request B").msg + else { + panic!("expected elicitation request"); + }; + let ( + codex_protocol::mcp::RequestId::String(request_a_id), + codex_protocol::mcp::RequestId::String(request_b_id), + ) = (request_a.id, request_b.id) + else { + panic!("expected Codex-owned string request IDs"); + }; + assert_ne!(request_a_id, request_b_id); + + let response_a = ElicitationResponse { + action: ElicitationAction::Accept, + content: Some(serde_json::json!({"runtime": "a"})), + meta: None, + }; + manager_b + .resolve( + "server".to_string(), + NumberOrString::String(request_a_id.into()), + response_a.clone(), + ) + .await + .expect("runtime B should route a response to runtime A"); + let response_b = ElicitationResponse { + action: ElicitationAction::Accept, + content: Some(serde_json::json!({"runtime": "b"})), + meta: None, + }; + manager_a + .resolve( + "server".to_string(), + NumberOrString::String(request_b_id.into()), + response_b.clone(), + ) + .await + .expect("runtime A should route a response to runtime B"); + + assert_eq!( + pending_a + .await + .expect("request A task") + .expect("request A response"), + response_a + ); + assert_eq!( + pending_b + .await + .expect("request B task") + .expect("request B response"), + response_b + ); +} + #[test] fn test_normalize_tools_short_non_duplicated_names() { let tools = vec![ @@ -1158,6 +1255,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() { ToolPluginProvenance::default(), /*auth*/ None, /*elicitation_reviewer*/ None, + ElicitationRequestRouter::default(), ) .await; diff --git a/codex-rs/codex-mcp/src/elicitation.rs b/codex-rs/codex-mcp/src/elicitation.rs index d6a685b50..e4fcf49d2 100644 --- a/codex-rs/codex-mcp/src/elicitation.rs +++ b/codex-rs/codex-mcp/src/elicitation.rs @@ -8,6 +8,8 @@ use std::collections::HashMap; use std::sync::Arc; use std::sync::Mutex as StdMutex; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; use crate::mcp::McpPermissionPromptAutoApproveContext; use crate::mcp::mcp_permission_prompt_is_auto_approved; @@ -32,6 +34,8 @@ use rmcp::model::RequestId; use tokio::sync::Mutex; use tokio::sync::oneshot; +static NEXT_ELICITATION_REQUEST_ID: AtomicU64 = AtomicU64::new(0); + #[derive(Debug, Clone)] pub struct ElicitationReviewRequest { pub server_name: String, @@ -48,9 +52,36 @@ pub trait ElicitationReviewer: Send + Sync { pub type ElicitationReviewerHandle = Arc; +/// Routes model-visible elicitation response tokens to their exact pending responders. +/// +/// One router is shared by every MCP runtime created for a thread. The public response token is +/// generated by Codex rather than copied from the MCP connection, so separate runtimes may reuse +/// the same server request ID without colliding. +#[derive(Clone, Default)] +pub struct ElicitationRequestRouter { + requests: Arc>, +} + +impl ElicitationRequestRouter { + async fn resolve( + &self, + server_name: String, + id: RequestId, + response: ElicitationResponse, + ) -> Result<()> { + self.requests + .lock() + .await + .remove(&(server_name, id)) + .ok_or_else(|| anyhow!("elicitation request not found"))? + .send(response) + .map_err(|e| anyhow!("failed to send elicitation response: {e:?}")) + } +} + #[derive(Clone)] pub(crate) struct ElicitationRequestManager { - requests: Arc>, + router: ElicitationRequestRouter, pub(crate) approval_policy: Arc>, pub(crate) permission_profile: Arc>, auto_deny: Arc>, @@ -62,9 +93,10 @@ impl ElicitationRequestManager { approval_policy: AskForApproval, permission_profile: PermissionProfile, reviewer: Option, + router: ElicitationRequestRouter, ) -> Self { Self { - requests: Arc::new(Mutex::new(HashMap::new())), + router, approval_policy: Arc::new(StdMutex::new(approval_policy)), permission_profile: Arc::new(StdMutex::new(permission_profile)), auto_deny: Arc::new(StdMutex::new(false)), @@ -91,13 +123,11 @@ impl ElicitationRequestManager { id: RequestId, response: ElicitationResponse, ) -> Result<()> { - self.requests - .lock() - .await - .remove(&(server_name, id)) - .ok_or_else(|| anyhow!("elicitation request not found"))? - .send(response) - .map_err(|e| anyhow!("failed to send elicitation response: {e:?}")) + self.router.resolve(server_name, id, response).await + } + + pub(crate) fn router(&self) -> ElicitationRequestRouter { + self.router.clone() } pub(crate) fn make_sender( @@ -105,13 +135,13 @@ impl ElicitationRequestManager { server_name: String, tx_event: Sender, ) -> SendElicitation { - let elicitation_requests = self.requests.clone(); + let router = self.router.clone(); let approval_policy = self.approval_policy.clone(); let permission_profile = self.permission_profile.clone(); let auto_deny = self.auto_deny.clone(); let reviewer = self.reviewer.clone(); Box::new(move |id, elicitation| { - let elicitation_requests = elicitation_requests.clone(); + let router = router.clone(); let tx_event = tx_event.clone(); let server_name = server_name.clone(); let approval_policy = approval_policy.clone(); @@ -171,6 +201,11 @@ impl ElicitationRequestManager { } } + let public_request_id = format!( + "codex-mcp-elicitation-{}", + NEXT_ELICITATION_REQUEST_ID.fetch_add(1, Ordering::Relaxed) + ); + let routed_request_id = RequestId::String(public_request_id.clone().into()); let request = match elicitation { Elicitation::Mcp( rmcp::model::CreateElicitationRequestParams::FormElicitationParams { @@ -215,8 +250,8 @@ impl ElicitationRequestManager { }; let (tx, rx) = oneshot::channel(); { - let mut lock = elicitation_requests.lock().await; - lock.insert((server_name.clone(), id.clone()), tx); + let mut lock = router.requests.lock().await; + lock.insert((server_name.clone(), routed_request_id), tx); } let _ = tx_event .send(Event { @@ -224,14 +259,7 @@ impl ElicitationRequestManager { msg: EventMsg::ElicitationRequest(ElicitationRequestEvent { turn_id: None, server_name, - id: match id.clone() { - rmcp::model::NumberOrString::String(value) => { - ProtocolRequestId::String(value.to_string()) - } - rmcp::model::NumberOrString::Number(value) => { - ProtocolRequestId::Integer(value) - } - }, + id: ProtocolRequestId::String(public_request_id), request, }), }) diff --git a/codex-rs/codex-mcp/src/lib.rs b/codex-rs/codex-mcp/src/lib.rs index e611623d4..eda7ac239 100644 --- a/codex-rs/codex-mcp/src/lib.rs +++ b/codex-rs/codex-mcp/src/lib.rs @@ -1,5 +1,6 @@ pub use connection_manager::McpConnectionManager; pub use connection_manager::tool_is_model_visible; +pub use elicitation::ElicitationRequestRouter; pub use elicitation::ElicitationReviewRequest; pub use elicitation::ElicitationReviewer; pub use elicitation::ElicitationReviewerHandle; diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 3f24a276e..ffb797f26 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -339,6 +339,7 @@ pub async fn read_mcp_resource( tool_plugin_provenance(config), auth, /*elicitation_reviewer*/ None, + crate::elicitation::ElicitationRequestRouter::default(), ) .await; @@ -415,6 +416,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( tool_plugin_provenance, auth, /*elicitation_reviewer*/ None, + crate::elicitation::ElicitationRequestRouter::default(), ) .await; diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 97f24e043..b45cc841a 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -283,6 +283,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager( ToolPluginProvenance::default(), auth.as_ref(), /*elicitation_reviewer*/ None, + codex_mcp::ElicitationRequestRouter::default(), ) .await; diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 8c8d82037..271141613 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1416,6 +1416,7 @@ async fn host_owned_codex_apps_manager( codex_mcp::ToolPluginProvenance::default(), auth.as_ref(), /*elicitation_reviewer*/ None, + codex_mcp::ElicitationRequestRouter::default(), ) .await; Arc::new(manager) diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 50921f159..83a59c960 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -316,6 +316,7 @@ impl Session { *guard = cancellation_token.clone(); cancellation_token }; + let current_runtime = self.services.latest_mcp_runtime(); let refreshed_manager = McpConnectionManager::new( &mcp_servers, mcp_config.mcp_oauth_credentials_store_mode, @@ -338,13 +339,11 @@ impl Session { tool_plugin_provenance, auth.as_ref(), elicitation_reviewer, + current_runtime.manager().elicitation_router(), ) .await; - { - let current_manager = self.services.latest_mcp_runtime(); - refreshed_manager - .set_elicitations_auto_deny(current_manager.manager().elicitations_auto_deny()); - } + refreshed_manager + .set_elicitations_auto_deny(current_runtime.manager().elicitations_auto_deny()); self.services.publish_mcp_runtime( mcp_config, mcp_runtime_context, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 0e1609207..ed07352b1 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1207,6 +1207,7 @@ impl Session { tool_plugin_provenance, auth, Some(sess.mcp_elicitation_reviewer()), + codex_mcp::ElicitationRequestRouter::default(), ) .instrument(info_span!( "session_init.mcp_manager_init",