//! Configuration object accepted by the `codex` MCP tool-call. use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; use mcp_types::Tool; use mcp_types::ToolInputSchema; use schemars::JsonSchema; use schemars::r#gen::SchemaSettings; use serde::Deserialize; use std::collections::HashMap; use std::path::PathBuf; use crate::json_to_toml::json_to_toml; /// Client-supplied configuration for a `codex` tool-call. #[derive(Debug, Clone, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub(crate) struct CodexToolCallParam { /// The *initial user prompt* to start the Codex conversation. pub prompt: String, /// Optional override for the model name (e.g. "o3", "o4-mini"). #[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`, `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>, } /// Custom enum mirroring [`AskForApproval`], but has an extra dependency on /// [`JsonSchema`]. #[derive(Debug, Clone, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub(crate) enum CodexToolCallApprovalPolicy { Untrusted, OnFailure, Never, } impl From for AskForApproval { fn from(value: CodexToolCallApprovalPolicy) -> Self { match value { CodexToolCallApprovalPolicy::Untrusted => AskForApproval::UnlessTrusted, CodexToolCallApprovalPolicy::OnFailure => AskForApproval::OnFailure, CodexToolCallApprovalPolicy::Never => AskForApproval::Never, } } } /// Custom enum mirroring [`SandboxMode`] from config_types.rs, but with /// `JsonSchema` support. #[derive(Debug, Clone, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub(crate) 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::(); #[expect(clippy::expect_used)] let schema_value = serde_json::to_value(&schema).expect("Codex tool schema should serialise to JSON"); let tool_input_schema = serde_json::from_value::(schema_value).unwrap_or_else(|e| { panic!("failed to create Tool from schema: {e}"); }); Tool { name: "codex".to_string(), input_schema: tool_input_schema, description: Some( "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.".to_string(), ), annotations: None, } } impl CodexToolCallParam { /// Returns the initial user prompt to start the Codex conversation and the /// effective Config object generated from the supplied parameters. pub fn into_config( self, codex_linux_sandbox_exe: Option, ) -> std::io::Result<(String, codex_core::config::Config)> { let Self { prompt, model, profile, cwd, approval_policy, sandbox, config: cli_overrides, } = self; // Build the `ConfigOverrides` recognised by codex-core. let overrides = codex_core::config::ConfigOverrides { model, config_profile: profile, cwd: cwd.map(PathBuf::from), approval_policy: approval_policy.map(Into::into), sandbox_mode: sandbox.map(Into::into), model_provider: None, codex_linux_sandbox_exe, }; let cli_overrides = cli_overrides .unwrap_or_default() .into_iter() .map(|(k, v)| (k, json_to_toml(v))) .collect(); let cfg = codex_core::config::Config::load_with_cli_overrides(cli_overrides, overrides)?; Ok((prompt, cfg)) } } #[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(); #[expect(clippy::expect_used)] let tool_json = serde_json::to_value(&tool).expect("tool serializes"); let expected_tool_json = serde_json::json!({ "name": "codex", "description": "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.", "inputSchema": { "type": "object", "properties": { "approval-policy": { "description": "Approval policy for shell commands generated by the model: `untrusted`, `on-failure`, `never`.", "enum": [ "untrusted", "on-failure", "never" ], "type": "string" }, "sandbox": { "description": "Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.", "enum": [ "read-only", "workspace-write", "danger-full-access" ], "type": "string" }, "config": { "description": "Individual config settings that will override what is in CODEX_HOME/config.toml.", "additionalProperties": true, "type": "object" }, "cwd": { "description": "Working directory for the session. If relative, it is resolved against the server process's current working directory.", "type": "string" }, "model": { "description": "Optional override for the model name (e.g. \"o3\", \"o4-mini\").", "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" }, }, "required": [ "prompt" ] } }); assert_eq!(expected_tool_json, tool_json); } }