Skip to content

Commit d3820f4

Browse files
authored
[MCP] Add an enabled config field (#4917)
This lets users more easily toggle MCP servers.
1 parent e896db1 commit d3820f4

File tree

9 files changed

+155
-14
lines changed

9 files changed

+155
-14
lines changed

codex-rs/cli/src/mcp_cmd.rs

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
234234

235235
let new_entry = McpServerConfig {
236236
transport,
237+
enabled: true,
237238
startup_timeout_sec: None,
238239
tool_timeout_sec: None,
239240
};
@@ -365,6 +366,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
365366

366367
serde_json::json!({
367368
"name": name,
369+
"enabled": cfg.enabled,
368370
"transport": transport,
369371
"startup_timeout_sec": cfg
370372
.startup_timeout_sec
@@ -385,8 +387,8 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
385387
return Ok(());
386388
}
387389

388-
let mut stdio_rows: Vec<[String; 4]> = Vec::new();
389-
let mut http_rows: Vec<[String; 3]> = Vec::new();
390+
let mut stdio_rows: Vec<[String; 5]> = Vec::new();
391+
let mut http_rows: Vec<[String; 4]> = Vec::new();
390392

391393
for (name, cfg) in entries {
392394
match &cfg.transport {
@@ -409,52 +411,79 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
409411
.join(", ")
410412
}
411413
};
412-
stdio_rows.push([name.clone(), command.clone(), args_display, env_display]);
414+
let status = if cfg.enabled {
415+
"enabled".to_string()
416+
} else {
417+
"disabled".to_string()
418+
};
419+
stdio_rows.push([
420+
name.clone(),
421+
command.clone(),
422+
args_display,
423+
env_display,
424+
status,
425+
]);
413426
}
414427
McpServerTransportConfig::StreamableHttp {
415428
url,
416429
bearer_token_env_var,
417430
} => {
431+
let status = if cfg.enabled {
432+
"enabled".to_string()
433+
} else {
434+
"disabled".to_string()
435+
};
418436
http_rows.push([
419437
name.clone(),
420438
url.clone(),
421439
bearer_token_env_var.clone().unwrap_or("-".to_string()),
440+
status,
422441
]);
423442
}
424443
}
425444
}
426445

427446
if !stdio_rows.is_empty() {
428-
let mut widths = ["Name".len(), "Command".len(), "Args".len(), "Env".len()];
447+
let mut widths = [
448+
"Name".len(),
449+
"Command".len(),
450+
"Args".len(),
451+
"Env".len(),
452+
"Status".len(),
453+
];
429454
for row in &stdio_rows {
430455
for (i, cell) in row.iter().enumerate() {
431456
widths[i] = widths[i].max(cell.len());
432457
}
433458
}
434459

435460
println!(
436-
"{:<name_w$} {:<cmd_w$} {:<args_w$} {:<env_w$}",
461+
"{:<name_w$} {:<cmd_w$} {:<args_w$} {:<env_w$} {:<status_w$}",
437462
"Name",
438463
"Command",
439464
"Args",
440465
"Env",
466+
"Status",
441467
name_w = widths[0],
442468
cmd_w = widths[1],
443469
args_w = widths[2],
444470
env_w = widths[3],
471+
status_w = widths[4],
445472
);
446473

447474
for row in &stdio_rows {
448475
println!(
449-
"{:<name_w$} {:<cmd_w$} {:<args_w$} {:<env_w$}",
476+
"{:<name_w$} {:<cmd_w$} {:<args_w$} {:<env_w$} {:<status_w$}",
450477
row[0],
451478
row[1],
452479
row[2],
453480
row[3],
481+
row[4],
454482
name_w = widths[0],
455483
cmd_w = widths[1],
456484
args_w = widths[2],
457485
env_w = widths[3],
486+
status_w = widths[4],
458487
);
459488
}
460489
}
@@ -464,32 +493,41 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
464493
}
465494

