From cbbd48ab9c2f0f45e625fea211cdc7e18df77f6f Mon Sep 17 00:00:00 2001 From: shijie-openai Date: Fri, 21 Nov 2025 14:09:59 -0800 Subject: [PATCH 1/2] Chore: read mcp servers config via app server --- .../src/protocol/common.rs | 4 ++ .../app-server-protocol/src/protocol/v2.rs | 49 ++++++++++++++ .../app-server/src/codex_message_processor.rs | 64 +++++++++++++++++++ 3 files changed, 117 insertions(+) diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index fecdc5b71b..8ab53d70b5 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -242,6 +242,10 @@ client_request_definitions! { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: v1::GetUserSavedConfigResponse, }, + GetMcpServersConfig { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + response: v2::GetMcpServersConfigResponse, + }, SetDefaultModel { params: v1::SetDefaultModelParams, response: v1::SetDefaultModelResponse, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 13b7b8888c..0ee1ee9e0d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -210,6 +210,55 @@ impl From for SandboxPolicy { } } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GetMcpServersConfigResponse { + pub servers: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", tag = "type")] +#[ts(export_to = "v2/")] +pub enum McpServerTransportConfig { + Stdio { + command: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + args: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + env: Option>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + env_vars: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + cwd: Option, + }, + StreamableHttp { + url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + bearer_token_env_var: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + http_headers: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + env_http_headers: Option>, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerConfig { + pub transport: McpServerTransportConfig, + pub enabled: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub startup_timeout_sec: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_timeout_sec: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled_tools: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub disabled_tools: Option>, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 5815e57850..7a86a5fd4e 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -36,6 +36,7 @@ use codex_app_server_protocol::GetAuthStatusParams; use codex_app_server_protocol::GetAuthStatusResponse; use codex_app_server_protocol::GetConversationSummaryParams; use codex_app_server_protocol::GetConversationSummaryResponse; +use codex_app_server_protocol::GetMcpServersConfigResponse; use codex_app_server_protocol::GetUserAgentResponse; use codex_app_server_protocol::GetUserSavedConfigResponse; use codex_app_server_protocol::GitDiffToRemoteResponse; @@ -52,6 +53,8 @@ use codex_app_server_protocol::LoginChatGptCompleteNotification; use codex_app_server_protocol::LoginChatGptResponse; use codex_app_server_protocol::LogoutAccountResponse; use codex_app_server_protocol::LogoutChatGptResponse; +use codex_app_server_protocol::McpServerConfig as WireMcpServerConfig; +use codex_app_server_protocol::McpServerTransportConfig as WireMcpServerTransportConfig; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; use codex_app_server_protocol::NewConversationParams; @@ -110,6 +113,8 @@ use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config::ConfigToml; use codex_core::config::edit::ConfigEditsBuilder; +use codex_core::config::types::McpServerConfig as CoreMcpServerConfig; +use codex_core::config::types::McpServerTransportConfig as CoreMcpServerTransportConfig; use codex_core::config_loader::load_config_as_toml; use codex_core::default_client::get_codex_user_agent; use codex_core::exec::ExecParams; @@ -451,6 +456,12 @@ impl CodexMessageProcessor { } => { self.get_user_saved_config(request_id).await; } + ClientRequest::GetMcpServersConfig { + request_id, + params: _, + } => { + self.get_mcp_servers_config(request_id).await; + } ClientRequest::SetDefaultModel { request_id, params } => { self.set_default_model(request_id, params).await; } @@ -1116,6 +1127,17 @@ impl CodexMessageProcessor { self.outgoing.send_response(request_id, response).await; } + async fn get_mcp_servers_config(&self, request_id: RequestId) { + let servers = self + .config + .mcp_servers + .iter() + .map(|(name, server)| (name.clone(), to_wire_mcp_server_config(server))) + .collect(); + let response = GetMcpServersConfigResponse { servers }; + self.outgoing.send_response(request_id, response).await; + } + async fn get_user_info(&self, request_id: RequestId) { // Read alleged user email from cached auth (best-effort; not verified). let alleged_user_email = self.auth_manager.auth().and_then(|a| a.get_account_email()); @@ -2847,6 +2869,48 @@ impl CodexMessageProcessor { } } +fn to_wire_mcp_server_config(config: &CoreMcpServerConfig) -> WireMcpServerConfig { + let transport = match &config.transport { + CoreMcpServerTransportConfig::Stdio { + command, + args, + env, + env_vars, + cwd, + } => WireMcpServerTransportConfig::Stdio { + command: command.clone(), + args: args.clone(), + env: env.clone(), + env_vars: env_vars.clone(), + cwd: cwd.clone(), + }, + CoreMcpServerTransportConfig::StreamableHttp { + url, + bearer_token_env_var, + http_headers, + env_http_headers, + } => WireMcpServerTransportConfig::StreamableHttp { + url: url.clone(), + bearer_token_env_var: bearer_token_env_var.clone(), + http_headers: http_headers.clone(), + env_http_headers: env_http_headers.clone(), + }, + }; + + WireMcpServerConfig { + transport, + enabled: config.enabled, + startup_timeout_sec: config.startup_timeout_sec.and_then(duration_to_seconds), + tool_timeout_sec: config.tool_timeout_sec.and_then(duration_to_seconds), + enabled_tools: config.enabled_tools.clone(), + disabled_tools: config.disabled_tools.clone(), + } +} + +fn duration_to_seconds(duration: Duration) -> Option { + i64::try_from(duration.as_secs()).ok() +} + async fn derive_config_from_params( overrides: ConfigOverrides, cli_overrides: Option>, From 96f34427f6c971fa8be67ff29d386aaffe1f888f Mon Sep 17 00:00:00 2001 From: shijie-openai Date: Mon, 24 Nov 2025 11:45:02 -0800 Subject: [PATCH 2/2] Update to include ts optional --- codex-rs/app-server-protocol/src/protocol/v2.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 0ee1ee9e0d..0fe4210b28 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -226,19 +226,24 @@ pub enum McpServerTransportConfig { #[serde(default, skip_serializing_if = "Vec::is_empty")] args: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] env: Option>, #[serde(default, skip_serializing_if = "Vec::is_empty")] env_vars: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] cwd: Option, }, StreamableHttp { url: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] bearer_token_env_var: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] http_headers: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] env_http_headers: Option>, }, } @@ -250,12 +255,16 @@ pub struct McpServerConfig { pub transport: McpServerTransportConfig, pub enabled: bool, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub startup_timeout_sec: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub tool_timeout_sec: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub enabled_tools: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub disabled_tools: Option>, }