[MCP] Add configuration options to enable or disable specific tools (#5367)

Some MCP servers expose a lot of tools. In those cases, it is reasonable
to allow/denylist tools for Codex to use so it doesn't get overwhelmed
with too many tools.

The new configuration options available in the `mcp_server` toml table
are:
* `enabled_tools`
* `disabled_tools`

Fixes #4796
This commit is contained in:
Gabriel Peal
2025-10-20 15:35:36 -07:00
committed by GitHub
parent c37469b5ba
commit 740b4a95f4
8 changed files with 361 additions and 81 deletions

View File

@@ -253,6 +253,8 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
enabled: true, enabled: true,
startup_timeout_sec: None, startup_timeout_sec: None,
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}; };
servers.insert(name.clone(), new_entry); servers.insert(name.clone(), new_entry);
@@ -676,6 +678,8 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
"name": get_args.name, "name": get_args.name,
"enabled": server.enabled, "enabled": server.enabled,
"transport": transport, "transport": transport,
"enabled_tools": server.enabled_tools.clone(),
"disabled_tools": server.disabled_tools.clone(),
"startup_timeout_sec": server "startup_timeout_sec": server
.startup_timeout_sec .startup_timeout_sec
.map(|timeout| timeout.as_secs_f64()), .map(|timeout| timeout.as_secs_f64()),
@@ -687,8 +691,28 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
return Ok(()); return Ok(());
} }
if !server.enabled {
println!("{} (disabled)", get_args.name);
return Ok(());
}
println!("{}", get_args.name); println!("{}", get_args.name);
println!(" enabled: {}", server.enabled); println!(" enabled: {}", server.enabled);
let format_tool_list = |tools: &Option<Vec<String>>| -> String {
match tools {
Some(list) if list.is_empty() => "[]".to_string(),
Some(list) => list.join(", "),
None => "-".to_string(),
}
};
if server.enabled_tools.is_some() {
let enabled_tools_display = format_tool_list(&server.enabled_tools);
println!(" enabled_tools: {enabled_tools_display}");
}
if server.disabled_tools.is_some() {
let disabled_tools_display = format_tool_list(&server.disabled_tools);
println!(" disabled_tools: {disabled_tools_display}");
}
match &server.transport { match &server.transport {
McpServerTransportConfig::Stdio { McpServerTransportConfig::Stdio {
command, command,

View File

@@ -134,3 +134,28 @@ async fn list_and_get_render_expected_output() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test]
async fn get_disabled_server_shows_single_line() -> Result<()> {
let codex_home = TempDir::new()?;
let mut add = codex_command(codex_home.path())?;
add.args(["mcp", "add", "docs", "--", "docs-server"])
.assert()
.success();
let mut servers = load_global_mcp_servers(codex_home.path()).await?;
let docs = servers
.get_mut("docs")
.expect("docs server should exist after add");
docs.enabled = false;
write_global_mcp_servers(codex_home.path(), &servers)?;
let mut get_cmd = codex_command(codex_home.path())?;
let get_output = get_cmd.args(["mcp", "get", "docs"]).output()?;
assert!(get_output.status.success());
let stdout = String::from_utf8(get_output.stdout)?;
assert_eq!(stdout.trim_end(), "docs (disabled)");
Ok(())
}

View File

@@ -484,6 +484,16 @@ pub fn write_global_mcp_servers(
entry["tool_timeout_sec"] = toml_edit::value(timeout.as_secs_f64()); entry["tool_timeout_sec"] = toml_edit::value(timeout.as_secs_f64());
} }
if let Some(enabled_tools) = &config.enabled_tools {
entry["enabled_tools"] =
TomlItem::Value(enabled_tools.iter().collect::<TomlArray>().into());
}
if let Some(disabled_tools) = &config.disabled_tools {
entry["disabled_tools"] =
TomlItem::Value(disabled_tools.iter().collect::<TomlArray>().into());
}
doc["mcp_servers"][name.as_str()] = TomlItem::Table(entry); doc["mcp_servers"][name.as_str()] = TomlItem::Table(entry);
} }
} }
@@ -1923,6 +1933,8 @@ approve_all = true
enabled: true, enabled: true,
startup_timeout_sec: Some(Duration::from_secs(3)), startup_timeout_sec: Some(Duration::from_secs(3)),
tool_timeout_sec: Some(Duration::from_secs(5)), tool_timeout_sec: Some(Duration::from_secs(5)),
enabled_tools: None,
disabled_tools: None,
}, },
); );
@@ -2059,6 +2071,8 @@ bearer_token = "secret"
enabled: true, enabled: true,
startup_timeout_sec: None, startup_timeout_sec: None,
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
)]); )]);
@@ -2121,6 +2135,8 @@ ZIG_VAR = "3"
enabled: true, enabled: true,
startup_timeout_sec: None, startup_timeout_sec: None,
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
)]); )]);
@@ -2163,6 +2179,8 @@ ZIG_VAR = "3"
enabled: true, enabled: true,
startup_timeout_sec: None, startup_timeout_sec: None,
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
)]); )]);
@@ -2204,6 +2222,8 @@ ZIG_VAR = "3"
enabled: true, enabled: true,
startup_timeout_sec: Some(Duration::from_secs(2)), startup_timeout_sec: Some(Duration::from_secs(2)),
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
)]); )]);
@@ -2261,6 +2281,8 @@ startup_timeout_sec = 2.0
enabled: true, enabled: true,
startup_timeout_sec: Some(Duration::from_secs(2)), startup_timeout_sec: Some(Duration::from_secs(2)),
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
)]); )]);
write_global_mcp_servers(codex_home.path(), &servers)?; write_global_mcp_servers(codex_home.path(), &servers)?;
@@ -2330,6 +2352,8 @@ X-Auth = "DOCS_AUTH"
enabled: true, enabled: true,
startup_timeout_sec: Some(Duration::from_secs(2)), startup_timeout_sec: Some(Duration::from_secs(2)),
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
)]); )]);
@@ -2351,6 +2375,8 @@ X-Auth = "DOCS_AUTH"
enabled: true, enabled: true,
startup_timeout_sec: None, startup_timeout_sec: None,
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
); );
write_global_mcp_servers(codex_home.path(), &servers)?; write_global_mcp_servers(codex_home.path(), &servers)?;
@@ -2410,6 +2436,8 @@ url = "https://example.com/mcp"
enabled: true, enabled: true,
startup_timeout_sec: Some(Duration::from_secs(2)), startup_timeout_sec: Some(Duration::from_secs(2)),
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
), ),
( (
@@ -2425,6 +2453,8 @@ url = "https://example.com/mcp"
enabled: true, enabled: true,
startup_timeout_sec: None, startup_timeout_sec: None,
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
), ),
]); ]);
@@ -2499,6 +2529,8 @@ url = "https://example.com/mcp"
enabled: false, enabled: false,
startup_timeout_sec: None, startup_timeout_sec: None,
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
)]); )]);
@@ -2518,6 +2550,49 @@ url = "https://example.com/mcp"
Ok(()) Ok(())
} }
#[tokio::test]
async fn write_global_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
transport: McpServerTransportConfig::Stdio {
command: "docs-server".to_string(),
args: Vec::new(),
env: None,
env_vars: Vec::new(),
cwd: None,
},
enabled: true,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: Some(vec!["allowed".to_string()]),
disabled_tools: Some(vec!["blocked".to_string()]),
},
)]);
write_global_mcp_servers(codex_home.path(), &servers)?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
let serialized = std::fs::read_to_string(&config_path)?;
assert!(serialized.contains(r#"enabled_tools = ["allowed"]"#));
assert!(serialized.contains(r#"disabled_tools = ["blocked"]"#));
let loaded = load_global_mcp_servers(codex_home.path()).await?;
let docs = loaded.get("docs").expect("docs entry");
assert_eq!(
docs.enabled_tools.as_ref(),
Some(&vec!["allowed".to_string()])
);
assert_eq!(
docs.disabled_tools.as_ref(),
Some(&vec!["blocked".to_string()])
);
Ok(())
}
#[tokio::test] #[tokio::test]
async fn persist_model_selection_updates_defaults() -> anyhow::Result<()> { async fn persist_model_selection_updates_defaults() -> anyhow::Result<()> {
let codex_home = TempDir::new()?; let codex_home = TempDir::new()?;

View File

@@ -35,6 +35,14 @@ pub struct McpServerConfig {
/// Default timeout for MCP tool calls initiated via this server. /// Default timeout for MCP tool calls initiated via this server.
#[serde(default, with = "option_duration_secs")] #[serde(default, with = "option_duration_secs")]
pub tool_timeout_sec: Option<Duration>, pub tool_timeout_sec: Option<Duration>,
/// 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>>,
/// Explicit deny-list of tools. These tools will be removed after applying `enabled_tools`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disabled_tools: Option<Vec<String>>,
} }
impl<'de> Deserialize<'de> for McpServerConfig { impl<'de> Deserialize<'de> for McpServerConfig {
@@ -42,7 +50,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
#[derive(Deserialize)] #[derive(Deserialize, Clone)]
struct RawMcpServerConfig { struct RawMcpServerConfig {
// stdio // stdio
command: Option<String>, command: Option<String>,
@@ -72,9 +80,13 @@ impl<'de> Deserialize<'de> for McpServerConfig {
tool_timeout_sec: Option<Duration>, tool_timeout_sec: Option<Duration>,
#[serde(default)] #[serde(default)]
enabled: Option<bool>, enabled: Option<bool>,
#[serde(default)]
enabled_tools: Option<Vec<String>>,
#[serde(default)]
disabled_tools: Option<Vec<String>>,
} }
let raw = RawMcpServerConfig::deserialize(deserializer)?; let mut raw = RawMcpServerConfig::deserialize(deserializer)?;
let startup_timeout_sec = match (raw.startup_timeout_sec, raw.startup_timeout_ms) { let startup_timeout_sec = match (raw.startup_timeout_sec, raw.startup_timeout_ms) {
(Some(sec), _) => { (Some(sec), _) => {
@@ -84,6 +96,10 @@ impl<'de> Deserialize<'de> for McpServerConfig {
(None, Some(ms)) => Some(Duration::from_millis(ms)), (None, Some(ms)) => Some(Duration::from_millis(ms)),
(None, None) => None, (None, None) => None,
}; };
let tool_timeout_sec = raw.tool_timeout_sec;
let enabled = raw.enabled.unwrap_or_else(default_enabled);
let enabled_tools = raw.enabled_tools.clone();
let disabled_tools = raw.disabled_tools.clone();
fn throw_if_set<E, T>(transport: &str, field: &str, value: Option<&T>) -> Result<(), E> fn throw_if_set<E, T>(transport: &str, field: &str, value: Option<&T>) -> Result<(), E>
where where
@@ -97,72 +113,46 @@ impl<'de> Deserialize<'de> for McpServerConfig {
))) )))
} }
let transport = match raw { let transport = if let Some(command) = raw.command.clone() {
RawMcpServerConfig { throw_if_set("stdio", "url", raw.url.as_ref())?;
command: Some(command), throw_if_set(
args, "stdio",
env, "bearer_token_env_var",
env_vars, raw.bearer_token_env_var.as_ref(),
cwd, )?;
url, throw_if_set("stdio", "bearer_token", raw.bearer_token.as_ref())?;
bearer_token_env_var, throw_if_set("stdio", "http_headers", raw.http_headers.as_ref())?;
http_headers, throw_if_set("stdio", "env_http_headers", raw.env_http_headers.as_ref())?;
env_http_headers, McpServerTransportConfig::Stdio {
..
} => {
throw_if_set("stdio", "url", url.as_ref())?;
throw_if_set(
"stdio",
"bearer_token_env_var",
bearer_token_env_var.as_ref(),
)?;
throw_if_set("stdio", "http_headers", http_headers.as_ref())?;
throw_if_set("stdio", "env_http_headers", env_http_headers.as_ref())?;
McpServerTransportConfig::Stdio {
command,
args: args.unwrap_or_default(),
env,
env_vars: env_vars.unwrap_or_default(),
cwd,
}
}
RawMcpServerConfig {
url: Some(url),
bearer_token,
bearer_token_env_var,
command, command,
args, args: raw.args.clone().unwrap_or_default(),
env, env: raw.env.clone(),
env_vars, env_vars: raw.env_vars.clone().unwrap_or_default(),
cwd, cwd: raw.cwd.take(),
http_headers,
env_http_headers,
startup_timeout_sec: _,
tool_timeout_sec: _,
startup_timeout_ms: _,
enabled: _,
} => {
throw_if_set("streamable_http", "command", command.as_ref())?;
throw_if_set("streamable_http", "args", args.as_ref())?;
throw_if_set("streamable_http", "env", env.as_ref())?;
throw_if_set("streamable_http", "env_vars", env_vars.as_ref())?;
throw_if_set("streamable_http", "cwd", cwd.as_ref())?;
throw_if_set("streamable_http", "bearer_token", bearer_token.as_ref())?;
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
http_headers,
env_http_headers,
}
} }
_ => return Err(SerdeError::custom("invalid transport")), } else if let Some(url) = raw.url.clone() {
throw_if_set("streamable_http", "args", raw.args.as_ref())?;
throw_if_set("streamable_http", "env", raw.env.as_ref())?;
throw_if_set("streamable_http", "env_vars", raw.env_vars.as_ref())?;
throw_if_set("streamable_http", "cwd", raw.cwd.as_ref())?;
throw_if_set("streamable_http", "bearer_token", raw.bearer_token.as_ref())?;
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var: raw.bearer_token_env_var.clone(),
http_headers: raw.http_headers.clone(),
env_http_headers: raw.env_http_headers.take(),
}
} else {
return Err(SerdeError::custom("invalid transport"));
}; };
Ok(Self { Ok(Self {
transport, transport,
startup_timeout_sec, startup_timeout_sec,
tool_timeout_sec: raw.tool_timeout_sec, tool_timeout_sec,
enabled: raw.enabled.unwrap_or_else(default_enabled), enabled,
enabled_tools,
disabled_tools,
}) })
} }
} }
@@ -527,6 +517,8 @@ mod tests {
} }
); );
assert!(cfg.enabled); assert!(cfg.enabled);
assert!(cfg.enabled_tools.is_none());
assert!(cfg.disabled_tools.is_none());
} }
#[test] #[test]
@@ -701,6 +693,21 @@ mod tests {
); );
} }
#[test]
fn deserialize_server_config_with_tool_filters() {
let cfg: McpServerConfig = toml::from_str(
r#"
command = "echo"
enabled_tools = ["allowed"]
disabled_tools = ["blocked"]
"#,
)
.expect("should deserialize tool filters");
assert_eq!(cfg.enabled_tools, Some(vec!["allowed".to_string()]));
assert_eq!(cfg.disabled_tools, Some(vec!["blocked".to_string()]));
}
#[test] #[test]
fn deserialize_rejects_command_and_url() { fn deserialize_rejects_command_and_url() {
toml::from_str::<McpServerConfig>( toml::from_str::<McpServerConfig>(

View File

@@ -237,6 +237,9 @@ pub(crate) struct McpConnectionManager {
/// Fully qualified tool name -> tool instance. /// Fully qualified tool name -> tool instance.
tools: HashMap<String, ToolInfo>, tools: HashMap<String, ToolInfo>,
/// Server-name -> configured tool filters.
tool_filters: HashMap<String, ToolFilter>,
} }
impl McpConnectionManager { impl McpConnectionManager {
@@ -261,6 +264,7 @@ impl McpConnectionManager {
// Launch all configured servers concurrently. // Launch all configured servers concurrently.
let mut join_set = JoinSet::new(); let mut join_set = JoinSet::new();
let mut errors = ClientStartErrors::new(); let mut errors = ClientStartErrors::new();
let mut tool_filters: HashMap<String, ToolFilter> = HashMap::new();
for (server_name, cfg) in mcp_servers { for (server_name, cfg) in mcp_servers {
// Validate server name before spawning // Validate server name before spawning
@@ -273,11 +277,13 @@ impl McpConnectionManager {
} }
if !cfg.enabled { if !cfg.enabled {
tool_filters.insert(server_name, ToolFilter::from_config(&cfg));
continue; continue;
} }
let startup_timeout = cfg.startup_timeout_sec.unwrap_or(DEFAULT_STARTUP_TIMEOUT); let startup_timeout = cfg.startup_timeout_sec.unwrap_or(DEFAULT_STARTUP_TIMEOUT);
let tool_timeout = cfg.tool_timeout_sec.unwrap_or(DEFAULT_TOOL_TIMEOUT); let tool_timeout = cfg.tool_timeout_sec.unwrap_or(DEFAULT_TOOL_TIMEOUT);
tool_filters.insert(server_name.clone(), ToolFilter::from_config(&cfg));
let resolved_bearer_token = match &cfg.transport { let resolved_bearer_token = match &cfg.transport {
McpServerTransportConfig::StreamableHttp { McpServerTransportConfig::StreamableHttp {
@@ -393,9 +399,17 @@ impl McpConnectionManager {
} }
}; };
let tools = qualify_tools(all_tools); let filtered_tools = filter_tools(all_tools, &tool_filters);
let tools = qualify_tools(filtered_tools);
Ok((Self { clients, tools }, errors)) Ok((
Self {
clients,
tools,
tool_filters,
},
errors,
))
} }
/// Returns a single map that contains all tools. Each key is the /// Returns a single map that contains all tools. Each key is the
@@ -541,6 +555,13 @@ impl McpConnectionManager {
tool: &str, tool: &str,
arguments: Option<serde_json::Value>, arguments: Option<serde_json::Value>,
) -> Result<mcp_types::CallToolResult> { ) -> Result<mcp_types::CallToolResult> {
if let Some(filter) = self.tool_filters.get(server)
&& !filter.allows(tool)
{
return Err(anyhow!(
"tool '{tool}' is disabled for MCP server '{server}'"
));
}
let managed = self let managed = self
.clients .clients
.get(server) .get(server)
@@ -619,6 +640,52 @@ impl McpConnectionManager {
} }
} }
/// A tool is allowed to be used if both are true:
/// 1. enabled is None (no allowlist is set) or the tool is explicitly enabled.
/// 2. The tool is not explicitly disabled.
#[derive(Default, Clone)]
struct ToolFilter {
enabled: Option<HashSet<String>>,
disabled: HashSet<String>,
}
impl ToolFilter {
fn from_config(cfg: &McpServerConfig) -> Self {
let enabled = cfg
.enabled_tools
.as_ref()
.map(|tools| tools.iter().cloned().collect::<HashSet<_>>());
let disabled = cfg
.disabled_tools
.as_ref()
.map(|tools| tools.iter().cloned().collect::<HashSet<_>>())
.unwrap_or_default();
Self { enabled, disabled }
}
fn allows(&self, tool_name: &str) -> bool {
if let Some(enabled) = &self.enabled
&& !enabled.contains(tool_name)
{
return false;
}
!self.disabled.contains(tool_name)
}
}
fn filter_tools(tools: Vec<ToolInfo>, filters: &HashMap<String, ToolFilter>) -> Vec<ToolInfo> {
tools
.into_iter()
.filter(|tool| {
filters
.get(&tool.server_name)
.is_none_or(|filter| filter.allows(&tool.tool_name))
})
.collect()
}
fn resolve_bearer_token( fn resolve_bearer_token(
server_name: &str, server_name: &str,
bearer_token_env_var: Option<&str>, bearer_token_env_var: Option<&str>,
@@ -711,6 +778,7 @@ fn is_valid_mcp_server_name(server_name: &str) -> bool {
mod tests { mod tests {
use super::*; use super::*;
use mcp_types::ToolInputSchema; use mcp_types::ToolInputSchema;
use std::collections::HashSet;
fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo { fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
ToolInfo { ToolInfo {
@@ -793,4 +861,75 @@ mod tests {
"mcp__my_server__yet_anot419a82a89325c1b477274a41f8c65ea5f3a7f341" "mcp__my_server__yet_anot419a82a89325c1b477274a41f8c65ea5f3a7f341"
); );
} }
#[test]
fn tool_filter_allows_by_default() {
let filter = ToolFilter::default();
assert!(filter.allows("any"));
}
#[test]
fn tool_filter_applies_enabled_list() {
let filter = ToolFilter {
enabled: Some(HashSet::from(["allowed".to_string()])),
disabled: HashSet::new(),
};
assert!(filter.allows("allowed"));
assert!(!filter.allows("denied"));
}
#[test]
fn tool_filter_applies_disabled_list() {
let filter = ToolFilter {
enabled: None,
disabled: HashSet::from(["blocked".to_string()]),
};
assert!(!filter.allows("blocked"));
assert!(filter.allows("open"));
}
#[test]
fn tool_filter_applies_enabled_then_disabled() {
let filter = ToolFilter {
enabled: Some(HashSet::from(["keep".to_string(), "remove".to_string()])),
disabled: HashSet::from(["remove".to_string()]),
};
assert!(filter.allows("keep"));
assert!(!filter.allows("remove"));
assert!(!filter.allows("unknown"));
}
#[test]
fn filter_tools_applies_per_server_filters() {
let tools = vec![
create_test_tool("server1", "tool_a"),
create_test_tool("server1", "tool_b"),
create_test_tool("server2", "tool_a"),
];
let mut filters = HashMap::new();
filters.insert(
"server1".to_string(),
ToolFilter {
enabled: Some(HashSet::from(["tool_a".to_string(), "tool_b".to_string()])),
disabled: HashSet::from(["tool_b".to_string()]),
},
);
filters.insert(
"server2".to_string(),
ToolFilter {
enabled: None,
disabled: HashSet::from(["tool_a".to_string()]),
},
);
let filtered = filter_tools(tools, &filters);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].server_name, "server1");
assert_eq!(filtered[0].tool_name, "tool_a");
}
} }

View File

@@ -94,6 +94,8 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
enabled: true, enabled: true,
startup_timeout_sec: Some(Duration::from_secs(10)), startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
); );
}) })
@@ -230,6 +232,8 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
enabled: true, enabled: true,
startup_timeout_sec: Some(Duration::from_secs(10)), startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
); );
}) })
@@ -381,6 +385,8 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
enabled: true, enabled: true,
startup_timeout_sec: Some(Duration::from_secs(10)), startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
); );
}) })
@@ -564,6 +570,8 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> {
enabled: true, enabled: true,
startup_timeout_sec: Some(Duration::from_secs(10)), startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None, tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
}, },
); );
}) })