466495
if !http_rows.is_empty() {
467-
let mut widths = ["Name".len(), "Url".len(), "Bearer Token Env Var".len()];
496+
let mut widths = [
497+
"Name".len(),
498+
"Url".len(),
499+
"Bearer Token Env Var".len(),
500+
"Status".len(),
501+
];
468502
for row in &http_rows {
469503
for (i, cell) in row.iter().enumerate() {
470504
widths[i] = widths[i].max(cell.len());
471505
}
472506
}
473507

474508
println!(
475-
"{:<name_w$} {:<url_w$} {:<token_w$}",
509+
"{:<name_w$} {:<url_w$} {:<token_w$} {:<status_w$}",
476510
"Name",
477511
"Url",
478512
"Bearer Token Env Var",
513+
"Status",
479514
name_w = widths[0],
480515
url_w = widths[1],
481516
token_w = widths[2],
517+
status_w = widths[3],
482518
);
483519

484520
for row in &http_rows {
485521
println!(
486-
"{:<name_w$} {:<url_w$} {:<token_w$}",
522+
"{:<name_w$} {:<url_w$} {:<token_w$} {:<status_w$}",
487523
row[0],
488524
row[1],
489525
row[2],
526+
row[3],
490527
name_w = widths[0],
491528
url_w = widths[1],
492529
token_w = widths[2],
530+
status_w = widths[3],
493531
);
494532
}
495533
}
@@ -526,6 +564,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
526564
};
527565
let output = serde_json::to_string_pretty(&serde_json::json!({
528566
"name": get_args.name,
567+
"enabled": server.enabled,
529568
"transport": transport,
530569
"startup_timeout_sec": server
531570
.startup_timeout_sec
@@ -539,6 +578,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
539578
}
540579

541580
println!("{}", get_args.name);
581+
println!(" enabled: {}", server.enabled);
542582
match &server.transport {
543583
McpServerTransportConfig::Stdio { command, args, env } => {
544584
println!(" transport: stdio");

codex-rs/cli/tests/mcp_add_remove.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ async fn add_and_remove_server_updates_global_config() -> Result<()> {
3535
}
3636
other => panic!("unexpected transport: {other:?}"),
3737
}
38+
assert!(docs.enabled);
3839

3940
let mut remove_cmd = codex_command(codex_home.path())?;
4041
remove_cmd
@@ -90,6 +91,7 @@ async fn add_with_env_preserves_key_order_and_values() -> Result<()> {
9091
assert_eq!(env.len(), 2);
9192
assert_eq!(env.get("FOO"), Some(&"bar".to_string()));
9293
assert_eq!(env.get("ALPHA"), Some(&"beta".to_string()));
94+
assert!(envy.enabled);
9395

9496
Ok(())
9597
}
@@ -116,6 +118,7 @@ async fn add_streamable_http_without_manual_token() -> Result<()> {
116118
}
117119
other => panic!("unexpected transport: {other:?}"),
118120
}
121+
assert!(github.enabled);
119122

120123
assert!(!codex_home.path().join(".credentials.json").exists());
121124
assert!(!codex_home.path().join(".env").exists());
@@ -153,6 +156,7 @@ async fn add_streamable_http_with_custom_env_var() -> Result<()> {
153156
}
154157
other => panic!("unexpected transport: {other:?}"),
155158
}
159+
assert!(issues.enabled);
156160
Ok(())
157161
}
158162

codex-rs/cli/tests/mcp_list.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::path::Path;
22

