From 740b4a95f44aa0a2671d0195d45a8c7185cd9b21 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Mon, 20 Oct 2025 15:35:36 -0700 Subject: [PATCH] [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 --- codex-rs/cli/src/mcp_cmd.rs | 24 ++++ codex-rs/cli/tests/mcp_list.rs | 25 ++++ codex-rs/core/src/config.rs | 75 ++++++++++ codex-rs/core/src/config_types.rs | 129 +++++++++--------- codex-rs/core/src/mcp_connection_manager.rs | 143 +++++++++++++++++++- codex-rs/core/tests/suite/rmcp_client.rs | 8 ++ codex-rs/tui/src/history_cell.rs | 30 ++-- docs/config.md | 8 ++ 8 files changed, 361 insertions(+), 81 deletions(-) diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 50274f3e..3a4d0358 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -253,6 +253,8 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re enabled: true, startup_timeout_sec: None, tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }; 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, "enabled": server.enabled, "transport": transport, + "enabled_tools": server.enabled_tools.clone(), + "disabled_tools": server.disabled_tools.clone(), "startup_timeout_sec": server .startup_timeout_sec .map(|timeout| timeout.as_secs_f64()), @@ -687,8 +691,28 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re return Ok(()); } + if !server.enabled { + println!("{} (disabled)", get_args.name); + return Ok(()); + } + println!("{}", get_args.name); println!(" enabled: {}", server.enabled); + let format_tool_list = |tools: &Option>| -> 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 { McpServerTransportConfig::Stdio { command, diff --git a/codex-rs/cli/tests/mcp_list.rs b/codex-rs/cli/tests/mcp_list.rs index ea0d6fc1..6440660c 100644 --- a/codex-rs/cli/tests/mcp_list.rs +++ b/codex-rs/cli/tests/mcp_list.rs @@ -134,3 +134,28 @@ async fn list_and_get_render_expected_output() -> Result<()> { 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(()) +} diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 93d0ad4b..fb974045 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -484,6 +484,16 @@ pub fn write_global_mcp_servers( 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::().into()); + } + + if let Some(disabled_tools) = &config.disabled_tools { + entry["disabled_tools"] = + TomlItem::Value(disabled_tools.iter().collect::().into()); + } + doc["mcp_servers"][name.as_str()] = TomlItem::Table(entry); } } @@ -1923,6 +1933,8 @@ approve_all = true enabled: true, startup_timeout_sec: Some(Duration::from_secs(3)), tool_timeout_sec: Some(Duration::from_secs(5)), + enabled_tools: None, + disabled_tools: None, }, ); @@ -2059,6 +2071,8 @@ bearer_token = "secret" enabled: true, startup_timeout_sec: None, tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, )]); @@ -2121,6 +2135,8 @@ ZIG_VAR = "3" enabled: true, startup_timeout_sec: None, tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, )]); @@ -2163,6 +2179,8 @@ ZIG_VAR = "3" enabled: true, startup_timeout_sec: None, tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, )]); @@ -2204,6 +2222,8 @@ ZIG_VAR = "3" enabled: true, startup_timeout_sec: Some(Duration::from_secs(2)), tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, )]); @@ -2261,6 +2281,8 @@ startup_timeout_sec = 2.0 enabled: true, startup_timeout_sec: Some(Duration::from_secs(2)), tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, )]); write_global_mcp_servers(codex_home.path(), &servers)?; @@ -2330,6 +2352,8 @@ X-Auth = "DOCS_AUTH" enabled: true, startup_timeout_sec: Some(Duration::from_secs(2)), tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, )]); @@ -2351,6 +2375,8 @@ X-Auth = "DOCS_AUTH" enabled: true, startup_timeout_sec: None, tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, ); write_global_mcp_servers(codex_home.path(), &servers)?; @@ -2410,6 +2436,8 @@ url = "https://example.com/mcp" enabled: true, startup_timeout_sec: Some(Duration::from_secs(2)), tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, ), ( @@ -2425,6 +2453,8 @@ url = "https://example.com/mcp" enabled: true, startup_timeout_sec: None, tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, ), ]); @@ -2499,6 +2529,8 @@ url = "https://example.com/mcp" enabled: false, startup_timeout_sec: None, tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, )]); @@ -2518,6 +2550,49 @@ url = "https://example.com/mcp" 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] async fn persist_model_selection_updates_defaults() -> anyhow::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index 3da61086..2f4be2b6 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -35,6 +35,14 @@ pub struct McpServerConfig { /// Default timeout for MCP tool calls initiated via this server. #[serde(default, with = "option_duration_secs")] pub tool_timeout_sec: Option, + + /// 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>, + + /// 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>, } impl<'de> Deserialize<'de> for McpServerConfig { @@ -42,7 +50,7 @@ impl<'de> Deserialize<'de> for McpServerConfig { where D: Deserializer<'de>, { - #[derive(Deserialize)] + #[derive(Deserialize, Clone)] struct RawMcpServerConfig { // stdio command: Option, @@ -72,9 +80,13 @@ impl<'de> Deserialize<'de> for McpServerConfig { tool_timeout_sec: Option, #[serde(default)] enabled: Option, + #[serde(default)] + enabled_tools: Option>, + #[serde(default)] + disabled_tools: Option>, } - let raw = RawMcpServerConfig::deserialize(deserializer)?; + let mut raw = RawMcpServerConfig::deserialize(deserializer)?; let startup_timeout_sec = match (raw.startup_timeout_sec, raw.startup_timeout_ms) { (Some(sec), _) => { @@ -84,6 +96,10 @@ impl<'de> Deserialize<'de> for McpServerConfig { (None, Some(ms)) => Some(Duration::from_millis(ms)), (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(transport: &str, field: &str, value: Option<&T>) -> Result<(), E> where @@ -97,72 +113,46 @@ impl<'de> Deserialize<'de> for McpServerConfig { ))) } - let transport = match raw { - RawMcpServerConfig { - command: Some(command), - args, - env, - env_vars, - cwd, - url, - bearer_token_env_var, - http_headers, - env_http_headers, - .. - } => { - 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, + let transport = if let Some(command) = raw.command.clone() { + throw_if_set("stdio", "url", raw.url.as_ref())?; + throw_if_set( + "stdio", + "bearer_token_env_var", + raw.bearer_token_env_var.as_ref(), + )?; + throw_if_set("stdio", "bearer_token", raw.bearer_token.as_ref())?; + throw_if_set("stdio", "http_headers", raw.http_headers.as_ref())?; + throw_if_set("stdio", "env_http_headers", raw.env_http_headers.as_ref())?; + McpServerTransportConfig::Stdio { command, - args, - env, - env_vars, - cwd, - 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, - } + args: raw.args.clone().unwrap_or_default(), + env: raw.env.clone(), + env_vars: raw.env_vars.clone().unwrap_or_default(), + cwd: raw.cwd.take(), } - _ => 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 { transport, startup_timeout_sec, - tool_timeout_sec: raw.tool_timeout_sec, - enabled: raw.enabled.unwrap_or_else(default_enabled), + tool_timeout_sec, + enabled, + enabled_tools, + disabled_tools, }) } } @@ -527,6 +517,8 @@ mod tests { } ); assert!(cfg.enabled); + assert!(cfg.enabled_tools.is_none()); + assert!(cfg.disabled_tools.is_none()); } #[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] fn deserialize_rejects_command_and_url() { toml::from_str::( diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 9ea63442..edf612c4 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -237,6 +237,9 @@ pub(crate) struct McpConnectionManager { /// Fully qualified tool name -> tool instance. tools: HashMap, + + /// Server-name -> configured tool filters. + tool_filters: HashMap, } impl McpConnectionManager { @@ -261,6 +264,7 @@ impl McpConnectionManager { // Launch all configured servers concurrently. let mut join_set = JoinSet::new(); let mut errors = ClientStartErrors::new(); + let mut tool_filters: HashMap = HashMap::new(); for (server_name, cfg) in mcp_servers { // Validate server name before spawning @@ -273,11 +277,13 @@ impl McpConnectionManager { } if !cfg.enabled { + tool_filters.insert(server_name, ToolFilter::from_config(&cfg)); continue; } let startup_timeout = cfg.startup_timeout_sec.unwrap_or(DEFAULT_STARTUP_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 { 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 @@ -541,6 +555,13 @@ impl McpConnectionManager { tool: &str, arguments: Option, ) -> Result { + 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 .clients .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>, + disabled: HashSet, +} + +impl ToolFilter { + fn from_config(cfg: &McpServerConfig) -> Self { + let enabled = cfg + .enabled_tools + .as_ref() + .map(|tools| tools.iter().cloned().collect::>()); + let disabled = cfg + .disabled_tools + .as_ref() + .map(|tools| tools.iter().cloned().collect::>()) + .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, filters: &HashMap) -> Vec { + tools + .into_iter() + .filter(|tool| { + filters + .get(&tool.server_name) + .is_none_or(|filter| filter.allows(&tool.tool_name)) + }) + .collect() +} + fn resolve_bearer_token( server_name: &str, bearer_token_env_var: Option<&str>, @@ -711,6 +778,7 @@ fn is_valid_mcp_server_name(server_name: &str) -> bool { mod tests { use super::*; use mcp_types::ToolInputSchema; + use std::collections::HashSet; fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo { ToolInfo { @@ -793,4 +861,75 @@ mod tests { "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"); + } } diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 609ae871..99c863b8 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -94,6 +94,8 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { enabled: true, startup_timeout_sec: Some(Duration::from_secs(10)), 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, startup_timeout_sec: Some(Duration::from_secs(10)), 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, startup_timeout_sec: Some(Duration::from_secs(10)), 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, startup_timeout_sec: Some(Duration::from_secs(10)), tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, }, ); }) diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 6bae1bd0..fe85fb87 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1056,18 +1056,21 @@ pub(crate) fn new_mcp_tools_output( .collect(); names.sort(); - let status = auth_statuses + let auth_status = auth_statuses .get(server.as_str()) .copied() .unwrap_or(McpAuthStatus::Unsupported); - lines.push(vec![" • Server: ".into(), server.clone().into()].into()); - let status_line = if cfg.enabled { - vec![" • Status: ".into(), "enabled".green()].into() - } else { - vec![" • Status: ".into(), "disabled".red()].into() - }; - lines.push(status_line); - lines.push(vec![" • Auth: ".into(), status.to_string().into()].into()); + let mut header: Vec> = vec![" • ".into(), server.clone().into()]; + if !cfg.enabled { + header.push(" ".into()); + header.push("(disabled)".red()); + lines.push(header.into()); + lines.push(Line::from("")); + continue; + } + 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 { 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() { lines.push(" • Tools: (none)".into()); } else { diff --git a/docs/config.md b/docs/config.md index 8d495214..b92342b2 100644 --- a/docs/config.md +++ b/docs/config.md @@ -441,8 +441,14 @@ startup_timeout_sec = 20 tool_timeout_sec = 30 # Optional: disable a server without removing it 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 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..enabled` | boolean | When false, Codex skips starting the server (default: true). | | `mcp_servers..startup_timeout_sec` | number | Startup timeout in seconds (default: 10). Timeout is applied both for initializing MCP server and initially listing tools. | | `mcp_servers..tool_timeout_sec` | number | Per-tool timeout in seconds (default: 60). Accepts fractional values; omit to use the default. | +| `mcp_servers..enabled_tools` | array | Restrict the server to the listed tool names. | +| `mcp_servers..disabled_tools` | array | Remove the listed tool names after applying `enabled_tools`, if any. | | `model_providers..name` | string | Display name. | | `model_providers..base_url` | string | API base URL. | | `model_providers..env_key` | string | Env var for API key. |