use super::*; impl Session { #[expect( clippy::await_holding_invalid_type, reason = "active turn checks and turn state updates must remain atomic" )] pub async fn request_mcp_server_elicitation( &self, turn_context: &TurnContext, request_id: RequestId, params: McpServerElicitationRequestParams, ) -> Option { let server_name = params.server_name.clone(); let request = match params.request { McpServerElicitationRequest::Form { meta, message, requested_schema, } => { let requested_schema = match serde_json::to_value(requested_schema) { Ok(requested_schema) => requested_schema, Err(err) => { warn!( "failed to serialize MCP elicitation schema for server_name: {server_name}, request_id: {request_id}: {err:#}" ); return None; } }; codex_protocol::approvals::ElicitationRequest::Form { meta, message, requested_schema, } } McpServerElicitationRequest::Url { meta, message, url, elicitation_id, } => codex_protocol::approvals::ElicitationRequest::Url { meta, message, url, elicitation_id, }, }; let (tx_response, rx_response) = oneshot::channel(); let prev_entry = { let mut active = self.active_turn.lock().await; match active.as_mut() { Some(at) => { let mut ts = at.turn_state.lock().await; ts.insert_pending_elicitation( server_name.clone(), request_id.clone(), tx_response, ) } None => None, } }; if prev_entry.is_some() { warn!( "Overwriting existing pending elicitation for server_name: {server_name}, request_id: {request_id}" ); } let id = match request_id { rmcp::model::NumberOrString::String(value) => { codex_protocol::mcp::RequestId::String(value.to_string()) } rmcp::model::NumberOrString::Number(value) => { codex_protocol::mcp::RequestId::Integer(value) } }; let event = EventMsg::ElicitationRequest(ElicitationRequestEvent { turn_id: params.turn_id, server_name, id, request, }); self.send_event(turn_context, event).await; rx_response.await.ok() } #[expect( clippy::await_holding_invalid_type, reason = "active turn checks and manager fallback must stay serialized" )] pub async fn resolve_elicitation( &self, server_name: String, id: RequestId, response: ElicitationResponse, ) -> anyhow::Result<()> { let entry = { let mut active = self.active_turn.lock().await; match active.as_mut() { Some(at) => { let mut ts = at.turn_state.lock().await; ts.remove_pending_elicitation(&server_name, &id) } None => None, } }; if let Some(tx_response) = entry { tx_response .send(response) .map_err(|e| anyhow::anyhow!("failed to send elicitation response: {e:?}"))?; return Ok(()); } self.services .mcp_connection_manager .read() .await .resolve_elicitation(server_name, id, response) .await } #[expect( clippy::await_holding_invalid_type, reason = "MCP resource calls are serialized through the session-owned manager guard" )] pub async fn list_resources( &self, server: &str, params: Option, ) -> anyhow::Result { self.services .mcp_connection_manager .read() .await .list_resources(server, params) .await } #[expect( clippy::await_holding_invalid_type, reason = "MCP resource calls are serialized through the session-owned manager guard" )] pub async fn list_resource_templates( &self, server: &str, params: Option, ) -> anyhow::Result { self.services .mcp_connection_manager .read() .await .list_resource_templates(server, params) .await } #[expect( clippy::await_holding_invalid_type, reason = "MCP resource calls are serialized through the session-owned manager guard" )] pub async fn read_resource( &self, server: &str, params: ReadResourceRequestParams, ) -> anyhow::Result { self.services .mcp_connection_manager .read() .await .read_resource(server, params) .await } #[expect( clippy::await_holding_invalid_type, reason = "MCP tool calls are serialized through the session-owned manager guard" )] pub async fn call_tool( &self, server: &str, tool: &str, arguments: Option, meta: Option, ) -> anyhow::Result { self.services .mcp_connection_manager .read() .await .call_tool(server, tool, arguments, meta) .await } #[expect( clippy::await_holding_invalid_type, reason = "MCP tool metadata reads through the session-owned manager guard" )] pub(crate) async fn resolve_mcp_tool_info(&self, tool_name: &ToolName) -> Option { self.services .mcp_connection_manager .read() .await .resolve_tool_info(tool_name) .await } async fn refresh_mcp_servers_inner( &self, turn_context: &TurnContext, mcp_servers: HashMap, store_mode: OAuthCredentialsStoreMode, ) { let auth = self.services.auth_manager.auth().await; let config = self.get_config().await; let mcp_config = config .to_mcp_config(self.services.plugins_manager.as_ref()) .await; let tool_plugin_provenance = self .services .mcp_manager .tool_plugin_provenance(config.as_ref()) .await; let background_authorization_header_value = if let Some(auth) = auth.as_ref() { self.services .auth_manager .chatgpt_authorization_header_for_auth(auth) .await } else { None }; let mcp_servers = with_codex_apps_mcp_with_authorization_header( mcp_servers, auth.as_ref(), &mcp_config, background_authorization_header_value.as_deref(), ); let auth_statuses = compute_auth_statuses(mcp_servers.iter(), store_mode).await; { let mut guard = self.services.mcp_startup_cancellation_token.lock().await; guard.cancel(); *guard = CancellationToken::new(); } let (refreshed_manager, cancel_token) = McpConnectionManager::new( &mcp_servers, store_mode, auth_statuses, &turn_context.config.permissions.approval_policy, turn_context.sub_id.clone(), self.get_tx_event(), turn_context.sandbox_policy.get().clone(), McpRuntimeEnvironment::new( turn_context .environment .clone() .unwrap_or_else(|| Arc::new(Environment::default())), turn_context.cwd.to_path_buf(), ), config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), tool_plugin_provenance, ) .await; { let mut guard = self.services.mcp_startup_cancellation_token.lock().await; if guard.is_cancelled() { cancel_token.cancel(); } *guard = cancel_token; } let mut manager = self.services.mcp_connection_manager.write().await; *manager = refreshed_manager; } pub(crate) async fn refresh_mcp_servers_if_requested(&self, turn_context: &TurnContext) { let refresh_config = { self.pending_mcp_server_refresh_config.lock().await.take() }; let Some(refresh_config) = refresh_config else { return; }; let McpServerRefreshConfig { mcp_servers, mcp_oauth_credentials_store_mode, } = refresh_config; let mcp_servers = match serde_json::from_value::>(mcp_servers) { Ok(servers) => servers, Err(err) => { warn!("failed to parse MCP server refresh config: {err}"); return; } }; let store_mode = match serde_json::from_value::( mcp_oauth_credentials_store_mode, ) { Ok(mode) => mode, Err(err) => { warn!("failed to parse MCP OAuth refresh config: {err}"); return; } }; self.refresh_mcp_servers_inner(turn_context, mcp_servers, store_mode) .await; } pub(crate) async fn refresh_mcp_servers_now( &self, turn_context: &TurnContext, mcp_servers: HashMap, store_mode: OAuthCredentialsStoreMode, ) { self.refresh_mcp_servers_inner(turn_context, mcp_servers, store_mode) .await; } #[cfg(test)] pub(crate) async fn mcp_startup_cancellation_token(&self) -> CancellationToken { self.services .mcp_startup_cancellation_token .lock() .await .clone() } pub(crate) async fn cancel_mcp_startup(&self) { self.services .mcp_startup_cancellation_token .lock() .await .cancel(); } }