mirror of
https://github.com/openai/codex.git
synced 2026-05-04 19:36:45 +00:00
[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:
@@ -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;
|
||||
|
||||
@@ -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)>,
|
||||
|
||||
@@ -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>,
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user