View File

@@ -1056,18 +1056,21 @@ pub(crate) fn new_mcp_tools_output(
.collect(); .collect();
names.sort(); names.sort();
let status = auth_statuses let auth_status = auth_statuses
.get(server.as_str()) .get(server.as_str())
.copied() .copied()
.unwrap_or(McpAuthStatus::Unsupported); .unwrap_or(McpAuthStatus::Unsupported);
lines.push(vec![" Server: ".into(), server.clone().into()].into()); let mut header: Vec<Span<'static>> = vec!["".into(), server.clone().into()];
let status_line = if cfg.enabled { if !cfg.enabled {
vec![" • Status: ".into(), "enabled".green()].into() header.push(" ".into());
} else { header.push("(disabled)".red());
vec![" • Status: ".into(), "disabled".red()].into() lines.push(header.into());
}; lines.push(Line::from(""));
lines.push(status_line); continue;
lines.push(vec![" • Auth: ".into(), status.to_string().into()].into()); }
lines.push(header.into());
lines.push(vec![" • Status: ".into(), "enabled".green()].into());
lines.push(vec![" • Auth: ".into(), auth_status.to_string().into()].into());
match &cfg.transport { match &cfg.transport {
McpServerTransportConfig::Stdio { McpServerTransportConfig::Stdio {
@@ -1128,15 +1131,6 @@ pub(crate) fn new_mcp_tools_output(
} }
} }
if !cfg.enabled {
let disabled = "(disabled)".red();
lines.push(vec![" • Tools: ".into(), disabled.clone()].into());
lines.push(vec![" • Resources: ".into(), disabled.clone()].into());
lines.push(vec![" • Resource templates: ".into(), disabled].into());
lines.push(Line::from(""));
continue;
}
if names.is_empty() { if names.is_empty() {
lines.push(" • Tools: (none)".into()); lines.push(" • Tools: (none)".into());
} else { } else {

View File

@@ -441,8 +441,14 @@ startup_timeout_sec = 20
tool_timeout_sec = 30 tool_timeout_sec = 30
# Optional: disable a server without removing it # Optional: disable a server without removing it
enabled = false enabled = false
# Optional: only expose a subset of tools from this server
enabled_tools = ["search", "summarize"]
# Optional: hide specific tools (applied after `enabled_tools`, if set)
disabled_tools = ["search"]
``` ```
When both `enabled_tools` and `disabled_tools` are specified, Codex first restricts the server to the allow-list and then removes any tools that appear in the deny-list.
#### Experimental RMCP client #### Experimental RMCP client
Codex is transitioning to the [official Rust MCP SDK](https://github.com/modelcontextprotocol/rust-sdk). Codex is transitioning to the [official Rust MCP SDK](https://github.com/modelcontextprotocol/rust-sdk).
@@ -871,6 +877,8 @@ If `forced_chatgpt_workspace_id` is set but `forced_login_method` is not set, AP
| `mcp_servers.<id>.enabled` | boolean | When false, Codex skips starting the server (default: true). | | `mcp_servers.<id>.enabled` | boolean | When false, Codex skips starting the server (default: true). |
| `mcp_servers.<id>.startup_timeout_sec` | number | Startup timeout in seconds (default: 10). Timeout is applied both for initializing MCP server and initially listing tools. | | `mcp_servers.<id>.startup_timeout_sec` | number | Startup timeout in seconds (default: 10). Timeout is applied both for initializing MCP server and initially listing tools. |
| `mcp_servers.<id>.tool_timeout_sec` | number | Per-tool timeout in seconds (default: 60). Accepts fractional values; omit to use the default. | | `mcp_servers.<id>.tool_timeout_sec` | number | Per-tool timeout in seconds (default: 60). Accepts fractional values; omit to use the default. |
| `mcp_servers.<id>.enabled_tools` | array<string> | Restrict the server to the listed tool names. |
| `mcp_servers.<id>.disabled_tools` | array<string> | Remove the listed tool names after applying `enabled_tools`, if any. |
| `model_providers.<id>.name` | string | Display name. | | `model_providers.<id>.name` | string | Display name. |
| `model_providers.<id>.base_url` | string | API base URL. | | `model_providers.<id>.base_url` | string | API base URL. |
| `model_providers.<id>.env_key` | string | Env var for API key. | | `model_providers.<id>.env_key` | string | Env var for API key. |