Add server-level approval defaults for custom MCP servers (#17843)

## Summary
- Add `default_tools_approval_mode` support for custom MCP server
configs, matching the existing `codex_apps` behavior
- Apply approval precedence as per-tool override, then server default,
then `auto`
- Update config serialization, CLI display, schema generation, docs, and
tests

## Testing
- `cargo check -p codex-config`
- `cargo check -p codex-core`
- `just write-config-schema`
- `just fmt`
- `cargo test -p codex-config`
- Targeted `codex-core` tests for config parsing, config writes, and MCP
approval precedence
- `just fix -p codex-config -p codex-core`
This commit is contained in:
Matthew Zeng
2026-04-16 11:18:07 -07:00
committed by GitHub
parent 206dd13c32
commit 71174574ad
27 changed files with 231 additions and 12 deletions

View File

@@ -189,6 +189,13 @@ fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem {
if let Some(timeout) = config.tool_timeout_sec {
entry["tool_timeout_sec"] = value(timeout.as_secs_f64());
}
if let Some(approval_mode) = config.default_tools_approval_mode {
entry["default_tools_approval_mode"] = value(match approval_mode {
AppToolApproval::Auto => "auto",
AppToolApproval::Prompt => "prompt",
AppToolApproval::Approve => "approve",
});
}
if let Some(enabled_tools) = &config.enabled_tools
&& !enabled_tools.is_empty()
{

View File

@@ -29,6 +29,7 @@ async fn replace_mcp_servers_serializes_per_tool_approval_overrides() -> anyhow:
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
default_tools_approval_mode: Some(AppToolApproval::Auto),
enabled_tools: None,
disabled_tools: None,
scopes: None,
@@ -62,6 +63,7 @@ async fn replace_mcp_servers_serializes_per_tool_approval_overrides() -> anyhow:
r#"[mcp_servers.docs]
command = "docs-server"
supports_parallel_tool_calls = true
default_tools_approval_mode = "auto"
[mcp_servers.docs.tools]

View File

@@ -93,6 +93,10 @@ pub struct McpServerConfig {
#[serde(default, with = "option_duration_secs")]
pub tool_timeout_sec: Option<Duration>,
/// Approval mode for tools in this server unless a tool override exists.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_tools_approval_mode: Option<AppToolApproval>,
/// Explicit allow-list of tools exposed from this server. When set, only these tools will be registered.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled_tools: Option<Vec<String>>,
@@ -158,6 +162,8 @@ pub struct RawMcpServerConfig {
#[serde(default)]
pub supports_parallel_tool_calls: Option<bool>,
#[serde(default)]
pub default_tools_approval_mode: Option<AppToolApproval>,
#[serde(default)]
pub enabled_tools: Option<Vec<String>>,
#[serde(default)]
pub disabled_tools: Option<Vec<String>>,
@@ -194,6 +200,7 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
enabled,
required,
supports_parallel_tool_calls,
default_tools_approval_mode,
enabled_tools,
disabled_tools,
scopes,
@@ -260,6 +267,7 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
required: required.unwrap_or_default(),
supports_parallel_tool_calls: supports_parallel_tool_calls.unwrap_or_default(),
disabled_reason: None,
default_tools_approval_mode,
enabled_tools,
disabled_tools,
scopes,

View File

@@ -258,6 +258,38 @@ fn deserialize_server_config_with_parallel_tool_calls() {
assert!(cfg.supports_parallel_tool_calls);
}
#[test]
fn deserialize_server_config_with_default_tool_approval_mode() {
let cfg: McpServerConfig = toml::from_str(
r#"
command = "echo"
default_tools_approval_mode = "approve"
[tools.search]
approval_mode = "prompt"
"#,
)
.expect("should deserialize default tool approval mode");
assert_eq!(
cfg.default_tools_approval_mode,
Some(AppToolApproval::Approve)
);
assert_eq!(
cfg.tools.get("search"),
Some(&McpServerToolConfig {
approval_mode: Some(AppToolApproval::Prompt),
})
);
let serialized = toml::to_string(&cfg).expect("should serialize MCP config");
assert!(serialized.contains("default_tools_approval_mode = \"approve\""));
let round_tripped: McpServerConfig =
toml::from_str(&serialized).expect("should deserialize serialized MCP config");
assert_eq!(round_tripped, cfg);
}
#[test]
fn serialize_round_trips_server_config_with_parallel_tool_calls() {
let cfg: McpServerConfig = toml::from_str(
@@ -304,6 +336,7 @@ fn deserialize_ignores_unknown_server_fields() {
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
default_tools_approval_mode: None,
enabled_tools: None,
disabled_tools: None,
scopes: None,