//! Configuration object accepted by the `codex` MCP tool-call. use codex_arg0::Arg0DispatchPaths; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_protocol::ThreadId; use codex_protocol::config_types::SandboxMode; use codex_protocol::protocol::AskForApproval; use codex_utils_json_to_toml::json_to_toml; use rmcp::model::JsonObject; use rmcp::model::Tool; use schemars::JsonSchema; use schemars::r#gen::SchemaSettings; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; /// Client-supplied configuration for a `codex` tool-call. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] #[serde(rename_all = "kebab-case")] pub struct CodexToolCallParam { /// The *initial user prompt* to start the Codex conversation. pub prompt: String, /// Optional override for the model name (e.g. 'gpt-5.2', 'gpt-5.2-codex'). #[serde(default, skip_serializing_if = "Option::is_none")] pub model: Option, /// Configuration profile from config.toml to specify default options. #[serde(default, skip_serializing_if = "Option::is_none")] pub profile: Option, /// Working directory for the session. If relative, it is resolved against /// the server process's current working directory. #[serde(default, skip_serializing_if = "Option::is_none")] pub cwd: Option, /// Approval policy for shell commands generated by the model: /// `untrusted`, `on-failure`, `on-request`, `never`. #[serde(default, skip_serializing_if = "Option::is_none")] pub approval_policy: Option, /// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`. #[serde(default, skip_serializing_if = "Option::is_none")] pub sandbox: Option, /// Individual config settings that will override what is in /// CODEX_HOME/config.toml. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option>, /// The set of instructions to use instead of the default ones. #[serde(default, skip_serializing_if = "Option::is_none")] pub base_instructions: Option, /// Developer instructions that should be injected as a developer role message. #[serde(default, skip_serializing_if = "Option::is_none")] pub developer_instructions: Option, /// Prompt used when compacting the conversation. #[serde(default, skip_serializing_if = "Option::is_none")] pub compact_prompt: Option, } /// Custom enum mirroring [`AskForApproval`], but has an extra dependency on /// [`JsonSchema`]. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum CodexToolCallApprovalPolicy { Untrusted, OnFailure, OnRequest, Never, } impl From for AskForApproval { fn from(value: CodexToolCallApprovalPolicy) -> Self { match value { CodexToolCallApprovalPolicy::Untrusted => AskForApproval::UnlessTrusted, CodexToolCallApprovalPolicy::OnFailure => AskForApproval::OnFailure, CodexToolCallApprovalPolicy::OnRequest => AskForApproval::OnRequest, CodexToolCallApprovalPolicy::Never => AskForApproval::Never, } } } /// Custom enum mirroring [`SandboxMode`] from config_types.rs, but with /// `JsonSchema` support. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum CodexToolCallSandboxMode { ReadOnly, WorkspaceWrite, DangerFullAccess, } impl From for SandboxMode { fn from(value: CodexToolCallSandboxMode) -> Self { match value { CodexToolCallSandboxMode::ReadOnly => SandboxMode::ReadOnly, CodexToolCallSandboxMode::WorkspaceWrite => SandboxMode::WorkspaceWrite, CodexToolCallSandboxMode::DangerFullAccess => SandboxMode::DangerFullAccess, } } } /// Builds a `Tool` definition (JSON schema etc.) for the Codex tool-call. pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool { let schema = SchemaSettings::draft2019_09() .with(|s| { s.inline_subschemas = true; s.option_add_null_type = false; }) .into_generator() .into_root_schema_for::(); let input_schema = create_tool_input_schema(schema, "Codex tool schema should serialize"); Tool { name: "codex".into(), title: Some("Codex".to_string()), input_schema, output_schema: Some(codex_tool_output_schema()), description: Some( "Run a Codex session. Accepts configuration parameters matching the Codex Config struct." .into(), ), annotations: None, execution: None, icons: None, meta: None, } } fn codex_tool_output_schema() -> Arc { let schema = serde_json::json!({ "type": "object", "properties": { "threadId": { "type": "string" }, "content": { "type": "string" } }, "required": ["threadId", "content"], }); match schema { serde_json::Value::Object(map) => Arc::new(map), _ => unreachable!("json literal must be an object"), } } impl CodexToolCallParam { /// Returns the initial user prompt to start the Codex conversation and the /// effective Config object generated from the supplied parameters. pub async fn into_config( self, arg0_paths: Arg0DispatchPaths, ) -> std::io::Result<(String, Config)> { let Self { prompt, model, profile, cwd, approval_policy, sandbox, config: cli_overrides, base_instructions, developer_instructions, compact_prompt, } = self; // Build the `ConfigOverrides` recognized by codex-core. let overrides = ConfigOverrides { model, config_profile: profile, cwd: cwd.map(PathBuf::from), approval_policy: approval_policy.map(Into::into), sandbox_mode: sandbox.map(Into::into), codex_self_exe: arg0_paths.codex_self_exe.clone(), codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), base_instructions, developer_instructions, compact_prompt, ..Default::default() }; let cli_overrides = cli_overrides .unwrap_or_default() .into_iter() .map(|(k, v)| (k, json_to_toml(v))) .collect(); let cfg = Config::load_with_cli_overrides_and_harness_overrides(cli_overrides, overrides).await?; Ok((prompt, cfg)) } } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct CodexToolCallReplyParam { /// DEPRECATED: use threadId instead. #[serde(default, skip_serializing_if = "Option::is_none")] conversation_id: Option, /// The thread id for this Codex session. /// This field is required, but we keep it optional here for backward /// compatibility for clients that still use conversationId. #[serde(default, skip_serializing_if = "Option::is_none")] thread_id: Option, /// The *next user prompt* to continue the Codex conversation. pub prompt: String, } impl CodexToolCallReplyParam { pub(crate) fn get_thread_id(&self) -> anyhow::Result { if let Some(thread_id) = &self.thread_id { let thread_id = ThreadId::from_string(thread_id)?; Ok(thread_id) } else if let Some(conversation_id) = &self.conversation_id { let thread_id = ThreadId::from_string(conversation_id)?; Ok(thread_id) } else { Err(anyhow::anyhow!( "either threadId or conversationId must be provided" )) } } } /// Builds a `Tool` definition for the `codex-reply` tool-call. pub(crate) fn create_tool_for_codex_tool_call_reply_param() -> Tool { let schema = SchemaSettings::draft2019_09() .with(|s| { s.inline_subschemas = true; s.option_add_null_type = false; }) .into_generator() .into_root_schema_for::(); let input_schema = create_tool_input_schema(schema, "Codex reply tool schema should serialize"); Tool { name: "codex-reply".into(), title: Some("Codex Reply".to_string()), input_schema, output_schema: Some(codex_tool_output_schema()), description: Some( "Continue a Codex conversation by providing the thread id and prompt.".into(), ), annotations: None, execution: None, icons: None, meta: None, } } fn create_tool_input_schema( schema: schemars::schema::RootSchema, panic_message: &str, ) -> Arc { #[expect(clippy::expect_used)] let schema_value = serde_json::to_value(&schema).expect(panic_message); let mut schema_object = match schema_value { serde_json::Value::Object(object) => object, _ => panic!("tool schema should serialize to a JSON object"), }; // Prefer keeping the "core" JSON Schema keys while still preserving `$defs` // in case any `$ref` leaks into the generated schema (even though we try // to inline subschemas). let mut input_schema = JsonObject::new(); for key in ["properties", "required", "type", "$defs", "definitions"] { if let Some(value) = schema_object.remove(key) { input_schema.insert(key.to_string(), value); } } Arc::new(input_schema) } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; /// We include a test to verify the exact JSON schema as "executable /// documentation" for the schema. When can track changes to this test as a /// way to audit changes to the generated schema. /// /// Seeing the fully expanded schema makes it easier to casually verify that /// the generated JSON for enum types such as "approval-policy" is compact. /// Ideally, modelcontextprotocol/inspector would provide a simpler UI for /// enum fields versus open string fields to take advantage of this. /// /// As of 2025-05-04, there is an open PR for this: /// https://github.com/modelcontextprotocol/inspector/pull/196 #[test] fn verify_codex_tool_json_schema() { let tool = create_tool_for_codex_tool_call_param(); let tool_json = serde_json::to_value(&tool).expect("tool serializes"); let expected_tool_json = serde_json::json!({ "description": "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.", "inputSchema": { "properties": { "approval-policy": { "description": "Approval policy for shell commands generated by the model: `untrusted`, `on-failure`, `on-request`, `never`.", "enum": [ "untrusted", "on-failure", "on-request", "never" ], "type": "string" }, "base-instructions": { "description": "The set of instructions to use instead of the default ones.", "type": "string" }, "compact-prompt": { "description": "Prompt used when compacting the conversation.", "type": "string" }, "config": { "additionalProperties": true, "description": "Individual config settings that will override what is in CODEX_HOME/config.toml.", "type": "object" }, "cwd": { "description": "Working directory for the session. If relative, it is resolved against the server process's current working directory.", "type": "string" }, "developer-instructions": { "description": "Developer instructions that should be injected as a developer role message.", "type": "string" }, "model": { "description": "Optional override for the model name (e.g. 'gpt-5.2', 'gpt-5.2-codex').", "type": "string" }, "profile": { "description": "Configuration profile from config.toml to specify default options.", "type": "string" }, "prompt": { "description": "The *initial user prompt* to start the Codex conversation.", "type": "string" }, "sandbox": { "description": "Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.", "enum": [ "read-only", "workspace-write", "danger-full-access" ], "type": "string" } }, "required": [ "prompt" ], "type": "object" }, "name": "codex", "outputSchema": { "properties": { "content": { "type": "string" }, "threadId": { "type": "string" } }, "required": [ "threadId", "content" ], "type": "object" }, "title": "Codex" }); assert_eq!(expected_tool_json, tool_json); } #[test] fn verify_codex_tool_reply_json_schema() { let tool = create_tool_for_codex_tool_call_reply_param(); let tool_json = serde_json::to_value(&tool).expect("tool serializes"); let expected_tool_json = serde_json::json!({ "description": "Continue a Codex conversation by providing the thread id and prompt.", "inputSchema": { "properties": { "conversationId": { "description": "DEPRECATED: use threadId instead.", "type": "string" }, "prompt": { "description": "The *next user prompt* to continue the Codex conversation.", "type": "string" }, "threadId": { "description": "The thread id for this Codex session. This field is required, but we keep it optional here for backward compatibility for clients that still use conversationId.", "type": "string" } }, "required": [ "prompt", ], "type": "object", }, "name": "codex-reply", "outputSchema": { "properties": { "content": { "type": "string" }, "threadId": { "type": "string" } }, "required": [ "threadId", "content" ], "type": "object" }, "title": "Codex Reply", }); assert_eq!(expected_tool_json, tool_json); } }