[5/6] Wire executor-backed MCP stdio (#18212)

## Summary
- Add the executor-backed RMCP stdio transport.
- Wire MCP stdio placement through the executor environment config.
- Cover local and executor-backed stdio paths with the existing MCP test
helpers.

## Stack
```text
o  #18027 [6/6] Fail exec client operations after disconnect
│
@  #18212 [5/6] Wire executor-backed MCP stdio
│
o  #18087 [4/6] Abstract MCP stdio server launching
│
o  #18020 [3/6] Add pushed exec process events
│
o  #18086 [2/6] Support piped stdin in exec process API
│
o  #18085 [1/6] Add MCP server environment config
│
o  main
```

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-04-18 21:47:43 -07:00
committed by GitHub
co-authored by Codex
parent e3f44ca3b3
commit 996aa23e4c
31 changed files with 1815 additions and 76 deletions
+59 -2
View File
@@ -32,6 +32,7 @@ use codex_config::types::ApprovalsReviewer;
use codex_config::types::BundledSkillsConfig;
use codex_config::types::FeedbackConfigToml;
use codex_config::types::HistoryPersistence;
use codex_config::types::McpServerEnvVar;
use codex_config::types::McpServerToolConfig;
use codex_config::types::McpServerTransportConfig;
use codex_config::types::MemoriesConfig;
@@ -2490,7 +2491,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
command: "docs-server".to_string(),
args: Vec::new(),
env: None,
env_vars: vec!["ALPHA".to_string(), "BETA".to_string()],
env_vars: vec!["ALPHA".into(), "BETA".into()],
cwd: None,
},
experimental_environment: None,
@@ -2526,7 +2527,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
let docs = loaded.get("docs").expect("docs entry");
match &docs.transport {
McpServerTransportConfig::Stdio { env_vars, .. } => {
assert_eq!(env_vars, &vec!["ALPHA".to_string(), "BETA".to_string()]);
assert_eq!(env_vars, &vec!["ALPHA".into(), "BETA".into()]);
}
other => panic!("unexpected transport {other:?}"),
}
@@ -2534,6 +2535,62 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test]
async fn replace_mcp_servers_serializes_sourced_env_vars() -> 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![
"LEGACY".into(),
McpServerEnvVar::Config {
name: "REMOTE_TOKEN".to_string(),
source: Some("remote".to_string()),
},
],
cwd: None,
},
experimental_environment: None,
enabled: true,
required: false,
supports_parallel_tool_calls: false,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
default_tools_approval_mode: None,
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
tools: HashMap::new(),
},
)]);
apply_blocking(
codex_home.path(),
/*profile*/ None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
let serialized = std::fs::read_to_string(&config_path)?;
assert!(
serialized
.contains(r#"env_vars = ["LEGACY", { name = "REMOTE_TOKEN", source = "remote" }]"#),
"serialized config missing sourced env_vars field:\n{serialized}"
);
let loaded = load_global_mcp_servers(codex_home.path()).await?;
assert_eq!(loaded, servers);
Ok(())
}
#[tokio::test]
async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
+20 -1
View File
@@ -136,6 +136,7 @@ pub fn model_availability_nux_count_edits(shown_count: &HashMap<String, u32>) ->
mod document_helpers {
use codex_config::types::AppToolApproval;
use codex_config::types::McpServerConfig;
use codex_config::types::McpServerEnvVar;
use codex_config::types::McpServerToolConfig;
use codex_config::types::McpServerTransportConfig;
use toml_edit::Array as TomlArray;
@@ -198,7 +199,7 @@ mod document_helpers {
entry["env"] = table_from_pairs(env.iter());
}
if !env_vars.is_empty() {
entry["env_vars"] = array_from_iter(env_vars.iter().cloned());
entry["env_vars"] = array_from_env_vars(env_vars);
}
if let Some(cwd) = cwd {
entry["cwd"] = value(cwd.to_string_lossy().to_string());
@@ -348,6 +349,24 @@ mod document_helpers {
TomlItem::Value(array.into())
}
fn array_from_env_vars(env_vars: &[McpServerEnvVar]) -> TomlItem {
let mut array = TomlArray::new();
for env_var in env_vars {
match env_var {
McpServerEnvVar::Name(name) => array.push(name.clone()),
McpServerEnvVar::Config { name, source } => {
let mut table = InlineTable::new();
table.insert("name", name.clone().into());
if let Some(source) = source {
table.insert("source", source.clone().into());
}
array.push(table);
}
}
}
TomlItem::Value(array.into())
}
fn table_from_pairs<'a, I>(pairs: I) -> TomlItem
where
I: IntoIterator<Item = (&'a String, &'a String)>,
+1 -1
View File
@@ -696,7 +696,7 @@ fn blocking_replace_mcp_servers_round_trips() {
.into_iter()
.collect(),
),
env_vars: vec!["FOO".to_string()],
env_vars: vec!["FOO".into()],
cwd: None,
},
experimental_environment: None,
+3
View File
@@ -13,6 +13,7 @@ pub use codex_app_server_protocol::AppInfo;
pub use codex_app_server_protocol::AppMetadata;
use codex_connectors::AllConnectorsCacheKey;
use codex_connectors::DirectoryListResponse;
use codex_exec_server::Environment;
use codex_login::token_data::TokenData;
use codex_protocol::protocol::SandboxPolicy;
use codex_tools::DiscoverableTool;
@@ -37,6 +38,7 @@ use codex_login::default_client::create_client;
use codex_login::default_client::originator;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::McpConnectionManager;
use codex_mcp::McpRuntimeEnvironment;
use codex_mcp::ToolInfo;
use codex_mcp::ToolPluginProvenance;
use codex_mcp::codex_apps_tools_cache_key;
@@ -241,6 +243,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
INITIAL_SUBMIT_ID.to_owned(),
tx_event,
SandboxPolicy::new_read_only_policy(),
McpRuntimeEnvironment::new(Arc::new(Environment::default()), config.cwd.to_path_buf()),
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth.as_ref()),
ToolPluginProvenance::default(),
+7
View File
@@ -205,6 +205,13 @@ impl Session {
turn_context.sub_id.clone(),
self.get_tx_event(),
turn_context.sandbox_policy.get().clone(),
McpRuntimeEnvironment::new(
turn_context
.environment
.clone()
.unwrap_or_else(|| Arc::new(Environment::default())),
turn_context.cwd.to_path_buf(),
),
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth.as_ref()),
tool_plugin_provenance,
+1
View File
@@ -55,6 +55,7 @@ use codex_login::CodexAuth;
use codex_login::auth_env_telemetry::collect_auth_env_telemetry;
use codex_login::default_client::originator;
use codex_mcp::McpConnectionManager;
use codex_mcp::McpRuntimeEnvironment;
use codex_mcp::ToolInfo;
use codex_mcp::codex_apps_tools_cache_key;
#[cfg(test)]
+7 -1
View File
@@ -676,7 +676,7 @@ impl Session {
code_mode_service: crate::tools::code_mode::CodeModeService::new(
config.js_repl_node_path.clone(),
),
environment,
environment: environment.clone(),
};
services
.model_client
@@ -770,6 +770,12 @@ impl Session {
INITIAL_SUBMIT_ID.to_owned(),
tx_event.clone(),
session_configuration.sandbox_policy.get().clone(),
McpRuntimeEnvironment::new(
environment
.clone()
.unwrap_or_else(|| Arc::new(Environment::default())),
session_configuration.cwd.to_path_buf(),
),
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth),
tool_plugin_provenance,