[mcp] Fix plugin MCP approval policy. (#19537)

Plugin MCP servers are loaded from plugin manifests rather than
top-level `[mcp_servers]`, so their tool approval preferences need to be
stored and applied through the owning plugin config. Without this,
choosing "Always allow" for a plugin MCP tool could write a preference
that was not reliably used on later tool calls.

## Summary
- Add plugin-scoped MCP policy config under
`plugins.<plugin>.mcp_servers`, including server enablement, tool
allow/deny lists, server defaults, and per-tool approval modes.
- Overlay plugin MCP policy onto manifest-provided server configs when
plugins are loaded.
- Route persistent "Always allow" writes for plugin MCP tools back to
the owning `plugins.<plugin>.mcp_servers.<server>.tools.<tool>` config
entry.
- Reload user config after persisting an approval and make the plugin
load cache config-aware so stale plugin MCP policy is not reused after
`config.toml` changes.
- Regenerate the config schema and add coverage for plugin MCP policy
loading, approval lookup, persistence, and stale-cache prevention.

## Testing
- `cargo test -p codex-config`
- `cargo test -p codex-core-plugins`
- `cargo test -p codex-core --lib plugin_mcp`
This commit is contained in:
Matthew Zeng
2026-04-29 15:40:03 -07:00
committed by GitHub
parent 4241df4d79
commit e20391e567
15 changed files with 919 additions and 28 deletions
+25 -1
View File
@@ -12,6 +12,7 @@ use codex_config::ConfigLayerStack;
use codex_config::HooksFile;
use codex_config::types::McpServerConfig;
use codex_config::types::PluginConfig;
use codex_config::types::PluginMcpServerConfig;
use codex_core_skills::SkillMetadata;
use codex_core_skills::config_rules::SkillConfigRules;
use codex_core_skills::config_rules::resolve_disabled_skill_paths;
@@ -175,6 +176,7 @@ pub fn remote_installed_plugins_to_config(
plugin_id.as_key(),
PluginConfig {
enabled: plugin.enabled,
mcp_servers: HashMap::new(),
},
))
})
@@ -575,7 +577,10 @@ async fn load_plugin(
let mut mcp_servers = HashMap::new();
for mcp_config_path in plugin_mcp_config_paths(plugin_root.as_path(), manifest_paths) {
let plugin_mcp = load_mcp_servers_from_file(plugin_root.as_path(), &mcp_config_path).await;
for (name, config) in plugin_mcp.mcp_servers {
for (name, mut config) in plugin_mcp.mcp_servers {
if let Some(policy) = plugin.mcp_servers.get(&name) {
apply_plugin_mcp_server_policy(&mut config, policy);
}
if mcp_servers.insert(name.clone(), config).is_some() {
warn!(
plugin = %plugin_root.display(),
@@ -599,6 +604,25 @@ async fn load_plugin(
loaded_plugin
}
fn apply_plugin_mcp_server_policy(config: &mut McpServerConfig, policy: &PluginMcpServerConfig) {
config.enabled = policy.enabled;
if let Some(approval_mode) = policy.default_tools_approval_mode {
config.default_tools_approval_mode = Some(approval_mode);
}
if let Some(enabled_tools) = &policy.enabled_tools {
config.enabled_tools = Some(enabled_tools.clone());
}
if let Some(disabled_tools) = &policy.disabled_tools {
config.disabled_tools = Some(disabled_tools.clone());
}
for (tool_name, tool_policy) in &policy.tools {
let tool_config = config.tools.entry(tool_name.clone()).or_default();
if let Some(approval_mode) = tool_policy.approval_mode {
tool_config.approval_mode = Some(approval_mode);
}
}
}
#[derive(Debug, Clone)]
pub struct ResolvedPluginSkills {
pub skills: Vec<SkillMetadata>,