[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
parent e3f44ca3b3
commit 996aa23e4c
31 changed files with 1815 additions and 76 deletions

View File

@@ -72,6 +72,7 @@ pub use mcp_edit::load_global_mcp_servers;
pub use mcp_types::AppToolApproval;
pub use mcp_types::McpServerConfig;
pub use mcp_types::McpServerDisabledReason;
pub use mcp_types::McpServerEnvVar;
pub use mcp_types::McpServerToolConfig;
pub use mcp_types::McpServerTransportConfig;
pub use mcp_types::RawMcpServerConfig;

View File

@@ -14,6 +14,7 @@ use toml_edit::value;
use crate::AppToolApproval;
use crate::CONFIG_TOML_FILE;
use crate::McpServerConfig;
use crate::McpServerEnvVar;
use crate::McpServerTransportConfig;
pub async fn load_global_mcp_servers(
@@ -142,7 +143,7 @@ fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem {
entry["env"] = table_from_pairs(env.iter());
}
if !env_vars.is_empty() {
entry["env_vars"] = array_from_strings(env_vars);
entry["env_vars"] = array_from_env_vars(env_vars);
}
if let Some(cwd) = cwd {
entry["cwd"] = value(cwd.to_string_lossy().to_string());
@@ -247,6 +248,24 @@ fn array_from_strings(values: &[String]) -> TomlItem {
TomlItem::Value(array.into())
}
fn array_from_env_vars(env_vars: &[McpServerEnvVar]) -> TomlItem {
let mut array = toml_edit::Array::new();
for env_var in env_vars {
match env_var {
McpServerEnvVar::Name(name) => array.push(name.clone()),
McpServerEnvVar::Config { name, source } => {
let mut table = toml_edit::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)>,

View File

@@ -56,6 +56,64 @@ pub struct McpServerToolConfig {
pub approval_mode: Option<AppToolApproval>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
#[serde(untagged, deny_unknown_fields)]
pub enum McpServerEnvVar {
Name(String),
Config {
name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
source: Option<String>,
},
}
impl McpServerEnvVar {
pub fn name(&self) -> &str {
match self {
McpServerEnvVar::Name(name) => name,
McpServerEnvVar::Config { name, .. } => name,
}
}
pub fn source(&self) -> Option<&str> {
match self {
McpServerEnvVar::Name(_) => None,
McpServerEnvVar::Config { source, .. } => source.as_deref(),
}
}
pub fn is_remote_source(&self) -> bool {
self.source() == Some("remote")
}
pub fn validate_source(&self) -> Result<(), String> {
match self.source() {
None | Some("local") | Some("remote") => Ok(()),
Some(source) => Err(format!(
"unsupported env_vars source `{source}`; expected `local` or `remote`"
)),
}
}
}
impl From<String> for McpServerEnvVar {
fn from(value: String) -> Self {
Self::Name(value)
}
}
impl From<&str> for McpServerEnvVar {
fn from(value: &str) -> Self {
Self::Name(value.to_string())
}
}
impl AsRef<str> for McpServerEnvVar {
fn as_ref(&self) -> &str {
self.name()
}
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct McpServerConfig {
#[serde(flatten)]
@@ -133,7 +191,7 @@ pub struct RawMcpServerConfig {
#[serde(default)]
pub env: Option<HashMap<String, String>>,
#[serde(default)]
pub env_vars: Option<Vec<String>>,
pub env_vars: Option<Vec<McpServerEnvVar>>,
#[serde(default)]
pub cwd: Option<PathBuf>,
pub http_headers: Option<HashMap<String, String>>,
@@ -235,11 +293,15 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
throw_if_set("stdio", "http_headers", http_headers.as_ref())?;
throw_if_set("stdio", "env_http_headers", env_http_headers.as_ref())?;
throw_if_set("stdio", "oauth_resource", oauth_resource.as_ref())?;
let env_vars = env_vars.unwrap_or_default();
for env_var in &env_vars {
env_var.validate_source()?;
}
McpServerTransportConfig::Stdio {
command,
args: args.unwrap_or_default(),
env,
env_vars: env_vars.unwrap_or_default(),
env_vars,
cwd,
}
} else if let Some(url) = url {
@@ -303,7 +365,7 @@ pub enum McpServerTransportConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
env: Option<HashMap<String, String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
env_vars: Vec<String>,
env_vars: Vec<McpServerEnvVar>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cwd: Option<PathBuf>,
},

View File

@@ -91,12 +91,65 @@ fn deserialize_stdio_command_server_config_with_env_vars() {
command: "echo".to_string(),
args: vec![],
env: None,
env_vars: vec!["FOO".to_string(), "BAR".to_string()],
env_vars: vec!["FOO".into(), "BAR".into()],
cwd: None,
}
);
}
#[test]
fn deserialize_stdio_command_server_config_with_env_var_sources() {
let cfg: McpServerConfig = toml::from_str(
r#"
command = "echo"
env_vars = [
"LEGACY_TOKEN",
{ name = "LOCAL_TOKEN", source = "local" },
{ name = "REMOTE_TOKEN", source = "remote" },
]
"#,
)
.expect("should deserialize command config with sourced env_vars");
assert_eq!(
cfg.transport,
McpServerTransportConfig::Stdio {
command: "echo".to_string(),
args: vec![],
env: None,
env_vars: vec![
McpServerEnvVar::Name("LEGACY_TOKEN".to_string()),
McpServerEnvVar::Config {
name: "LOCAL_TOKEN".to_string(),
source: Some("local".to_string()),
},
McpServerEnvVar::Config {
name: "REMOTE_TOKEN".to_string(),
source: Some("remote".to_string()),
},
],
cwd: None,
}
);
}
#[test]
fn deserialize_stdio_command_server_config_rejects_unknown_env_var_source() {
let err = toml::from_str::<McpServerConfig>(
r#"
command = "echo"
env_vars = [{ name = "TOKEN", source = "elsewhere" }]
"#,
)
.expect_err("unsupported env var source should be rejected");
assert!(
err.to_string()
.contains("unsupported env_vars source `elsewhere`"),
"unexpected error: {err}"
);
}
#[test]
fn deserialize_stdio_command_server_config_with_cwd() {
let cfg: McpServerConfig = toml::from_str(

View File

@@ -6,6 +6,7 @@
pub use crate::mcp_types::AppToolApproval;
pub use crate::mcp_types::McpServerConfig;
pub use crate::mcp_types::McpServerDisabledReason;
pub use crate::mcp_types::McpServerEnvVar;
pub use crate::mcp_types::McpServerToolConfig;
pub use crate::mcp_types::McpServerTransportConfig;
pub use crate::mcp_types::RawMcpServerConfig;