Propagate MCP disabled reason (#9207)

Indicate why MCP servers are disabled when they are disabled by
requirements:

```
➜  codex git:(main) ✗ just codex mcp list
cargo run --bin codex -- "$@"
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/codex mcp list`
Name         Command          Args  Env  Cwd  Status                                                                  Auth
docs         docs-mcp         -     -    -    disabled: requirements (MDM com.openai.codex:requirements_toml_base64)  Unsupported
hello_world  hello-world-mcp  -     -    -    disabled: requirements (MDM com.openai.codex:requirements_toml_base64)  Unsupported

➜  codex git:(main) ✗ just c
cargo run --bin codex -- "$@"
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.90s
     Running `target/debug/codex`
╭─────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0)                    │
│                                             │
│ model:     gpt-5.2 xhigh   /model to change │
│ directory: ~/code/codex/codex-rs            │
╰─────────────────────────────────────────────╯

/mcp

🔌  MCP Tools

  • No MCP tools available.

  • docs (disabled)
    • Reason: requirements (MDM com.openai.codex:requirements_toml_base64)

  • hello_world (disabled)
    • Reason: requirements (MDM com.openai.codex:requirements_toml_base64)
```
This commit is contained in:
gt-oai 2026-01-15 17:24:00 +00:00 committed by GitHub
parent ae96a15312
commit f6df1596eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 159 additions and 54 deletions

View file

@ -241,6 +241,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
let new_entry = McpServerConfig {
transport: transport.clone(),
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -448,6 +449,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
serde_json::json!({
"name": name,
"enabled": cfg.enabled,
"disabled_reason": cfg.disabled_reason.as_ref().map(ToString::to_string),
"transport": transport,
"startup_timeout_sec": cfg
.startup_timeout_sec
@ -492,11 +494,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
.map(|path| path.display().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "-".to_string());
let status = if cfg.enabled {
"enabled".to_string()
} else {
"disabled".to_string()
};
let status = format_mcp_status(cfg);
let auth_status = auth_statuses
.get(name.as_str())
.map(|entry| entry.auth_status)
@ -517,11 +515,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
bearer_token_env_var,
..
} => {
let status = if cfg.enabled {
"enabled".to_string()
} else {
"disabled".to_string()
};
let status = format_mcp_status(cfg);
let auth_status = auth_statuses
.get(name.as_str())
.map(|entry| entry.auth_status)
@ -691,6 +685,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
let output = serde_json::to_string_pretty(&serde_json::json!({
"name": get_args.name,
"enabled": server.enabled,
"disabled_reason": server.disabled_reason.as_ref().map(ToString::to_string),
"transport": transport,
"enabled_tools": server.enabled_tools.clone(),
"disabled_tools": server.disabled_tools.clone(),
@ -706,7 +701,11 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
}
if !server.enabled {
println!("{} (disabled)", get_args.name);
if let Some(reason) = server.disabled_reason.as_ref() {
println!("{name} (disabled: {reason})", name = get_args.name);
} else {
println!("{name} (disabled)", name = get_args.name);
}
return Ok(());
}
@ -828,3 +827,13 @@ fn validate_server_name(name: &str) -> Result<()> {
bail!("invalid server name '{name}' (use letters, numbers, '-', '_')");
}
}
fn format_mcp_status(config: &McpServerConfig) -> String {
if config.enabled {
"enabled".to_string()
} else if let Some(reason) = config.disabled_reason.as_ref() {
format!("disabled: {reason}")
} else {
"disabled".to_string()
}
}

View file

@ -89,6 +89,7 @@ async fn list_and_get_render_expected_output() -> Result<()> {
{
"name": "docs",
"enabled": true,
"disabled_reason": null,
"transport": {
"type": "stdio",
"command": "docs-server",

View file

@ -1124,6 +1124,7 @@ gpt-5 = "gpt-5.1"
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: Some(vec!["one".to_string(), "two".to_string()]),
@ -1145,6 +1146,7 @@ gpt-5 = "gpt-5.1"
env_http_headers: None,
},
enabled: false,
disabled_reason: None,
startup_timeout_sec: Some(std::time::Duration::from_secs(5)),
tool_timeout_sec: None,
enabled_tools: None,
@ -1209,6 +1211,7 @@ foo = { command = "cmd" }
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -1252,6 +1255,7 @@ foo = { command = "cmd" } # keep me
cwd: None,
},
enabled: false,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -1294,6 +1298,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -1337,6 +1342,7 @@ foo = { command = "cmd" }
cwd: None,
},
enabled: false,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,

View file

@ -2,6 +2,7 @@ use crate::auth::AuthCredentialsStoreMode;
use crate::config::types::DEFAULT_OTEL_ENVIRONMENT;
use crate::config::types::History;
use crate::config::types::McpServerConfig;
use crate::config::types::McpServerDisabledReason;
use crate::config::types::McpServerTransportConfig;
use crate::config::types::Notice;
use crate::config::types::Notifications;
@ -19,6 +20,7 @@ use crate::config_loader::ConfigRequirements;
use crate::config_loader::LoaderOverrides;
use crate::config_loader::McpServerIdentity;
use crate::config_loader::McpServerRequirement;
use crate::config_loader::Sourced;
use crate::config_loader::load_config_layers_state;
use crate::features::Feature;
use crate::features::FeatureOverrides;
@ -539,25 +541,32 @@ fn deserialize_config_toml_with_base(
fn filter_mcp_servers_by_requirements(
mcp_servers: &mut HashMap<String, McpServerConfig>,
mcp_requirements: Option<&BTreeMap<String, McpServerRequirement>>,
mcp_requirements: Option<&Sourced<BTreeMap<String, McpServerRequirement>>>,
) {
let Some(allowlist) = mcp_requirements else {
return;
};
let source = allowlist.source.clone();
for (name, server) in mcp_servers.iter_mut() {
let allowed = allowlist
.value
.get(name)
.is_some_and(|requirement| mcp_server_matches_requirement(requirement, server));
if !allowed {
if allowed {
server.disabled_reason = None;
} else {
server.enabled = false;
server.disabled_reason = Some(McpServerDisabledReason::Requirements {
source: source.clone(),
});
}
}
}
fn constrain_mcp_servers(
mcp_servers: HashMap<String, McpServerConfig>,
mcp_requirements: Option<&BTreeMap<String, McpServerRequirement>>,
mcp_requirements: Option<&Sourced<BTreeMap<String, McpServerRequirement>>>,
) -> ConstraintResult<Constrained<HashMap<String, McpServerConfig>>> {
if mcp_requirements.is_none() {
return Ok(Constrained::allow_any(mcp_servers));
@ -1707,6 +1716,7 @@ mod tests {
use crate::config::types::HistoryPersistence;
use crate::config::types::McpServerTransportConfig;
use crate::config::types::Notifications;
use crate::config_loader::RequirementSource;
use crate::features::Feature;
use super::*;
@ -1728,6 +1738,7 @@ mod tests {
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -1744,6 +1755,7 @@ mod tests {
env_http_headers: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -1976,9 +1988,9 @@ trust_level = "trusted"
(MATCHED_URL_SERVER.to_string(), http_mcp(GOOD_URL)),
(DIFFERENT_NAME_SERVER.to_string(), stdio_mcp("same-cmd")),
]);
filter_mcp_servers_by_requirements(
&mut servers,
Some(&BTreeMap::from([
let source = RequirementSource::LegacyManagedConfigTomlFromMdm;
let requirements = Sourced::new(
BTreeMap::from([
(
MISMATCHED_URL_SERVER.to_string(),
McpServerRequirement {
@ -2011,20 +2023,29 @@ trust_level = "trusted"
},
},
),
])),
]),
source.clone(),
);
filter_mcp_servers_by_requirements(&mut servers, Some(&requirements));
let reason = Some(McpServerDisabledReason::Requirements { source });
assert_eq!(
servers
.iter()
.map(|(name, server)| (name.clone(), server.enabled))
.collect::<HashMap<String, bool>>(),
.map(|(name, server)| (
name.clone(),
(server.enabled, server.disabled_reason.clone())
))
.collect::<HashMap<String, (bool, Option<McpServerDisabledReason>)>>(),
HashMap::from([
(MISMATCHED_URL_SERVER.to_string(), false),
(MISMATCHED_COMMAND_SERVER.to_string(), false),
(MATCHED_URL_SERVER.to_string(), true),
(MATCHED_COMMAND_SERVER.to_string(), true),
(DIFFERENT_NAME_SERVER.to_string(), false),
(MISMATCHED_URL_SERVER.to_string(), (false, reason.clone())),
(
MISMATCHED_COMMAND_SERVER.to_string(),
(false, reason.clone()),
),
(MATCHED_URL_SERVER.to_string(), (true, None)),
(MATCHED_COMMAND_SERVER.to_string(), (true, None)),
(DIFFERENT_NAME_SERVER.to_string(), (false, reason)),
])
);
}
@ -2041,11 +2062,14 @@ trust_level = "trusted"
assert_eq!(
servers
.iter()
.map(|(name, server)| (name.clone(), server.enabled))
.collect::<HashMap<String, bool>>(),
.map(|(name, server)| (
name.clone(),
(server.enabled, server.disabled_reason.clone())
))
.collect::<HashMap<String, (bool, Option<McpServerDisabledReason>)>>(),
HashMap::from([
("server-a".to_string(), true),
("server-b".to_string(), true),
("server-a".to_string(), (true, None)),
("server-b".to_string(), (true, None)),
])
);
}
@ -2057,16 +2081,22 @@ trust_level = "trusted"
("server-b".to_string(), http_mcp("https://example.com/b")),
]);
filter_mcp_servers_by_requirements(&mut servers, Some(&BTreeMap::new()));
let source = RequirementSource::LegacyManagedConfigTomlFromMdm;
let requirements = Sourced::new(BTreeMap::new(), source.clone());
filter_mcp_servers_by_requirements(&mut servers, Some(&requirements));
let reason = Some(McpServerDisabledReason::Requirements { source });
assert_eq!(
servers
.iter()
.map(|(name, server)| (name.clone(), server.enabled))
.collect::<HashMap<String, bool>>(),
.map(|(name, server)| (
name.clone(),
(server.enabled, server.disabled_reason.clone())
))
.collect::<HashMap<String, (bool, Option<McpServerDisabledReason>)>>(),
HashMap::from([
("server-a".to_string(), false),
("server-b".to_string(), false),
("server-a".to_string(), (false, reason.clone())),
("server-b".to_string(), (false, reason)),
])
);
}
@ -2491,6 +2521,7 @@ trust_level = "trusted"
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(3)),
tool_timeout_sec: Some(Duration::from_secs(5)),
enabled_tools: None,
@ -2644,6 +2675,7 @@ bearer_token = "secret"
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -2712,6 +2744,7 @@ ZIG_VAR = "3"
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -2760,6 +2793,7 @@ ZIG_VAR = "3"
cwd: Some(cwd_path.clone()),
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -2806,6 +2840,7 @@ ZIG_VAR = "3"
env_http_headers: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(2)),
tool_timeout_sec: None,
enabled_tools: None,
@ -2868,6 +2903,7 @@ startup_timeout_sec = 2.0
)])),
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(2)),
tool_timeout_sec: None,
enabled_tools: None,
@ -2942,6 +2978,7 @@ X-Auth = "DOCS_AUTH"
)])),
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(2)),
tool_timeout_sec: None,
enabled_tools: None,
@ -2969,6 +3006,7 @@ X-Auth = "DOCS_AUTH"
env_http_headers: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -3034,6 +3072,7 @@ url = "https://example.com/mcp"
)])),
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(2)),
tool_timeout_sec: None,
enabled_tools: None,
@ -3051,6 +3090,7 @@ url = "https://example.com/mcp"
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -3131,6 +3171,7 @@ url = "https://example.com/mcp"
cwd: None,
},
enabled: false,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -3173,6 +3214,7 @@ url = "https://example.com/mcp"
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: Some(vec!["allowed".to_string()]),

View file

@ -3,11 +3,13 @@
// Note this file should generally be restricted to simple struct/enum
// definitions that do not contain business logic.
use crate::config_loader::RequirementSource;
pub use codex_protocol::config_types::AltScreenMode;
pub use codex_protocol::config_types::WebSearchMode;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use std::time::Duration;
use wildmatch::WildMatchPattern;
@ -20,6 +22,23 @@ use serde::de::Error as SerdeError;
pub const DEFAULT_OTEL_ENVIRONMENT: &str = "dev";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum McpServerDisabledReason {
Unknown,
Requirements { source: RequirementSource },
}
impl fmt::Display for McpServerDisabledReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
McpServerDisabledReason::Unknown => write!(f, "unknown"),
McpServerDisabledReason::Requirements { source } => {
write!(f, "requirements ({source})")
}
}
}
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct McpServerConfig {
#[serde(flatten)]
@ -29,6 +48,10 @@ pub struct McpServerConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
/// Reason this server was disabled after applying requirements.
#[serde(skip)]
pub disabled_reason: Option<McpServerDisabledReason>,
/// Startup timeout in seconds for initializing MCP server & initially listing tools.
#[serde(
default,
@ -160,6 +183,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
startup_timeout_sec,
tool_timeout_sec,
enabled,
disabled_reason: None,
enabled_tools,
disabled_tools,
})

View file

@ -44,7 +44,7 @@ impl fmt::Display for RequirementSource {
pub struct ConfigRequirements {
pub approval_policy: Constrained<AskForApproval>,
pub sandbox_policy: Constrained<SandboxPolicy>,
pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
}
impl Default for ConfigRequirements {
@ -273,7 +273,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
Ok(ConfigRequirements {
approval_policy,
sandbox_policy,
mcp_servers: mcp_servers.map(|sourced| sourced.value),
mcp_servers,
})
}
}
@ -571,24 +571,27 @@ mod tests {
assert_eq!(
requirements.mcp_servers,
Some(BTreeMap::from([
(
"docs".to_string(),
McpServerRequirement {
identity: McpServerIdentity::Command {
command: "codex-mcp".to_string(),
Some(Sourced::new(
BTreeMap::from([
(
"docs".to_string(),
McpServerRequirement {
identity: McpServerIdentity::Command {
command: "codex-mcp".to_string(),
},
},
},
),
(
"remote".to_string(),
McpServerRequirement {
identity: McpServerIdentity::Url {
url: "https://example.com/mcp".to_string(),
),
(
"remote".to_string(),
McpServerRequirement {
identity: McpServerIdentity::Url {
url: "https://example.com/mcp".to_string(),
},
},
},
),
]))
),
]),
RequirementSource::Unknown,
))
);
Ok(())
}

View file

@ -30,6 +30,7 @@ pub use config_requirements::McpServerIdentity;
pub use config_requirements::McpServerRequirement;
pub use config_requirements::RequirementSource;
pub use config_requirements::SandboxModeRequirement;
pub use config_requirements::Sourced;
pub use merge::merge_toml_values;
pub(crate) use overrides::build_cli_overrides_layer;
pub use state::ConfigLayerEntry;

View file

@ -1171,6 +1171,7 @@ mod tests {
env_http_headers: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -1215,6 +1216,7 @@ mod tests {
env_http_headers: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,

View file

@ -88,6 +88,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
enabled_tools: None,
@ -225,6 +226,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
enabled_tools: None,
@ -420,6 +422,7 @@ async fn stdio_image_completions_round_trip() -> anyhow::Result<()> {
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
enabled_tools: None,
@ -563,6 +566,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
enabled_tools: None,
@ -717,6 +721,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
env_http_headers: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
enabled_tools: None,
@ -903,6 +908,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> {
env_http_headers: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
enabled_tools: None,

View file

@ -426,6 +426,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()>
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(std::time::Duration::from_secs(10)),
tool_timeout_sec: None,
enabled_tools: None,
@ -517,6 +518,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
enabled_tools: None,
@ -777,6 +779,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> {
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: Some(std::time::Duration::from_secs(10)),
tool_timeout_sec: None,
enabled_tools: None,

View file

@ -1358,7 +1358,6 @@ pub(crate) fn new_mcp_tools_output(
if tools.is_empty() {
lines.push(" • No MCP tools available.".italic().into());
lines.push("".into());
return PlainHistoryCell { lines };
}
let mut servers: Vec<_> = config.mcp_servers.iter().collect();
@ -1382,6 +1381,9 @@ pub(crate) fn new_mcp_tools_output(
header.push(" ".into());
header.push("(disabled)".red());
lines.push(header.into());
if let Some(reason) = cfg.disabled_reason.as_ref().map(ToString::to_string) {
lines.push(vec![" • Reason: ".into(), reason.dim()].into());
}
lines.push(Line::from(""));
continue;
}
@ -1836,6 +1838,7 @@ mod tests {
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -1856,6 +1859,7 @@ mod tests {
env_http_headers: Some(env_headers),
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,

View file

@ -1422,7 +1422,6 @@ pub(crate) fn new_mcp_tools_output(
if tools.is_empty() {
lines.push(" • No MCP tools available.".italic().into());
lines.push("".into());
return PlainHistoryCell { lines };
}
let mut servers: Vec<_> = config.mcp_servers.iter().collect();
@ -1446,6 +1445,9 @@ pub(crate) fn new_mcp_tools_output(
header.push(" ".into());
header.push("(disabled)".red());
lines.push(header.into());
if let Some(reason) = cfg.disabled_reason.as_ref().map(ToString::to_string) {
lines.push(vec![" • Reason: ".into(), reason.dim()].into());
}
lines.push(Line::from(""));
continue;
}
@ -1971,6 +1973,7 @@ mod tests {
cwd: None,
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
@ -1991,6 +1994,7 @@ mod tests {
env_http_headers: Some(env_headers),
},
enabled: true,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,