Repository navigation
[MCP] Allow specifying cwd and additional env vars #5246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| use std::path::Path; | ||
|
|
||
| use anyhow::Result; | ||
| use codex_core::config::load_global_mcp_servers; | ||
| use codex_core::config::write_global_mcp_servers; | ||
| use codex_core::config_types::McpServerTransportConfig; | ||
| use predicates::prelude::PredicateBooleanExt; | ||
| use predicates::str::contains; | ||
| use pretty_assertions::assert_eq; | ||
|
|
@@ -46,6 +49,20 @@ fn list_and_get_render_expected_output() -> Result<()> { | |
| .assert() | ||
| .success(); | ||
|
|
||
| let runtime = tokio::runtime::Runtime::new()?; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems unusual: is there a reason why you need to introduce a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bad Codex. Thanks 🙏 |
||
| let mut servers = runtime.block_on(load_global_mcp_servers(codex_home.path()))?; | ||
| drop(runtime); | ||
| let docs_entry = servers | ||
| .get_mut("docs") | ||
| .expect("docs server should exist after add"); | ||
| match &mut docs_entry.transport { | ||
| McpServerTransportConfig::Stdio { env_vars, .. } => { | ||
| *env_vars = vec!["APP_TOKEN".to_string(), "WORKSPACE_ID".to_string()]; | ||
| } | ||
| other => panic!("unexpected transport: {other:?}"), | ||
| } | ||
| write_global_mcp_servers(codex_home.path(), &servers)?; | ||
|
|
||
| let mut list_cmd = codex_command(codex_home.path())?; | ||
| let list_output = list_cmd.args(["mcp", "list"]).output()?; | ||
| assert!(list_output.status.success()); | ||
|
|
@@ -54,6 +71,8 @@ fn list_and_get_render_expected_output() -> Result<()> { | |
| assert!(stdout.contains("docs")); | ||
| assert!(stdout.contains("docs-server")); | ||
| assert!(stdout.contains("TOKEN=secret")); | ||
| assert!(stdout.contains("APP_TOKEN=$APP_TOKEN")); | ||
| assert!(stdout.contains("WORKSPACE_ID=$WORKSPACE_ID")); | ||
| assert!(stdout.contains("Status")); | ||
| assert!(stdout.contains("Auth")); | ||
| assert!(stdout.contains("enabled")); | ||
|
|
@@ -79,7 +98,12 @@ fn list_and_get_render_expected_output() -> Result<()> { | |
| ], | ||
| "env": { | ||
| "TOKEN": "secret" | ||
| } | ||
| }, | ||
| "env_vars": [ | ||
| "APP_TOKEN", | ||
| "WORKSPACE_ID" | ||
| ], | ||
| "cwd": null | ||
| }, | ||
| "startup_timeout_sec": null, | ||
| "tool_timeout_sec": null, | ||
|
|
@@ -98,6 +122,8 @@ fn list_and_get_render_expected_output() -> Result<()> { | |
| assert!(stdout.contains("command: docs-server")); | ||
| assert!(stdout.contains("args: --port 4000")); | ||
| assert!(stdout.contains("env: TOKEN=secret")); | ||
| assert!(stdout.contains("APP_TOKEN=$APP_TOKEN")); | ||
| assert!(stdout.contains("WORKSPACE_ID=$WORKSPACE_ID")); | ||
| assert!(stdout.contains("enabled: true")); | ||
| assert!(stdout.contains("remove: codex mcp remove docs")); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| use std::collections::HashMap; | ||
|
|
||
| pub fn format_env_display(env: Option<&HashMap<String, String>>, env_vars: &[String]) -> String { | ||
| let mut parts: Vec<String> = Vec::new(); | ||
|
|
||
| if let Some(map) = env { | ||
| let mut pairs: Vec<_> = map.iter().collect(); | ||
| pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); | ||
| parts.extend( | ||
| pairs | ||
| .into_iter() | ||
| .map(|(key, value)| format!("{key}={value}")), | ||
| ); | ||
| } | ||
|
|
||
| if !env_vars.is_empty() { | ||
| parts.extend(env_vars.iter().map(|var| format!("{var}=${var}"))); | ||
| } | ||
|
|
||
| if parts.is_empty() { | ||
| "-".to_string() | ||
| } else { | ||
| parts.join(", ") | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn returns_dash_when_empty() { | ||
| assert_eq!(format_env_display(None, &[]), "-"); | ||
|
|
||
| let empty_map = HashMap::new(); | ||
| assert_eq!(format_env_display(Some(&empty_map), &[]), "-"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn formats_sorted_env_pairs() { | ||
| let mut env = HashMap::new(); | ||
| env.insert("B".to_string(), "two".to_string()); | ||
| env.insert("A".to_string(), "one".to_string()); | ||
|
|
||
| assert_eq!(format_env_display(Some(&env), &[]), "A=one, B=two"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn formats_env_vars_with_dollar_prefix() { | ||
| let vars = vec!["TOKEN".to_string(), "PATH".to_string()]; | ||
|
|
||
| assert_eq!(format_env_display(None, &vars), "TOKEN=$TOKEN, PATH=$PATH"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn combines_env_pairs_and_vars() { | ||
| let mut env = HashMap::new(); | ||
| env.insert("HOME".to_string(), "/tmp".to_string()); | ||
| let vars = vec!["TOKEN".to_string()]; | ||
|
|
||
| assert_eq!( | ||
| format_env_display(Some(&env), &vars), | ||
| "HOME=/tmp, TOKEN=$TOKEN" | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why aren't we just using serde for this again? Because this is UI, I guess?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm just adding a couple of fields and didn't want to expand the scope of this PR to refactor all of the code around it since this PR is big enough already.