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>,