diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 8e0e2c39b4..c0ec0a877b 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -6089,6 +6089,16 @@ "boolean", "null" ] + }, + "mcp_app_message_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/v2/McpAppMessageApprovalMode" + }, + { + "type": "null" + } + ] } }, "type": "object" @@ -10866,6 +10876,13 @@ "title": "MarketplaceUpgradeResponse", "type": "object" }, + "McpAppMessageApprovalMode": { + "enum": [ + "auto", + "approve_in_thread" + ], + "type": "string" + }, "McpAuthStatus": { "enum": [ "unsupported", @@ -10990,6 +11007,62 @@ "title": "McpServerOauthLoginResponse", "type": "object" }, + "McpServerProvider": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "codex-apps" + ], + "title": "CodexAppsMcpServerProviderType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "CodexAppsMcpServerProvider", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "mcp" + ], + "title": "McpMcpServerProviderType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "McpMcpServerProvider", + "type": "object" + }, + { + "properties": { + "pluginId": { + "type": "string" + }, + "type": { + "enum": [ + "plugin-mcp" + ], + "title": "PluginMcpMcpServerProviderType", + "type": "string" + } + }, + "required": [ + "pluginId", + "type" + ], + "title": "PluginMcpMcpServerProvider", + "type": "object" + } + ] + }, "McpServerRefreshResponse": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "McpServerRefreshResponse", @@ -11012,6 +11085,9 @@ "name": { "type": "string" }, + "provider": { + "$ref": "#/definitions/v2/McpServerProvider" + }, "resourceTemplates": { "items": { "$ref": "#/definitions/v2/ResourceTemplate" @@ -11034,6 +11110,7 @@ "required": [ "authStatus", "name", + "provider", "resourceTemplates", "resources", "tools" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 67aaf9e389..f7cc654967 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -668,6 +668,16 @@ "boolean", "null" ] + }, + "mcp_app_message_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/McpAppMessageApprovalMode" + }, + { + "type": "null" + } + ] } }, "type": "object" @@ -7477,6 +7487,13 @@ "title": "MarketplaceUpgradeResponse", "type": "object" }, + "McpAppMessageApprovalMode": { + "enum": [ + "auto", + "approve_in_thread" + ], + "type": "string" + }, "McpAuthStatus": { "enum": [ "unsupported", @@ -7601,6 +7618,62 @@ "title": "McpServerOauthLoginResponse", "type": "object" }, + "McpServerProvider": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "codex-apps" + ], + "title": "CodexAppsMcpServerProviderType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "CodexAppsMcpServerProvider", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "mcp" + ], + "title": "McpMcpServerProviderType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "McpMcpServerProvider", + "type": "object" + }, + { + "properties": { + "pluginId": { + "type": "string" + }, + "type": { + "enum": [ + "plugin-mcp" + ], + "title": "PluginMcpMcpServerProviderType", + "type": "string" + } + }, + "required": [ + "pluginId", + "type" + ], + "title": "PluginMcpMcpServerProvider", + "type": "object" + } + ] + }, "McpServerRefreshResponse": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "McpServerRefreshResponse", @@ -7623,6 +7696,9 @@ "name": { "type": "string" }, + "provider": { + "$ref": "#/definitions/McpServerProvider" + }, "resourceTemplates": { "items": { "$ref": "#/definitions/ResourceTemplate" @@ -7645,6 +7721,7 @@ "required": [ "authStatus", "name", + "provider", "resourceTemplates", "resources", "tools" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index c348e7d955..20395923a3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -89,6 +89,16 @@ "boolean", "null" ] + }, + "mcp_app_message_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/McpAppMessageApprovalMode" + }, + { + "type": "null" + } + ] } }, "type": "object" @@ -585,6 +595,13 @@ ], "type": "string" }, + "McpAppMessageApprovalMode": { + "enum": [ + "auto", + "approve_in_thread" + ], + "type": "string" + }, "ProfileV2": { "additionalProperties": true, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json index fc181c2702..691195f366 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json @@ -10,6 +10,62 @@ ], "type": "string" }, + "McpServerProvider": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "codex-apps" + ], + "title": "CodexAppsMcpServerProviderType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "CodexAppsMcpServerProvider", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "mcp" + ], + "title": "McpMcpServerProviderType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "McpMcpServerProvider", + "type": "object" + }, + { + "properties": { + "pluginId": { + "type": "string" + }, + "type": { + "enum": [ + "plugin-mcp" + ], + "title": "PluginMcpMcpServerProviderType", + "type": "string" + } + }, + "required": [ + "pluginId", + "type" + ], + "title": "PluginMcpMcpServerProvider", + "type": "object" + } + ] + }, "McpServerStatus": { "properties": { "authStatus": { @@ -18,6 +74,9 @@ "name": { "type": "string" }, + "provider": { + "$ref": "#/definitions/McpServerProvider" + }, "resourceTemplates": { "items": { "$ref": "#/definitions/ResourceTemplate" @@ -40,6 +99,7 @@ "required": [ "authStatus", "name", + "provider", "resourceTemplates", "resources", "tools" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppToolsConfig.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppToolsConfig.ts index 16a1c22c6d..6dc576132f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppToolsConfig.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppToolsConfig.ts @@ -2,5 +2,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AppToolApproval } from "./AppToolApproval"; +import type { McpAppMessageApprovalMode } from "./McpAppMessageApprovalMode"; -export type AppToolsConfig = { [key in string]?: { enabled: boolean | null, approval_mode: AppToolApproval | null, } }; +export type AppToolsConfig = { [key in string]?: { enabled: boolean | null, approval_mode: AppToolApproval | null, mcp_app_message_approval_mode: McpAppMessageApprovalMode | null, } }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpAppMessageApprovalMode.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpAppMessageApprovalMode.ts new file mode 100644 index 0000000000..ddfe238346 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpAppMessageApprovalMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpAppMessageApprovalMode = "auto" | "approve_in_thread"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerProvider.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerProvider.ts new file mode 100644 index 0000000000..f7d07153a0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerProvider.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type McpServerProvider = { "type": "codex-apps" } | { "type": "mcp" } | { "type": "plugin-mcp", pluginId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts index 430494e268..790b7368d4 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts @@ -5,5 +5,6 @@ import type { Resource } from "../Resource"; import type { ResourceTemplate } from "../ResourceTemplate"; import type { Tool } from "../Tool"; import type { McpAuthStatus } from "./McpAuthStatus"; +import type { McpServerProvider } from "./McpServerProvider"; -export type McpServerStatus = { name: string, tools: { [key in string]?: Tool }, resources: Array, resourceTemplates: Array, authStatus: McpAuthStatus, }; +export type McpServerStatus = { name: string, provider: McpServerProvider, tools: { [key in string]?: Tool }, resources: Array, resourceTemplates: Array, authStatus: McpAuthStatus, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 547f0f1018..69aea26203 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -191,6 +191,7 @@ export type { MarketplaceRemoveResponse } from "./MarketplaceRemoveResponse"; export type { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo"; export type { MarketplaceUpgradeParams } from "./MarketplaceUpgradeParams"; export type { MarketplaceUpgradeResponse } from "./MarketplaceUpgradeResponse"; +export type { McpAppMessageApprovalMode } from "./McpAppMessageApprovalMode"; export type { McpAuthStatus } from "./McpAuthStatus"; export type { McpElicitationArrayType } from "./McpElicitationArrayType"; export type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema"; @@ -223,6 +224,7 @@ export type { McpServerMigration } from "./McpServerMigration"; export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthLoginCompletedNotification"; export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams"; export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse"; +export type { McpServerProvider } from "./McpServerProvider"; export type { McpServerRefreshResponse } from "./McpServerRefreshResponse"; export type { McpServerStartupState } from "./McpServerStartupState"; export type { McpServerStatus } from "./McpServerStatus"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index 1c1ef35d62..587c6c1292 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -166,6 +166,14 @@ pub enum AppToolApproval { Approve, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub enum McpAppMessageApprovalMode { + Auto, + ApproveInThread, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] @@ -184,6 +192,7 @@ pub struct AppsDefaultConfig { pub struct AppToolConfig { pub enabled: Option, pub approval_mode: Option, + pub mcp_app_message_approval_mode: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs index 9fd9384076..48e7043622 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs @@ -46,11 +46,28 @@ pub enum McpServerStatusDetail { ToolsAndAuthOnly, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum McpServerProvider { + #[serde(rename = "codex-apps")] + #[ts(rename = "codex-apps")] + CodexApps, + #[serde(rename = "mcp")] + #[ts(rename = "mcp")] + Mcp, + #[serde(rename = "plugin-mcp", rename_all = "camelCase")] + #[ts(rename = "plugin-mcp", rename_all = "camelCase")] + PluginMcp { plugin_id: String }, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct McpServerStatus { pub name: String, + pub provider: McpServerProvider, pub tools: std::collections::HashMap, pub resources: Vec, pub resource_templates: Vec, diff --git a/codex-rs/app-server/src/config_manager_service_tests.rs b/codex-rs/app-server/src/config_manager_service_tests.rs index e9b0b3c769..c91ef6b11f 100644 --- a/codex-rs/app-server/src/config_manager_service_tests.rs +++ b/codex-rs/app-server/src/config_manager_service_tests.rs @@ -234,6 +234,52 @@ async fn write_value_supports_custom_mcp_server_default_tool_approval_mode() -> Ok(()) } +#[tokio::test] +async fn write_value_supports_custom_mcp_server_message_approval_mode() -> Result<()> { + let tmp = tempdir().expect("tempdir"); + std::fs::write( + tmp.path().join(CONFIG_TOML_FILE), + "[mcp_servers.docs]\ncommand = \"docs-server\"\n", + )?; + + let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf()); + service + .write_value(ConfigValueWriteParams { + file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), + key_path: "mcp_servers.docs.tools".to_string(), + value: serde_json::json!({ + "search": { + "mcp_app_message_approval_mode": "approve_in_thread", + }, + }), + merge_strategy: MergeStrategy::Upsert, + expected_version: None, + }) + .await + .expect("write mcp server tool mcp_app_message_approval_mode succeeds"); + + let read = service + .read(ConfigReadParams { + include_layers: false, + cwd: None, + }) + .await + .expect("config read succeeds"); + + assert_eq!( + read.config + .additional + .get("mcp_servers") + .and_then(|servers| servers.get("docs")) + .and_then(|server| server.get("tools")) + .and_then(|tools| tools.get("search")) + .and_then(|tool| tool.get("mcp_app_message_approval_mode")), + Some(&serde_json::json!("approve_in_thread")) + ); + + Ok(()) +} + #[tokio::test] async fn read_includes_origins_and_layers() { let tmp = tempdir().expect("tempdir"); diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index be6d559866..96dcaf492c 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -92,6 +92,7 @@ use codex_app_server_protocol::McpResourceReadResponse; use codex_app_server_protocol::McpServerOauthLoginCompletedNotification; use codex_app_server_protocol::McpServerOauthLoginParams; use codex_app_server_protocol::McpServerOauthLoginResponse; +use codex_app_server_protocol::McpServerProvider; use codex_app_server_protocol::McpServerRefreshResponse; use codex_app_server_protocol::McpServerStatus; use codex_app_server_protocol::McpServerStatusDetail; @@ -320,6 +321,7 @@ use codex_login::complete_device_code_login; use codex_login::login_with_api_key; use codex_login::request_device_code; use codex_login::run_login_server; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::McpRuntimeEnvironment; use codex_mcp::McpServerStatusSnapshot; use codex_mcp::McpSnapshotDetail; diff --git a/codex-rs/app-server/src/request_processors/mcp_processor.rs b/codex-rs/app-server/src/request_processors/mcp_processor.rs index 22d43d87a2..0208bb6116 100644 --- a/codex-rs/app-server/src/request_processors/mcp_processor.rs +++ b/codex-rs/app-server/src/request_processors/mcp_processor.rs @@ -318,6 +318,29 @@ impl McpRequestProcessor { resource_templates, auth_statuses, } = snapshot; + let mut provider_by_server: HashMap = config + .mcp_servers + .keys() + .cloned() + .map(|name| (name, McpServerProvider::Mcp)) + .collect(); + for plugin in &mcp_config.plugin_capability_summaries { + for name in &plugin.mcp_server_names { + if effective_servers.contains_key(name) { + provider_by_server.entry(name.clone()).or_insert_with(|| { + McpServerProvider::PluginMcp { + plugin_id: plugin.config_name.clone(), + } + }); + } + } + } + if effective_servers.contains_key(CODEX_APPS_MCP_SERVER_NAME) { + provider_by_server.insert( + CODEX_APPS_MCP_SERVER_NAME.to_string(), + McpServerProvider::CodexApps, + ); + } let mut server_names: Vec = config .mcp_servers @@ -357,6 +380,10 @@ impl McpRequestProcessor { .iter() .map(|name| McpServerStatus { name: name.clone(), + provider: provider_by_server + .get(name) + .cloned() + .unwrap_or(McpServerProvider::Mcp), tools: tools_by_server.get(name).cloned().unwrap_or_default(), resources: resources.get(name).cloned().unwrap_or_default(), resource_templates: resource_templates.get(name).cloned().unwrap_or_default(), diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index e88c736db0..46a24fd5a8 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -88,6 +88,7 @@ pub use marketplace_edit::remove_user_marketplace_config; pub use mcp_edit::ConfigEditsBuilder; pub use mcp_edit::load_global_mcp_servers; pub use mcp_types::AppToolApproval; +pub use mcp_types::McpAppMessageApprovalMode; pub use mcp_types::McpServerConfig; pub use mcp_types::McpServerDisabledReason; pub use mcp_types::McpServerEnvVar; diff --git a/codex-rs/config/src/mcp_edit.rs b/codex-rs/config/src/mcp_edit.rs index f5881a1257..b54cc916c5 100644 --- a/codex-rs/config/src/mcp_edit.rs +++ b/codex-rs/config/src/mcp_edit.rs @@ -13,6 +13,7 @@ use toml_edit::value; use crate::AppToolApproval; use crate::CONFIG_TOML_FILE; +use crate::McpAppMessageApprovalMode; use crate::McpServerConfig; use crate::McpServerEnvVar; use crate::McpServerTransportConfig; @@ -232,6 +233,12 @@ fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem { AppToolApproval::Approve => "approve", }); } + if let Some(approval_mode) = tool_config.mcp_app_message_approval_mode { + tool_entry["mcp_app_message_approval_mode"] = value(match approval_mode { + McpAppMessageApprovalMode::Auto => "auto", + McpAppMessageApprovalMode::ApproveInThread => "approve_in_thread", + }); + } tools.insert(name, TomlItem::Table(tool_entry)); } entry.insert("tools", TomlItem::Table(tools)); diff --git a/codex-rs/config/src/mcp_edit_tests.rs b/codex-rs/config/src/mcp_edit_tests.rs index cfcd73c3e5..f40474b344 100644 --- a/codex-rs/config/src/mcp_edit_tests.rs +++ b/codex-rs/config/src/mcp_edit_tests.rs @@ -39,12 +39,16 @@ async fn replace_mcp_servers_serializes_per_tool_approval_overrides() -> anyhow: "search".to_string(), McpServerToolConfig { approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: Some( + McpAppMessageApprovalMode::ApproveInThread, + ), }, ), ( "read".to_string(), McpServerToolConfig { approval_mode: Some(AppToolApproval::Prompt), + mcp_app_message_approval_mode: None, }, ), ]), @@ -72,6 +76,7 @@ approval_mode = "prompt" [mcp_servers.docs.tools.search] approval_mode = "approve" +mcp_app_message_approval_mode = "approve_in_thread" "# ); diff --git a/codex-rs/config/src/mcp_types.rs b/codex-rs/config/src/mcp_types.rs index d642d9fc57..60a6ecc7d7 100644 --- a/codex-rs/config/src/mcp_types.rs +++ b/codex-rs/config/src/mcp_types.rs @@ -22,6 +22,13 @@ pub enum AppToolApproval { Approve, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum McpAppMessageApprovalMode { + Auto, + ApproveInThread, +} + /// Human-readable reason a configured MCP server was disabled after requirements /// were applied. /// @@ -54,6 +61,10 @@ pub struct McpServerToolConfig { /// Approval mode for this tool. #[serde(default, skip_serializing_if = "Option::is_none")] pub approval_mode: Option, + + /// Approval mode for MCP app messages sent by this tool. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mcp_app_message_approval_mode: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] diff --git a/codex-rs/config/src/mcp_types_tests.rs b/codex-rs/config/src/mcp_types_tests.rs index 50f705d0f8..6aa1f6ab25 100644 --- a/codex-rs/config/src/mcp_types_tests.rs +++ b/codex-rs/config/src/mcp_types_tests.rs @@ -320,6 +320,7 @@ fn deserialize_server_config_with_default_tool_approval_mode() { [tools.search] approval_mode = "prompt" + mcp_app_message_approval_mode = "approve_in_thread" "#, ) .expect("should deserialize default tool approval mode"); @@ -332,11 +333,13 @@ fn deserialize_server_config_with_default_tool_approval_mode() { cfg.tools.get("search"), Some(&McpServerToolConfig { approval_mode: Some(AppToolApproval::Prompt), + mcp_app_message_approval_mode: Some(McpAppMessageApprovalMode::ApproveInThread), }) ); let serialized = toml::to_string(&cfg).expect("should serialize MCP config"); assert!(serialized.contains("default_tools_approval_mode = \"approve\"")); + assert!(serialized.contains("mcp_app_message_approval_mode = \"approve_in_thread\"")); let round_tripped: McpServerConfig = toml::from_str(&serialized).expect("should deserialize serialized MCP config"); diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 6cb9abc507..3621a2b323 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -4,6 +4,7 @@ // definitions that do not contain business logic. pub use crate::mcp_types::AppToolApproval; +pub use crate::mcp_types::McpAppMessageApprovalMode; pub use crate::mcp_types::McpServerConfig; pub use crate::mcp_types::McpServerDisabledReason; pub use crate::mcp_types::McpServerEnvVar; @@ -399,6 +400,10 @@ pub struct AppToolConfig { /// Approval mode for this tool. #[serde(default, skip_serializing_if = "Option::is_none")] pub approval_mode: Option, + + /// Approval mode for MCP app messages sent by resources returned from this tool. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mcp_app_message_approval_mode: Option, } /// Tool settings for a single app. diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index 8abff7700b..a102ebc41e 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -325,6 +325,7 @@ approval_mode = "approve" server.tools.get("search"), Some(&McpServerToolConfig { approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: None, }) ); } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index c40b7654ab..2286f68af4 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -161,6 +161,14 @@ "enabled": { "description": "Whether this tool is enabled. `Some(true)` explicitly allows this tool.", "type": "boolean" + }, + "mcp_app_message_approval_mode": { + "allOf": [ + { + "$ref": "#/definitions/McpAppMessageApprovalMode" + } + ], + "description": "Approval mode for MCP app messages sent by resources returned from this tool." } }, "type": "object" @@ -1148,6 +1156,13 @@ }, "type": "object" }, + "McpAppMessageApprovalMode": { + "enum": [ + "auto", + "approve_in_thread" + ], + "type": "string" + }, "McpServerEnvVar": { "anyOf": [ { @@ -1181,6 +1196,14 @@ } ], "description": "Approval mode for this tool." + }, + "mcp_app_message_approval_mode": { + "allOf": [ + { + "$ref": "#/definitions/McpAppMessageApprovalMode" + } + ], + "description": "Approval mode for MCP app messages sent by this tool." } }, "type": "object" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index c35a3767cb..86bc445a72 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -3787,6 +3787,7 @@ approval_mode = "approve" server.tools.get("search"), Some(&McpServerToolConfig { approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: None, }) ); } @@ -3830,6 +3831,7 @@ approval_mode = "approve" tool, &McpServerToolConfig { approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: None, } ); } diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index 1362103364..7e3d5a29d7 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -197,6 +197,7 @@ pub fn model_availability_nux_count_edits(shown_count: &HashMap) -> // TODO(jif) move to a dedicated file mod document_helpers { use codex_config::types::AppToolApproval; + use codex_config::types::McpAppMessageApprovalMode; use codex_config::types::McpServerConfig; use codex_config::types::McpServerEnvVar; use codex_config::types::McpServerToolConfig; @@ -361,6 +362,12 @@ mod document_helpers { AppToolApproval::Approve => "approve", }); } + if let Some(approval_mode) = config.mcp_app_message_approval_mode { + entry["mcp_app_message_approval_mode"] = value(match approval_mode { + McpAppMessageApprovalMode::Auto => "auto", + McpAppMessageApprovalMode::ApproveInThread => "approve_in_thread", + }); + } TomlItem::Table(entry) } diff --git a/codex-rs/core/src/config/edit_tests.rs b/codex-rs/core/src/config/edit_tests.rs index 45af723b8f..ebd26c6b70 100644 --- a/codex-rs/core/src/config/edit_tests.rs +++ b/codex-rs/core/src/config/edit_tests.rs @@ -1,5 +1,6 @@ use super::*; use codex_config::types::AppToolApproval; +use codex_config::types::McpAppMessageApprovalMode; use codex_config::types::McpServerToolConfig; use codex_config::types::McpServerTransportConfig; use codex_config::types::SessionPickerViewMode; @@ -1011,6 +1012,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() { "search".to_string(), McpServerToolConfig { approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: Some(McpAppMessageApprovalMode::ApproveInThread), }, )]), }, @@ -1031,6 +1033,7 @@ default_tools_approval_mode = \"prompt\" [mcp_servers.docs.tools.search] approval_mode = \"approve\" +mcp_app_message_approval_mode = \"approve_in_thread\" "; assert_eq!(raw, expected); } diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index 513f677e9b..3140226f2e 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -859,6 +859,7 @@ fn app_tool_policy_per_tool_enabled_true_overrides_app_level_disable_flags() { AppToolConfig { enabled: Some(true), approval_mode: None, + mcp_app_message_approval_mode: None, }, )]), }), @@ -1009,6 +1010,7 @@ fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() { AppToolConfig { enabled: Some(true), approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: None, }, )]), }), diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 3d81f05c72..2c288bfe40 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1527,6 +1527,7 @@ async fn persist_codex_app_tool_approval_writes_tool_override() { AppToolConfig { enabled: None, approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: None, }, )]), }), @@ -1567,6 +1568,7 @@ async fn persist_custom_mcp_tool_approval_writes_tool_override() { tool, &McpServerToolConfig { approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: None, } ); assert!(contents.contains("[mcp_servers.docs.tools.search]")); @@ -1736,6 +1738,7 @@ async fn maybe_persist_mcp_tool_approval_reloads_session_config() { &AppToolConfig { enabled: None, approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: None, } ); assert_eq!(mcp_tool_approval_is_remembered(&session, &key).await, true); @@ -1778,6 +1781,7 @@ async fn maybe_persist_mcp_tool_approval_reloads_session_config_for_custom_serve tool, &McpServerToolConfig { approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: None, } ); assert_eq!(mcp_tool_approval_is_remembered(&session, &key).await, true); @@ -1827,6 +1831,7 @@ enabled = true tool, &McpServerToolConfig { approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: None, } ); assert!(contents.contains(r#"[plugins."sample@test".mcp_servers.sample.tools.search]"#)); @@ -1883,6 +1888,7 @@ async fn maybe_persist_mcp_tool_approval_writes_project_config_for_project_serve tool, &McpServerToolConfig { approval_mode: Some(AppToolApproval::Approve), + mcp_app_message_approval_mode: None, } ); assert!(contents.contains("[mcp_servers.docs.tools.search]")); diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index 3233db8276..848ebbce68 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -12,6 +12,8 @@ use codex_app_server_protocol::MarketplaceRemoveParams; use codex_app_server_protocol::MarketplaceRemoveResponse; use codex_app_server_protocol::MarketplaceUpgradeParams; use codex_app_server_protocol::MarketplaceUpgradeResponse; +#[cfg(test)] +use codex_app_server_protocol::McpServerProvider; use codex_app_server_protocol::RequestId; @@ -1044,6 +1046,7 @@ mod tests { let statuses = vec![ McpServerStatus { name: "docs".to_string(), + provider: McpServerProvider::Mcp, tools: HashMap::from([( "list".to_string(), Tool { @@ -1063,6 +1066,7 @@ mod tests { }, McpServerStatus { name: "disabled".to_string(), + provider: McpServerProvider::Mcp, tools: HashMap::new(), resources: Vec::new(), resource_templates: Vec::new(), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 0e59964c3b..f0c8bd84f8 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -37,6 +37,7 @@ use codex_app_server_protocol::FileChangeRequestApprovalParams; use codex_app_server_protocol::FileUpdateChange; use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::McpServerProvider; use codex_app_server_protocol::McpServerStartupState; use codex_app_server_protocol::McpServerStatusUpdatedNotification; use codex_app_server_protocol::NetworkApprovalContext as AppServerNetworkApprovalContext; @@ -111,6 +112,7 @@ async fn handle_mcp_inventory_result_clears_committed_loading_cell() { app.handle_mcp_inventory_result( Ok(vec![McpServerStatus { name: "docs".to_string(), + provider: McpServerProvider::Mcp, tools: HashMap::new(), resources: Vec::new(), resource_templates: Vec::new(), diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index a9dd59e794..eabd7ded28 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -48,6 +48,8 @@ use crate::wrapping::adaptive_wrap_lines; use base64::Engine; use codex_app_server_protocol::AskForApproval; use codex_app_server_protocol::McpAuthStatus; +#[cfg(test)] +use codex_app_server_protocol::McpServerProvider; use codex_app_server_protocol::McpServerStatus; use codex_app_server_protocol::McpServerStatusDetail; use codex_app_server_protocol::PermissionProfile as AppServerPermissionProfile; @@ -4198,6 +4200,7 @@ mod tests { let statuses = vec![McpServerStatus { name: "plugin_docs".to_string(), + provider: McpServerProvider::Mcp, tools: HashMap::from([( "lookup".to_string(), Tool { @@ -4239,6 +4242,7 @@ mod tests { let statuses = vec![McpServerStatus { name: "plugin_docs".to_string(), + provider: McpServerProvider::Mcp, tools: HashMap::from([( "lookup".to_string(), Tool {