33
use anyhow::Result;
4+
use predicates::prelude::PredicateBooleanExt;
45
use predicates::str::contains;
56
use pretty_assertions::assert_eq;
67
use serde_json::Value as JsonValue;
@@ -53,6 +54,8 @@ fn list_and_get_render_expected_output() -> Result<()> {
5354
assert!(stdout.contains("docs"));
5455
assert!(stdout.contains("docs-server"));
5556
assert!(stdout.contains("TOKEN=secret"));
57+
assert!(stdout.contains("Status"));
58+
assert!(stdout.contains("enabled"));
5659

5760
let mut list_json_cmd = codex_command(codex_home.path())?;
5861
let json_output = list_json_cmd.args(["mcp", "list", "--json"]).output()?;
@@ -64,6 +67,7 @@ fn list_and_get_render_expected_output() -> Result<()> {
6467
json!([
6568
{
6669
"name": "docs",
70+
"enabled": true,
6771
"transport": {
6872
"type": "stdio",
6973
"command": "docs-server",
@@ -91,14 +95,15 @@ fn list_and_get_render_expected_output() -> Result<()> {
9195
assert!(stdout.contains("command: docs-server"));
9296
assert!(stdout.contains("args: --port 4000"));
9397
assert!(stdout.contains("env: TOKEN=secret"));
98+
assert!(stdout.contains("enabled: true"));
9499
assert!(stdout.contains("remove: codex mcp remove docs"));
95100

96101
let mut get_json_cmd = codex_command(codex_home.path())?;
97102
get_json_cmd
98103
.args(["mcp", "get", "docs", "--json"])
99104
.assert()
100105
.success()
101-
.stdout(contains("\"name\": \"docs\""));
106+
.stdout(contains("\"name\": \"docs\"").and(contains("\"enabled\": true")));
102107

103108
Ok(())
104109
}

codex-rs/core/src/config.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,10 @@ pub fn write_global_mcp_servers(
401401
}
402402
}
403403

404+
if !config.enabled {
405+
entry["enabled"] = toml_edit::value(false);
406+
}
407+
404408
if let Some(timeout) = config.startup_timeout_sec {
405409
entry["startup_timeout_sec"] = toml_edit::value(timeout.as_secs_f64());
406410
}
@@ -1515,6 +1519,7 @@ exclude_slash_tmp = true
15151519
args: vec!["hello".to_string()],
15161520
env: None,
15171521
},
1522+
enabled: true,
15181523
startup_timeout_sec: Some(Duration::from_secs(3)),
15191524
tool_timeout_sec: Some(Duration::from_secs(5)),
15201525
},
@@ -1535,6 +1540,7 @@ exclude_slash_tmp = true
15351540
}
15361541
assert_eq!(docs.startup_timeout_sec, Some(Duration::from_secs(3)));
15371542
assert_eq!(docs.tool_timeout_sec, Some(Duration::from_secs(5)));
1543+
assert!(docs.enabled);
15381544

15391545
let empty = BTreeMap::new();
15401546
write_global_mcp_servers(codex_home.path(), &empty)?;
@@ -1639,6 +1645,7 @@ bearer_token = "secret"
16391645
("ALPHA_VAR".to_string(), "1".to_string()),
16401646
])),
16411647
},
1648+
enabled: true,
16421649
startup_timeout_sec: None,
16431650
tool_timeout_sec: None,
16441651
},
@@ -1689,6 +1696,7 @@ ZIG_VAR = "3"
16891696
url: "https://example.com/mcp".to_string(),
16901697
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
16911698
},
1699+
enabled: true,
16921700
startup_timeout_sec: Some(Duration::from_secs(2)),
16931701
tool_timeout_sec: None,
16941702
},
@@ -1728,6 +1736,7 @@ startup_timeout_sec = 2.0
17281736
url: "https://example.com/mcp".to_string(),
17291737
bearer_token_env_var: None,
17301738
},
1739+
enabled: true,
17311740
startup_timeout_sec: None,
17321741
tool_timeout_sec: None,
17331742
},
@@ -1758,6 +1767,40 @@ url = "https://example.com/mcp"
17581767
Ok(())
17591768
}
17601769

1770+
#[tokio::test]
1771+
async fn write_global_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
1772+
let codex_home = TempDir::new()?;
1773+
1774+
let servers = BTreeMap::from([(
1775+
"docs".to_string(),
1776+
McpServerConfig {
1777+
transport: McpServerTransportConfig::Stdio {
1778+
command: "docs-server".to_string(),
1779+
args: Vec::new(),
1780+
env: None,
1781+
},
1782+
enabled: false,
1783+
startup_timeout_sec: None,
1784+
tool_timeout_sec: None,
1785+
},
1786+
)]);
1787+
1788+
write_global_mcp_servers(codex_home.path(), &servers)?;
1789+
1790+
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
1791+
let serialized = std::fs::read_to_string(&config_path)?;
1792+
assert!(
1793+
serialized.contains("enabled = false"),
1794+
"serialized config missing disabled flag:\n{serialized}"
1795+
);
1796+
1797+
let loaded = load_global_mcp_servers(codex_home.path()).await?;
1798+
let docs = loaded.get("docs").expect("docs entry");
1799+
assert!(!docs.enabled);
1800+
1801+
Ok(())
1802+
}
1803+
17611804
#[tokio::test]
17621805
async fn persist_model_selection_updates_defaults() -> anyhow::Result<()> {
17631806
let codex_home = TempDir::new()?;

0 commit comments

Comments
 (0)