Skip to content

Commit 84da181

Browse files
authored
[MCP] Add configuration options to enable or disable specific tools (openai#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 openai#4796
1 parent 42c57dc commit 84da181

File tree

8 files changed

+361
-81
lines changed

8 files changed

+361
-81
lines changed

codex-rs/cli/src/mcp_cmd.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
253253
enabled: true,
254254
startup_timeout_sec: None,
255255
tool_timeout_sec: None,
256+
enabled_tools: None,
257+
disabled_tools: None,
256258
};
257259

258260
servers.insert(name.clone(), new_entry);
@@ -676,6 +678,8 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
676678
"name": get_args.name,
677679
"enabled": server.enabled,
678680
"transport": transport,
681+
"enabled_tools": server.enabled_tools.clone(),
682+
"disabled_tools": server.disabled_tools.clone(),
679683
"startup_timeout_sec": server
680684
.startup_timeout_sec
681685
.map(|timeout| timeout.as_secs_f64()),
@@ -687,8 +691,28 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
687691
return Ok(());
688692
}
689693

694+
if !server.enabled {
695+
println!("{} (disabled)", get_args.name);
696+
return Ok(());
697+
}
698+
690699
println!("{}", get_args.name);
691700
println!(" enabled: {}", server.enabled);
701+
let format_tool_list = |tools: &Option<Vec<String>>| -> String {
702+
match tools {
703+
Some(list) if list.is_empty() => "[]".to_string(),
704+
Some(list) => list.join(", "),
705+
None => "-".to_string(),
706+
}
707+
};
708+
if server.enabled_tools.is_some() {
709+
let enabled_tools_display = format_tool_list(&server.enabled_tools);
710+
println!(" enabled_tools: {enabled_tools_display}");
711+
}
712+
if server.disabled_tools.is_some() {
713+
let disabled_tools_display = format_tool_list(&server.disabled_tools);
714+
println!(" disabled_tools: {disabled_tools_display}");
715+
}
692716
match &server.transport {
693717
McpServerTransportConfig::Stdio {
694718
command,

codex-rs/cli/tests/mcp_list.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,28 @@ async fn list_and_get_render_expected_output() -> Result<()> {
134134

135135
Ok(())
136136
}
137+
138+
#[tokio::test]
139+
async fn get_disabled_server_shows_single_line() -> Result<()> {
140+
let codex_home = TempDir::new()?;
141+
142+
let mut add = codex_command(codex_home.path())?;
143+
add.args(["mcp", "add", "docs", "--", "docs-server"])
144+
.assert()
145+
.success();
146+
147+
let mut servers = load_global_mcp_servers(codex_home.path()).await?;
148+
let docs = servers
149+
.get_mut("docs")
150+
.expect("docs server should exist after add");
151+
docs.enabled = false;
152+
write_global_mcp_servers(codex_home.path(), &servers)?;
153+
154+
let mut get_cmd = codex_command(codex_home.path())?;
155+
let get_output = get_cmd.args(["mcp", "get", "docs"]).output()?;
156+
assert!(get_output.status.success());
157+
let stdout = String::from_utf8(get_output.stdout)?;
158+
assert_eq!(stdout.trim_end(), "docs (disabled)");
159+
160+
Ok(())
161+
}

codex-rs/core/src/config.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,16 @@ pub fn write_global_mcp_servers(
484484
entry["tool_timeout_sec"] = toml_edit::value(timeout.as_secs_f64());
485485
}
486486

487+
if let Some(enabled_tools) = &config.enabled_tools {
488+
entry["enabled_tools"] =
489+
TomlItem::Value(enabled_tools.iter().collect::<TomlArray>().into());
490+
}
491+
492+
if let Some(disabled_tools) = &config.disabled_tools {
493+
entry["disabled_tools"] =
494+
TomlItem::Value(disabled_tools.iter().collect::<TomlArray>().into());
495+
}
496+
487497
doc["mcp_servers"][name.as_str()] = TomlItem::Table(entry);
488498
}
489499
}
@@ -1923,6 +1933,8 @@ approve_all = true
19231933
enabled: true,
19241934
startup_timeout_sec: Some(Duration::from_secs(3)),
19251935
tool_timeout_sec: Some(Duration::from_secs(5)),
1936+
enabled_tools: None,
1937+
disabled_tools: None,
19261938
},
19271939
);
19281940

@@ -2059,6 +2071,8 @@ bearer_token = "secret"
20592071
enabled: true,
20602072
startup_timeout_sec: None,
20612073
tool_timeout_sec: None,
2074+
enabled_tools: None,
2075+
disabled_tools: None,
20622076
},
20632077
)]);
20642078

@@ -2121,6 +2135,8 @@ ZIG_VAR = "3"
21212135
enabled: true,
21222136
startup_timeout_sec: None,
21232137
tool_timeout_sec: None,
2138+
enabled_tools: None,
2139+
disabled_tools: None,
21242140
},
21252141
)]);
21262142

