From 0cec0770e288c427a36ee1b790476ab65b017ff4 Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 27 Aug 2025 09:59:03 -0700 Subject: [PATCH] [mcp-server] Add GetConfig endpoint (#2725) ## Summary Adds a GetConfig request to the MCP Protocol, so MCP clients can evaluate the resolved config.toml settings which the harness is using. ## Testing - [x] Added an end to end test of the endpoint --- .../mcp-server/src/codex_message_processor.rs | 62 ++++++++++++++ .../mcp-server/tests/common/mcp_process.rs | 5 ++ codex-rs/mcp-server/tests/suite/config.rs | 80 +++++++++++++++++++ codex-rs/mcp-server/tests/suite/mod.rs | 1 + codex-rs/protocol/src/config_types.rs | 12 +++ codex-rs/protocol/src/mcp_protocol.rs | 25 ++++++ 6 files changed, 185 insertions(+) create mode 100644 codex-rs/mcp-server/tests/suite/config.rs diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index 97a3602d..1623e766 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -8,6 +8,8 @@ use codex_core::ConversationManager; use codex_core::NewConversation; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config::ConfigToml; +use codex_core::config::load_config_as_toml; use codex_core::git_info::git_diff_to_remote; use codex_core::protocol::ApplyPatchApprovalRequestEvent; use codex_core::protocol::Event; @@ -46,6 +48,7 @@ use codex_protocol::mcp_protocol::ConversationId; use codex_protocol::mcp_protocol::EXEC_COMMAND_APPROVAL_METHOD; use codex_protocol::mcp_protocol::ExecCommandApprovalParams; use codex_protocol::mcp_protocol::ExecCommandApprovalResponse; +use codex_protocol::mcp_protocol::GetConfigTomlResponse; use codex_protocol::mcp_protocol::InputItem as WireInputItem; use codex_protocol::mcp_protocol::InterruptConversationParams; use codex_protocol::mcp_protocol::InterruptConversationResponse; @@ -146,6 +149,9 @@ impl CodexMessageProcessor { ClientRequest::GetAuthStatus { request_id, params } => { self.get_auth_status(request_id, params).await; } + ClientRequest::GetConfigToml { request_id } => { + self.get_config_toml(request_id).await; + } } } @@ -354,6 +360,62 @@ impl CodexMessageProcessor { self.outgoing.send_response(request_id, response).await; } + async fn get_config_toml(&self, request_id: RequestId) { + let toml_value = match load_config_as_toml(&self.config.codex_home) { + Ok(val) => val, + Err(err) => { + let error = JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to load config.toml: {err}"), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + let cfg: ConfigToml = match toml_value.try_into() { + Ok(cfg) => cfg, + Err(err) => { + let error = JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to parse config.toml: {err}"), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + let profiles: HashMap = cfg + .profiles + .into_iter() + .map(|(k, v)| { + ( + k, + // Define this explicitly here to avoid the need to + // implement `From` + // for the `ConfigProfile` type and introduce a dependency on codex_core + codex_protocol::config_types::ConfigProfile { + model: v.model, + approval_policy: v.approval_policy, + model_reasoning_effort: v.model_reasoning_effort, + }, + ) + }) + .collect(); + + let response = GetConfigTomlResponse { + approval_policy: cfg.approval_policy, + sandbox_mode: cfg.sandbox_mode, + model_reasoning_effort: cfg.model_reasoning_effort, + profile: cfg.profile, + profiles: Some(profiles), + }; + + self.outgoing.send_response(request_id, response).await; + } + async fn process_new_conversation(&self, request_id: RequestId, params: NewConversationParams) { let config = match derive_config_from_params(params, self.codex_linux_sandbox_exe.clone()) { Ok(config) => config, diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs index bcc37843..5788163c 100644 --- a/codex-rs/mcp-server/tests/common/mcp_process.rs +++ b/codex-rs/mcp-server/tests/common/mcp_process.rs @@ -228,6 +228,11 @@ impl McpProcess { self.send_request("getAuthStatus", params).await } + /// Send a `getConfigToml` JSON-RPC request. + pub async fn send_get_config_toml_request(&mut self) -> anyhow::Result { + self.send_request("getConfigToml", None).await + } + /// Send a `loginChatGpt` JSON-RPC request. pub async fn send_login_chat_gpt_request(&mut self) -> anyhow::Result { self.send_request("loginChatGpt", None).await diff --git a/codex-rs/mcp-server/tests/suite/config.rs b/codex-rs/mcp-server/tests/suite/config.rs new file mode 100644 index 00000000..cec41b25 --- /dev/null +++ b/codex-rs/mcp-server/tests/suite/config.rs @@ -0,0 +1,80 @@ +use std::collections::HashMap; +use std::path::Path; + +use codex_core::protocol::AskForApproval; +use codex_protocol::config_types::ConfigProfile; +use codex_protocol::config_types::ReasoningEffort; +use codex_protocol::config_types::SandboxMode; +use codex_protocol::mcp_protocol::GetConfigTomlResponse; +use mcp_test_support::McpProcess; +use mcp_test_support::to_response; +use mcp_types::JSONRPCResponse; +use mcp_types::RequestId; +use pretty_assertions::assert_eq; +use tempfile::TempDir; +use tokio::time::timeout; + +const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); + +fn create_config_toml(codex_home: &Path) -> std::io::Result<()> { + let config_toml = codex_home.join("config.toml"); + std::fs::write( + config_toml, + r#" +approval_policy = "on-request" +sandbox_mode = "workspace-write" +model_reasoning_effort = "high" +profile = "test" + +[profiles.test] +model = "gpt-4o" +approval_policy = "on-request" +model_reasoning_effort = "high" +model_reasoning_summary = "detailed" +"#, + ) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_config_toml_returns_subset() { + let codex_home = TempDir::new().unwrap_or_else(|e| panic!("create tempdir: {e}")); + create_config_toml(codex_home.path()).expect("write config.toml"); + + let mut mcp = McpProcess::new(codex_home.path()) + .await + .expect("spawn mcp process"); + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()) + .await + .expect("init timeout") + .expect("init failed"); + + let request_id = mcp + .send_get_config_toml_request() + .await + .expect("send getConfigToml"); + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await + .expect("getConfigToml timeout") + .expect("getConfigToml response"); + + let config: GetConfigTomlResponse = to_response(resp).expect("deserialize config"); + let expected = GetConfigTomlResponse { + approval_policy: Some(AskForApproval::OnRequest), + sandbox_mode: Some(SandboxMode::WorkspaceWrite), + model_reasoning_effort: Some(ReasoningEffort::High), + profile: Some("test".to_string()), + profiles: Some(HashMap::from([( + "test".into(), + ConfigProfile { + model: Some("gpt-4o".into()), + approval_policy: Some(AskForApproval::OnRequest), + model_reasoning_effort: Some(ReasoningEffort::High), + }, + )])), + }; + + assert_eq!(expected, config); +} diff --git a/codex-rs/mcp-server/tests/suite/mod.rs b/codex-rs/mcp-server/tests/suite/mod.rs index 7888a732..fb6d1cef 100644 --- a/codex-rs/mcp-server/tests/suite/mod.rs +++ b/codex-rs/mcp-server/tests/suite/mod.rs @@ -2,6 +2,7 @@ mod auth; mod codex_message_processor_flow; mod codex_tool; +mod config; mod create_conversation; mod interrupt; mod login; diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 3e543358..46fd9c13 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -4,6 +4,8 @@ use strum_macros::Display; use strum_macros::EnumIter; use ts_rs::TS; +use crate::protocol::AskForApproval; + /// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning #[derive( Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, TS, EnumIter, @@ -47,3 +49,13 @@ pub enum SandboxMode { #[serde(rename = "danger-full-access")] DangerFullAccess, } + +/// Collection of common configuration options that a user can define as a unit +/// in `config.toml`. Currently only a subset of the fields are supported. +#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct ConfigProfile { + pub model: Option, + pub approval_policy: Option, + pub model_reasoning_effort: Option, +} diff --git a/codex-rs/protocol/src/mcp_protocol.rs b/codex-rs/protocol/src/mcp_protocol.rs index 7fb087cf..9a45c167 100644 --- a/codex-rs/protocol/src/mcp_protocol.rs +++ b/codex-rs/protocol/src/mcp_protocol.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::fmt::Display; use std::path::PathBuf; +use crate::config_types::ConfigProfile; use crate::config_types::ReasoningEffort; use crate::config_types::ReasoningSummary; use crate::config_types::SandboxMode; @@ -101,6 +102,10 @@ pub enum ClientRequest { request_id: RequestId, params: GetAuthStatusParams, }, + GetConfigToml { + #[serde(rename = "id")] + request_id: RequestId, + }, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)] @@ -223,6 +228,26 @@ pub struct GetAuthStatusResponse { pub auth_token: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct GetConfigTomlResponse { + /// Approvals + #[serde(skip_serializing_if = "Option::is_none")] + pub approval_policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_mode: Option, + + /// Relevant model configuration + #[serde(skip_serializing_if = "Option::is_none")] + pub model_reasoning_effort: Option, + + /// Profiles + #[serde(skip_serializing_if = "Option::is_none")] + pub profile: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub profiles: Option>, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct SendUserMessageParams {