use std::collections::HashMap; use std::time::Duration; use std::time::Instant; use crate::config::Config; use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; use crate::connectors; use crate::guardian::GuardianApprovalRequest; use crate::guardian::GuardianMcpAnnotations; use crate::guardian::GuardianReviewContext; use crate::mcp_openai_file::rewrite_mcp_tool_arguments_for_openai_files; use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam; use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template; use crate::session::session::Session; use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::tools::ApprovalContext; use crate::tools::hook_names::HookToolName; use crate::tools::sandboxing::ApprovalAction; use crate::tools::sandboxing::ToolError; use crate::turn_metadata::McpTurnMetadataContext; use codex_analytics::AppInvocation; use codex_analytics::InvocationType; use codex_analytics::build_track_events_context; use codex_api::HostedFileUploadContext; use codex_config::ConfigLayerSource; use codex_config::types::AppToolApproval; use codex_connectors::AppToolPolicy; use codex_connectors::AppToolPolicyEvaluator; use codex_connectors::AppToolPolicyInput; use codex_features::Feature; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::MCP_TOOL_CODEX_APPS_META_KEY; use codex_mcp::McpPermissionPromptAutoApproveContext; use codex_mcp::PreparedMcpCall; use codex_mcp::SandboxState; use codex_mcp::ToolInfo; use codex_mcp::auth_elicitation_completed_result; use codex_mcp::build_auth_elicitation_plan; use codex_mcp::mcp_permission_prompt_is_auto_approved; use codex_protocol::ResponseItemId; use codex_protocol::approvals::ElicitationRequest; use codex_protocol::items::McpToolCallError; use codex_protocol::items::McpToolCallItem; use codex_protocol::items::McpToolCallStatus; use codex_protocol::items::TurnItem; use codex_protocol::mcp::CONFIRMATION_POLICIES_META_KEY; use codex_protocol::mcp::CallToolResult; use codex_protocol::mcp::is_node_repl_backed_server; use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY as MCP_TOOL_APPROVAL_KIND_KEY; use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL as MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL; use codex_protocol::mcp_approval_meta::CONNECTOR_DESCRIPTION_KEY as MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY; use codex_protocol::mcp_approval_meta::CONNECTOR_ID_KEY as MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY; use codex_protocol::mcp_approval_meta::CONNECTOR_NAME_KEY as MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY; use codex_protocol::mcp_approval_meta::PERSIST_ALWAYS as MCP_TOOL_APPROVAL_PERSIST_ALWAYS; use codex_protocol::mcp_approval_meta::PERSIST_KEY as MCP_TOOL_APPROVAL_PERSIST_KEY; use codex_protocol::mcp_approval_meta::PERSIST_SESSION as MCP_TOOL_APPROVAL_PERSIST_SESSION; use codex_protocol::mcp_approval_meta::SOURCE_CONNECTOR as MCP_TOOL_APPROVAL_SOURCE_CONNECTOR; use codex_protocol::mcp_approval_meta::SOURCE_KEY as MCP_TOOL_APPROVAL_SOURCE_KEY; use codex_protocol::mcp_approval_meta::TOOL_DESCRIPTION_KEY as MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY; use codex_protocol::mcp_approval_meta::TOOL_PARAMS_DISPLAY_KEY as MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY; use codex_protocol::mcp_approval_meta::TOOL_PARAMS_KEY as MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY; use codex_protocol::mcp_approval_meta::TOOL_TITLE_KEY as MCP_TOOL_APPROVAL_TOOL_TITLE_KEY; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::InputModality; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::McpInvocation; use codex_protocol::protocol::ReviewDecision; use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputArgs; use codex_protocol::request_user_input::RequestUserInputQuestion; use codex_protocol::request_user_input::RequestUserInputQuestionOption; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_rmcp_client::ElicitationAction; use codex_rmcp_client::ElicitationResponse; use codex_rollout::state_db; use codex_tools::ToolName; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_output_truncation::TruncationPolicy; use codex_utils_output_truncation::truncate_text; use codex_utils_path_uri::PathUri; use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP; use rmcp::model::ToolAnnotations; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; use std::sync::Arc; use tokio_util::sync::CancellationToken; use toml_edit::value; use tracing::Instrument; use tracing::Span; use tracing::error; use tracing::field::Empty; use url::Url; mod telemetry; use telemetry::McpCallMetricOutcome; use telemetry::emit_mcp_call_metrics; use telemetry::mcp_call_metric_outcome; use telemetry::record_mcp_call_outcome_span_telemetry; const MCP_RESULT_TELEMETRY_META_KEY: &str = "codex/telemetry"; const MCP_RESULT_TELEMETRY_SPAN_KEY: &str = "span"; const MCP_RESULT_TELEMETRY_TARGET_ID_KEY: &str = "target_id"; const MCP_RESULT_TELEMETRY_DID_TRIGGER_SERVER_USER_FLOW_KEY: &str = "did_trigger_server_user_flow"; const MCP_RESULT_TELEMETRY_TARGET_ID_SPAN_ATTR: &str = "codex.mcp.target.id"; const MCP_RESULT_TELEMETRY_SERVER_USER_FLOW_SPAN_ATTR: &str = "codex.mcp.server_user_flow.triggered"; const MCP_RESULT_TELEMETRY_TARGET_ID_MAX_CHARS: usize = 256; const MCP_TOOL_CALL_EVENT_RESULT_MAX_BYTES: usize = DEFAULT_OUTPUT_BYTES_CAP; /// Handles the specified tool call and dispatches the appropriate MCP tool-call /// item lifecycle events to the `Session`. #[expect(clippy::too_many_arguments)] pub(crate) async fn handle_mcp_tool_call( sess: Arc, step_context: &Arc, cancellation_token: &CancellationToken, call_id: String, originating_item_id: Option, tool_info: &ToolInfo, prepared_call: Option, hook_tool_name: HookToolName, invocation_tool_name: ToolName, arguments: String, ) -> HandledMcpToolCall { let turn_context = &step_context.turn; let server = tool_info.server_name.clone(); let tool_name = tool_info.tool.name.to_string(); // Parse the `arguments` as JSON. An empty string is OK, but invalid JSON // is not. let arguments_value = if arguments.trim().is_empty() { None } else { match serde_json::from_str::(&arguments) { Ok(value) => Some(value), Err(e) => { error!("failed to parse tool call arguments: {e}"); return HandledMcpToolCall { result: CallToolResult::from_error_text(format!("err: {e}")), tool_input: JsonValue::Object(serde_json::Map::new()), }; } } }; let invocation = McpInvocation { server: server.clone(), tool: tool_name.clone(), arguments: arguments_value.clone(), }; let Some(prepared_call) = prepared_call else { let item_metadata = McpToolCallItemMetadata::from_tool_metadata(&server, /*metadata*/ None); let result = notify_mcp_tool_call_skip( sess.as_ref(), turn_context.as_ref(), &call_id, invocation, item_metadata, format!("MCP tool `{server}/{tool_name}` is not available to the model"), /*already_started*/ false, ) .await; return HandledMcpToolCall { result: CallToolResult::from_result(result), tool_input: arguments_value .unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())), }; }; let metadata = mcp_tool_metadata(&prepared_call); let item_metadata = McpToolCallItemMetadata::from_tool_metadata(&server, Some(&metadata)); let runtime_config = prepared_call.config(); let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME { let annotations = metadata.annotations.as_ref(); AppToolPolicyEvaluator::new(&runtime_config.config_layer_stack).policy(AppToolPolicyInput { connector_id: metadata.connector_id.as_deref(), tool_name: &tool_name, tool_title: metadata.tool_title.as_deref(), destructive_hint: annotations.and_then(|annotations| annotations.destructive_hint), open_world_hint: annotations.and_then(|annotations| annotations.open_world_hint), }) } else { AppToolPolicy::default() }; let approval_mode = if server == CODEX_APPS_MCP_SERVER_NAME { app_tool_policy.approval } else { prepared_call.tool_approval_mode() }; let connector_id = metadata.connector_id.clone(); let connector_name = metadata.connector_name.clone(); if server == CODEX_APPS_MCP_SERVER_NAME && !app_tool_policy.enabled { let result = notify_mcp_tool_call_skip( sess.as_ref(), turn_context.as_ref(), &call_id, invocation, item_metadata.clone(), "MCP tool call blocked by app configuration".to_string(), /*already_started*/ false, ) .await; let status = if result.is_ok() { "ok" } else { "error" }; let outcome = McpCallMetricOutcome::from_status(status); emit_mcp_call_metrics( turn_context.as_ref(), &outcome, &server, &tool_name, connector_id.as_deref(), connector_name.as_deref(), /*duration*/ None, ); return HandledMcpToolCall { result: CallToolResult::from_result(result), tool_input: arguments_value .unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())), }; } sess.register_mcp_tool_approval_metadata(turn_context, &call_id, &invocation, metadata.clone()) .await; notify_mcp_tool_call_started( sess.as_ref(), turn_context.as_ref(), &call_id, invocation.clone(), item_metadata.clone(), ) .await; let approval_policy = if prepared_call.is_selected_plugin_server() { McpToolApprovalPolicy::for_selected_plugin(approval_mode) } else { McpToolApprovalPolicy::for_server(approval_mode) }; if let Some(decision) = maybe_request_mcp_tool_approval( &sess, step_context, cancellation_token, &call_id, &invocation, &invocation_tool_name, &hook_tool_name, &metadata, prepared_call.config(), prepared_call.permission_profile(), approval_policy, ) .await { let result = match decision { decision @ (ReviewDecision::Approved | ReviewDecision::ApprovedForSession | ReviewDecision::ApprovedMcpPolicyAmendment | ReviewDecision::ApprovedExecpolicyAmendment { .. } | ReviewDecision::NetworkPolicyAmendment { .. }) => { return handle_approved_mcp_tool_call( &sess, step_context.as_ref(), &call_id, originating_item_id.as_ref(), invocation, prepared_call, metadata, item_metadata, McpToolApprovalApplication::Apply { decision, policy: approval_policy, }, ) .await; } ReviewDecision::Denied { rejection } => { notify_mcp_tool_call_skip( sess.as_ref(), turn_context.as_ref(), &call_id, invocation, item_metadata.clone(), rejection, /*already_started*/ true, ) .await } ReviewDecision::TimedOut => { notify_mcp_tool_call_skip( sess.as_ref(), turn_context.as_ref(), &call_id, invocation, item_metadata.clone(), crate::guardian::guardian_timeout_message(turn_context.model_info()), /*already_started*/ true, ) .await } ReviewDecision::Abort => { let message = "user cancelled MCP tool call".to_string(); notify_mcp_tool_call_skip( sess.as_ref(), turn_context.as_ref(), &call_id, invocation, item_metadata.clone(), message, /*already_started*/ true, ) .await } }; let status = if result.is_ok() { "ok" } else { "error" }; let outcome = McpCallMetricOutcome::from_status(status); emit_mcp_call_metrics( turn_context.as_ref(), &outcome, &server, &tool_name, connector_id.as_deref(), connector_name.as_deref(), /*duration*/ None, ); return HandledMcpToolCall { result: CallToolResult::from_result(result), tool_input: arguments_value .unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())), }; } handle_approved_mcp_tool_call( &sess, step_context.as_ref(), &call_id, originating_item_id.as_ref(), invocation, prepared_call, metadata, item_metadata, McpToolApprovalApplication::NotRequired, ) .await } pub(crate) struct HandledMcpToolCall { pub(crate) result: CallToolResult, pub(crate) tool_input: JsonValue, } #[derive(Clone, Debug, PartialEq, Eq)] struct McpToolCallItemMetadata { connector_id: Option, link_id: Option, mcp_app_resource_uri: Option, app_name: Option, action_name: Option, plugin_id: Option, read_only_hint: Option, } impl McpToolCallItemMetadata { fn from_tool_metadata(server: &str, metadata: Option<&McpToolApprovalMetadata>) -> Self { let trusted_mcp_app_metadata = if server == CODEX_APPS_MCP_SERVER_NAME { metadata } else { None }; Self { connector_id: trusted_mcp_app_metadata .and_then(|metadata| metadata.connector_id.clone()), link_id: trusted_mcp_app_metadata.and_then(|metadata| metadata.link_id.clone()), mcp_app_resource_uri: metadata .and_then(|metadata| metadata.mcp_app_resource_uri.clone()), app_name: trusted_mcp_app_metadata.and_then(|metadata| metadata.connector_name.clone()), action_name: trusted_mcp_app_metadata .and_then(|metadata| metadata.codex_apps_meta.as_ref()) .and_then(|meta| meta.get(MCP_TOOL_RESOURCE_URI_META_KEY)) .and_then(serde_json::Value::as_str) .and_then(|resource_uri| resource_uri.trim_matches('/').rsplit('/').next()) .filter(|action_name| !action_name.is_empty()) .map(str::to_string), plugin_id: metadata.and_then(|metadata| metadata.plugin_id.clone()), read_only_hint: metadata .and_then(|metadata| metadata.annotations.as_ref()) .and_then(|annotations| annotations.read_only_hint), } } } #[expect( clippy::too_many_arguments, reason = "MCP approval must be applied inside the prepared call's catalog lease" )] async fn handle_approved_mcp_tool_call( sess: &Arc, step_context: &StepContext, call_id: &str, originating_item_id: Option<&ResponseItemId>, invocation: McpInvocation, prepared_call: PreparedMcpCall, metadata: McpToolApprovalMetadata, item_metadata: McpToolCallItemMetadata, approval_application: McpToolApprovalApplication, ) -> HandledMcpToolCall { let turn_context = step_context.turn.as_ref(); let server = invocation.server.clone(); let tool_name = invocation.tool.clone(); let arguments_value = invocation.arguments.clone(); let connector_id = metadata.connector_id.as_deref(); let connector_name = metadata.connector_name.as_deref(); let server_origin = prepared_call.server_origin().map(str::to_string); let start = Instant::now(); let mut tool_input = arguments_value .clone() .unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())); let result = async { let result = async { let result = prepared_call .call_with_preparation(/*requested_timeout*/ None, || async { if let McpToolApprovalApplication::Apply { decision, policy } = &approval_application { let session_approval_key = session_mcp_tool_approval_key( &invocation, Some(&metadata), policy.mode, ); let persistent_approval_key = if policy.allow_persistent { persistent_mcp_tool_approval_key( &invocation, Some(&metadata), policy.mode, ) } else { None }; apply_mcp_tool_approval_decision( sess, turn_context, decision, session_approval_key, persistent_approval_key, ) .await; } maybe_mark_thread_memory_mode_polluted(sess, turn_context, &prepared_call) .await; let hosted_upload = item_metadata .connector_id .as_ref() .zip(item_metadata.action_name.as_ref()) .map(|(connector_id, action_name)| HostedFileUploadContext { connector_id: connector_id.clone(), action_name: action_name.clone(), model: turn_context.model_info().slug.clone(), }); let rewritten_arguments = rewrite_mcp_tool_arguments_for_openai_files( sess, step_context, arguments_value, metadata.openai_file_input_optional_fields.as_ref(), hosted_upload.as_ref(), ) .await .map_err(anyhow::Error::msg)?; if let Some(rewritten_arguments) = rewritten_arguments.as_ref() { tool_input = rewritten_arguments.clone(); } let request_meta = build_mcp_tool_call_request_meta( step_context, &server, call_id, Some(&metadata), ); let request_meta = with_mcp_tool_call_ids_meta( request_meta, &sess.thread_id.to_string(), originating_item_id, ); let request_meta = augment_mcp_tool_request_meta_with_sandbox_state( step_context, &prepared_call, request_meta, ) .await?; let mcp_call_trace = sess .services .rollout_thread_trace .start_mcp_call_trace(call_id); Ok(( rewritten_arguments, mcp_call_trace.add_request_meta(request_meta), )) }) .await .map_err(|error| format!("tool call error: {error:?}"))?; let result = sanitize_mcp_tool_result_for_model( &turn_context.model_info().input_modalities, Ok(result), )?; Ok(maybe_request_codex_apps_auth_elicitation( sess, turn_context, prepared_call.config().approval_policy.value(), call_id, &invocation.server, Some(&metadata), result, ) .await) } .await; record_mcp_result_span_telemetry(&Span::current(), &result); result } .instrument(mcp_tool_call_span( sess, turn_context, McpToolCallSpanFields { server_name: &server, tool_name: &tool_name, call_id, server_origin: server_origin.as_deref(), connector_id, connector_name, }, )) .await; if let Err(error) = &result { tracing::warn!("MCP tool call error: {error:?}"); } let duration = start.elapsed(); notify_mcp_tool_call_completed( sess, turn_context, call_id, invocation, item_metadata, duration, truncate_mcp_tool_result_for_event(&result), ) .await; maybe_track_codex_app_used(sess, turn_context, &server, &metadata).await; let outcome = mcp_call_metric_outcome(&result); emit_mcp_call_metrics( turn_context, &outcome, &server, &tool_name, connector_id, connector_name, Some(duration), ); HandledMcpToolCall { result: CallToolResult::from_result(result), tool_input, } } fn mcp_tool_call_span( session: &Session, turn_context: &TurnContext, fields: McpToolCallSpanFields<'_>, ) -> Span { let transport = match fields.server_origin { Some("stdio") => "stdio", Some("in_process") => "in_process", Some(_) => "streamable_http", None => "", }; let span = tracing::info_span!( "mcp.tools.call", otel.kind = "client", rpc.system = "jsonrpc", rpc.method = "tools/call", mcp.server.name = fields.server_name, mcp.server.origin = fields.server_origin.unwrap_or(""), mcp.transport = transport, mcp.connector.id = fields.connector_id.unwrap_or(""), mcp.connector.name = fields.connector_name.unwrap_or(""), tool.name = fields.tool_name, tool.call_id = fields.call_id, conversation.id = %session.thread_id, session.id = %session.thread_id, turn.id = turn_context.sub_id.as_str(), server.address = Empty, server.port = Empty, codex.mcp.target.id = Empty, codex.mcp.server_user_flow.triggered = Empty, error.type = Empty, codex.mcp.error.code = Empty, ); record_server_fields(&span, fields.server_origin); span } struct McpToolCallSpanFields<'a> { server_name: &'a str, tool_name: &'a str, call_id: &'a str, server_origin: Option<&'a str>, connector_id: Option<&'a str>, connector_name: Option<&'a str>, } fn record_server_fields(span: &Span, url: Option<&str>) { let Some(url) = url else { return; }; let Ok(parsed) = Url::parse(url) else { return; }; if let Some(host) = parsed.host_str() { span.record("server.address", host); } if let Some(port) = parsed.port_or_known_default() { span.record("server.port", port as i64); } } fn record_mcp_result_span_telemetry(span: &Span, result: &Result) { record_mcp_call_outcome_span_telemetry(span, result); let Some(span_telemetry) = result .as_ref() .ok() .and_then(|result| result.meta.as_ref()) .and_then(JsonValue::as_object) .and_then(|meta| meta.get(MCP_RESULT_TELEMETRY_META_KEY)) .and_then(JsonValue::as_object) .and_then(|telemetry| telemetry.get(MCP_RESULT_TELEMETRY_SPAN_KEY)) .and_then(JsonValue::as_object) else { return; }; if let Some(target_id) = span_telemetry .get(MCP_RESULT_TELEMETRY_TARGET_ID_KEY) .and_then(JsonValue::as_str) .filter(|target_id| !target_id.is_empty()) { span.record( MCP_RESULT_TELEMETRY_TARGET_ID_SPAN_ATTR, truncate_str_to_char_boundary(target_id, MCP_RESULT_TELEMETRY_TARGET_ID_MAX_CHARS), ); } if let Some(did_trigger_server_user_flow) = span_telemetry .get(MCP_RESULT_TELEMETRY_DID_TRIGGER_SERVER_USER_FLOW_KEY) .and_then(JsonValue::as_bool) { span.record( MCP_RESULT_TELEMETRY_SERVER_USER_FLOW_SPAN_ATTR, did_trigger_server_user_flow, ); } } fn truncate_str_to_char_boundary(value: &str, max_chars: usize) -> &str { match value.char_indices().nth(max_chars) { Some((index, _)) => &value[..index], None => value, } } async fn maybe_request_codex_apps_auth_elicitation( sess: &Arc, turn_context: &TurnContext, approval_policy: AskForApproval, call_id: &str, server: &str, metadata: Option<&McpToolApprovalMetadata>, result: CallToolResult, ) -> CallToolResult { if server != CODEX_APPS_MCP_SERVER_NAME { return result; } if !turn_context .config .features .enabled(Feature::AuthElicitation) { return result; } match approval_policy { AskForApproval::Never => return result, AskForApproval::Granular(granular_config) if !granular_config.allows_mcp_elicitations() => { return result; } AskForApproval::OnRequest | AskForApproval::UnlessTrusted | AskForApproval::Granular(_) => { } } let connector_id = metadata.and_then(|metadata| metadata.connector_id.as_deref()); let connector_name = metadata.and_then(|metadata| metadata.connector_name.as_deref()); let install_url = connector_id.map(|connector_id| { codex_connectors::metadata::connector_install_url( connector_name.unwrap_or(connector_id), connector_id, ) }); let Some(plan) = build_auth_elicitation_plan(call_id, &result, connector_id, connector_name, install_url) else { return result; }; let request_id = rmcp::model::RequestId::String(plan.elicitation.elicitation_id.clone().into()); let request = ElicitationRequest::Url { meta: Some(plan.elicitation.meta), message: plan.elicitation.message, url: plan.elicitation.url, elicitation_id: plan.elicitation.elicitation_id, }; let response = sess .request_mcp_server_elicitation( turn_context, CODEX_APPS_MCP_SERVER_NAME.to_string(), request_id, request, ) .await .response; if !response .as_ref() .is_some_and(|response| response.action == ElicitationAction::Accept) { return result; } refresh_codex_apps_after_connector_auth(sess, turn_context).await; auth_elicitation_completed_result(&plan.auth_failure, result.meta) } async fn refresh_codex_apps_after_connector_auth(sess: &Arc, turn_context: &TurnContext) { let mcp_tools_result = sess.hard_refresh_latest_codex_apps_tools().await; match mcp_tools_result { Ok(mcp_tools) => { let auth = sess.services.auth_manager.auth().await; connectors::refresh_accessible_connectors_cache_from_mcp_tools( &turn_context.config, auth.as_ref(), &mcp_tools, ); } Err(err) => { tracing::warn!("failed to refresh Codex Apps tools after connector auth: {err:#}"); } } } async fn augment_mcp_tool_request_meta_with_sandbox_state( step_context: &StepContext, prepared_call: &PreparedMcpCall, mut meta: Option, ) -> anyhow::Result> { let supports_sandbox_state_meta = prepared_call .server_supports_sandbox_state_meta_capability() .await .unwrap_or(false); if !supports_sandbox_state_meta { return Ok(meta); } let server_environment_id = prepared_call.server_environment_id(); let Some(sandbox_cwd) = prepared_call .config() .environment_cwds .get(server_environment_id) .cloned() .or_else(|| sandbox_cwd_for_mcp_server(step_context, server_environment_id)) else { return Ok(meta); }; // TODO(anp): Build this metadata from the server's captured // TurnEnvironment::sandbox_context instead of the runtime-wide Landlock value. let sandbox_state = serde_json::to_value(SandboxState { permission_profile: prepared_call.permission_profile().clone(), codex_linux_sandbox_exe: prepared_call.config().codex_linux_sandbox_exe.clone(), sandbox_cwd, use_legacy_landlock: prepared_call.config().use_legacy_landlock, })?; match meta.as_mut() { Some(serde_json::Value::Object(map)) => { map.insert( codex_mcp::MCP_SANDBOX_STATE_META_CAPABILITY.to_string(), sandbox_state, ); } Some(_) => {} None => { let mut map = serde_json::Map::new(); map.insert( codex_mcp::MCP_SANDBOX_STATE_META_CAPABILITY.to_string(), sandbox_state, ); meta = Some(serde_json::Value::Object(map)); } } Ok(meta) } fn sandbox_cwd_for_mcp_server(step_context: &StepContext, environment_id: &str) -> Option { if let Some(environment) = step_context .environments .turn_environments() .find(|environment| environment.selection.environment_id == environment_id) { return Some(environment.cwd().clone()); } if environment_id == codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID { #[allow(deprecated)] return Some(PathUri::from_abs_path(&step_context.turn.cwd)); } None } async fn maybe_mark_thread_memory_mode_polluted( sess: &Session, turn_context: &TurnContext, prepared_call: &PreparedMcpCall, ) { if !turn_context.config.memories.disable_on_external_context { return; } if !prepared_call.server_pollutes_memory() { return; } state_db::mark_thread_memory_mode_polluted( sess.services.state_db.as_deref(), sess.thread_id, "mcp_tool_call", ) .await; } fn sanitize_mcp_tool_result_for_model( input_modalities: &[InputModality], result: Result, ) -> Result { let supports_image_input = input_modalities.contains(&InputModality::Image); let supports_audio_input = input_modalities.contains(&InputModality::Audio); if supports_image_input && supports_audio_input { return result; } result.map(|call_tool_result| CallToolResult { content: call_tool_result .content .iter() .map(|block| { if let Some(content_type) = block.get("type").and_then(serde_json::Value::as_str) { if content_type == "image" && !supports_image_input { return serde_json::json!({ "type": "text", "text": "", }); } if content_type == "audio" && !supports_audio_input { return serde_json::json!({ "type": "text", "text": "