@@ -2163,6 +2179,8 @@ ZIG_VAR = "3"
21632179
enabled: true,
21642180
startup_timeout_sec: None,
21652181
tool_timeout_sec: None,
2182+
enabled_tools: None,
2183+
disabled_tools: None,
21662184
},
21672185
)]);
21682186

@@ -2204,6 +2222,8 @@ ZIG_VAR = "3"
22042222
enabled: true,
22052223
startup_timeout_sec: Some(Duration::from_secs(2)),
22062224
tool_timeout_sec: None,
2225+
enabled_tools: None,
2226+
disabled_tools: None,
22072227
},
22082228
)]);
22092229

@@ -2261,6 +2281,8 @@ startup_timeout_sec = 2.0
22612281
enabled: true,
22622282
startup_timeout_sec: Some(Duration::from_secs(2)),
22632283
tool_timeout_sec: None,
2284+
enabled_tools: None,
2285+
disabled_tools: None,
22642286
},
22652287
)]);
22662288
write_global_mcp_servers(codex_home.path(), &servers)?;
@@ -2330,6 +2352,8 @@ X-Auth = "DOCS_AUTH"
23302352
enabled: true,
23312353
startup_timeout_sec: Some(Duration::from_secs(2)),
23322354
tool_timeout_sec: None,
2355+
enabled_tools: None,
2356+
disabled_tools: None,
23332357
},
23342358
)]);
23352359

@@ -2351,6 +2375,8 @@ X-Auth = "DOCS_AUTH"
23512375
enabled: true,
23522376
startup_timeout_sec: None,
23532377
tool_timeout_sec: None,
2378+
enabled_tools: None,
2379+
disabled_tools: None,
23542380
},
23552381
);
23562382
write_global_mcp_servers(codex_home.path(), &servers)?;
@@ -2410,6 +2436,8 @@ url = "https://example.com/mcp"
24102436
enabled: true,
24112437
startup_timeout_sec: Some(Duration::from_secs(2)),
24122438
tool_timeout_sec: None,
2439+
enabled_tools: None,
2440+
disabled_tools: None,
24132441
},
24142442
),
24152443
(
@@ -2425,6 +2453,8 @@ url = "https://example.com/mcp"
24252453
enabled: true,
24262454
startup_timeout_sec: None,
24272455
tool_timeout_sec: None,
2456+
enabled_tools: None,
2457+
disabled_tools: None,
24282458
},
24292459
),
24302460
]);
@@ -2499,6 +2529,8 @@ url = "https://example.com/mcp"
24992529
enabled: false,
25002530
startup_timeout_sec: None,
25012531
tool_timeout_sec: None,
2532+
enabled_tools: None,
2533+
disabled_tools: None,
25022534
},
25032535
)]);
25042536

@@ -2518,6 +2550,49 @@ url = "https://example.com/mcp"
25182550
Ok(())
25192551
}
25202552

2553+
#[tokio::test]
2554+
async fn write_global_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
2555+
let codex_home = TempDir::new()?;
2556+
2557+
let servers = BTreeMap::from([(
2558+
"docs".to_string(),
2559+
McpServerConfig {
2560+
transport: McpServerTransportConfig::Stdio {
2561+
command: "docs-server".to_string(),
2562+
args: Vec::new(),
2563+
env: None,
2564+
env_vars: Vec::new(),
2565+
cwd: None,
2566+
},
2567+
enabled: true,
2568+
startup_timeout_sec: None,
2569+
tool_timeout_sec: None,
2570+
enabled_tools: Some(vec!["allowed".to_string()]),
2571+
disabled_tools: Some(vec!["blocked".to_string()]),
2572+
},
2573+
)]);
2574+
2575+
write_global_mcp_servers(codex_home.path(), &servers)?;
2576+
2577+
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
2578+
let serialized = std::fs::read_to_string(&config_path)?;
2579+
assert!(serialized.contains(r#"enabled_tools = ["allowed"]"#));
2580+
assert!(serialized.contains(r#"disabled_tools = ["blocked"]"#));
2581+
2582+
let loaded = load_global_mcp_servers(codex_home.path()).await?;
2583+
let docs = loaded.get("docs").expect("docs entry");
2584+
assert_eq!(
2585+
docs.enabled_tools.as_ref(),
2586+
Some(&vec!["allowed".to_string()])
2587+
);
2588+
assert_eq!(
2589+
docs.disabled_tools.as_ref(),
2590+
Some(&vec!["blocked".to_string()])
2591+
);
2592+
2593+
Ok(())
2594+
}
2595+
25212596
#[tokio::test]
25222597
async fn persist_model_selection_updates_defaults() -> anyhow::Result<()> {
25232598
let codex_home = TempDir::new()?;

0 commit comments

Comments
 (0)