use std::collections::BTreeMap;use codex_app_server_protocol::AppInfo;use codex_app_server_protocol::McpElicitationObjectType;use codex_app_server_protocol::McpElicitationSchema;use codex_app_server_protocol::McpServerElicitationRequest;use codex_app_server_protocol::McpServerElicitationRequestParams;use serde::Deserialize;use serde::Serialize;use serde_json::json;use crate::DiscoverableTool;use crate::DiscoverableToolAction;use crate::DiscoverableToolType;pub const REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE: &str = "tool_suggestion";pub const REQUEST_PLUGIN_INSTALL_PERSIST_KEY: &str = "persist";pub const REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE: &str = "always";#[derive(Debug, Deserialize)]pub struct RequestPluginInstallArgs { pub tool_type: DiscoverableToolType, pub action_type: DiscoverableToolAction, pub tool_id: String, pub suggest_reason: String,}#[derive(Debug, Serialize, PartialEq, Eq)]pub struct RequestPluginInstallResult { pub completed: bool, pub user_confirmed: bool, pub tool_type: DiscoverableToolType, pub action_type: DiscoverableToolAction, pub tool_id: String, pub tool_name: String, pub suggest_reason: String,}#[derive(Debug, Serialize, PartialEq, Eq)]pub struct RequestPluginInstallMeta<'a> { pub codex_approval_kind: &'static str, pub persist: &'static str, pub tool_type: DiscoverableToolType, pub suggest_type: DiscoverableToolAction, pub suggest_reason: &'a str, pub tool_id: &'a str, pub tool_name: &'a str, #[serde(skip_serializing_if = "Option::is_none")] pub install_url: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] pub remote_plugin_id: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] pub app_connector_ids: Option<&'a [String]>,}pub fn build_request_plugin_install_elicitation_request( server_name: &str, thread_id: String, turn_id: String, args: &RequestPluginInstallArgs, suggest_reason: &str, tool: &DiscoverableTool,) -> McpServerElicitationRequestParams { let message = suggest_reason.to_string(); McpServerElicitationRequestParams { thread_id, turn_id: Some(turn_id), server_name: server_name.to_string(), request: McpServerElicitationRequest::Form { meta: Some(json!(build_request_plugin_install_meta( args.tool_type, args.action_type, suggest_reason, tool, ))), message, requested_schema: McpElicitationSchema { schema_uri: None, type_: McpElicitationObjectType::Object, properties: BTreeMap::new(), required: None, }, }, }}pub fn all_requested_connectors_picked_up( expected_connector_ids: &[String], accessible_connectors: &[AppInfo],) -> bool { expected_connector_ids.iter().all(|connector_id| { verified_connector_install_completed(connector_id, accessible_connectors) })}pub fn verified_connector_install_completed( tool_id: &str, accessible_connectors: &[AppInfo],) -> bool { accessible_connectors .iter() .find(|connector| connector.id == tool_id) .is_some_and(|connector| connector.is_accessible)}fn build_request_plugin_install_meta<'a>( tool_type: DiscoverableToolType, action_type: DiscoverableToolAction, suggest_reason: &'a str, tool: &'a DiscoverableTool,) -> RequestPluginInstallMeta<'a> { let (remote_plugin_id, app_connector_ids) = match tool { DiscoverableTool::Connector(_) => (None, None), DiscoverableTool::Plugin(plugin) => ( plugin.remote_plugin_id.as_deref(), Some(plugin.app_connector_ids.as_slice()), ), }; RequestPluginInstallMeta { codex_approval_kind: REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE, persist: REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE, tool_type, suggest_type: action_type, suggest_reason, tool_id: tool.id(), tool_name: tool.name(), install_url: tool.install_url(), remote_plugin_id, app_connector_ids, }}#[cfg(test)]#[path = "request_plugin_install_tests.rs"]mod tests;