mirror of
https://github.com/openai/codex.git
synced 2026-05-23 20:44:50 +00:00
## Summary - route each configured MCP server through an explicit per-server `environment_id` instead of a manager-wide remote toggle - default omitted `environment_id` to `local`, resolve named ids through `EnvironmentManager`, and fail only the affected MCP server when an explicit id is unknown - keep local stdio on the existing local launcher path for now, while named-environment stdio uses the selected environment backend and requires an absolute `cwd` - allow local HTTP MCP servers to keep using the ambient HTTP client when no local `Environment` is configured; named-environment HTTP MCPs use that environment's HTTP client ## Validation - devbox Bazel build: `bazel build --bes_backend= --bes_results_url= //codex-rs/cli:codex //codex-rs/rmcp-client:test_stdio_server //codex-rs/rmcp-client:test_streamable_http_server` - devbox app-server config matrix with real `config.toml` / `environments.toml` files covering omitted local, explicit local, omitted local under remote default, explicit remote stdio, local HTTP without local env, explicit remote HTTP, local stdio without local env, unknown explicit env, and remote stdio without `cwd`
297 lines
10 KiB
Rust
297 lines
10 KiB
Rust
//! Runtime support for Model Context Protocol (MCP) servers.
|
|
//!
|
|
//! This module contains data that describes the runtime environment in which MCP
|
|
//! servers execute, plus the sandbox state payload sent to capable servers and a
|
|
//! tiny shared metrics helper. Transport startup and orchestration live in
|
|
//! [`crate::rmcp_client`] and [`crate::connection_manager`].
|
|
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use codex_exec_server::Environment;
|
|
use codex_exec_server::EnvironmentManager;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SandboxState {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub permission_profile: Option<PermissionProfile>,
|
|
pub sandbox_policy: SandboxPolicy,
|
|
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
|
pub sandbox_cwd: PathBuf,
|
|
#[serde(default)]
|
|
pub use_legacy_landlock: bool,
|
|
}
|
|
|
|
/// Runtime context used when resolving per-server MCP environments.
|
|
///
|
|
/// `McpConfig` describes what servers exist. This value carries the canonical
|
|
/// environment registry plus the local stdio fallback cwd used when a local
|
|
/// stdio server omits its own working directory.
|
|
#[derive(Clone)]
|
|
pub struct McpRuntimeContext {
|
|
environment_manager: Arc<EnvironmentManager>,
|
|
local_stdio_fallback_cwd: PathBuf,
|
|
}
|
|
|
|
impl McpRuntimeContext {
|
|
pub fn new(
|
|
environment_manager: Arc<EnvironmentManager>,
|
|
local_stdio_fallback_cwd: PathBuf,
|
|
) -> Self {
|
|
Self {
|
|
environment_manager,
|
|
local_stdio_fallback_cwd,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn local_stdio_fallback_cwd(&self) -> PathBuf {
|
|
self.local_stdio_fallback_cwd.clone()
|
|
}
|
|
|
|
pub(crate) fn resolve_server_environment(
|
|
&self,
|
|
server_name: &str,
|
|
config: &codex_config::McpServerConfig,
|
|
) -> Result<Option<Arc<Environment>>, String> {
|
|
// Resolve `"local"` through the shared registry when available. Local
|
|
// HTTP is the one current exception: it can use the ambient HTTP client
|
|
// even when no local Environment is configured.
|
|
if let Some(environment) = self
|
|
.environment_manager
|
|
.get_environment(&config.environment_id)
|
|
{
|
|
if !config.is_local_environment() {
|
|
ensure_remote_stdio_cwd(server_name, config)?;
|
|
}
|
|
return Ok(Some(environment));
|
|
}
|
|
|
|
if config.is_local_environment() {
|
|
return match config.transport {
|
|
codex_config::McpServerTransportConfig::Stdio { .. } => Err(format!(
|
|
"local stdio MCP server `{server_name}` requires a local environment"
|
|
)),
|
|
codex_config::McpServerTransportConfig::StreamableHttp { .. } => Ok(None),
|
|
};
|
|
}
|
|
|
|
Err(format!(
|
|
"MCP server `{server_name}` references unknown environment id `{}`",
|
|
config.environment_id
|
|
))
|
|
}
|
|
}
|
|
|
|
fn ensure_remote_stdio_cwd(
|
|
server_name: &str,
|
|
config: &codex_config::McpServerConfig,
|
|
) -> Result<(), String> {
|
|
let codex_config::McpServerTransportConfig::Stdio { cwd, .. } = &config.transport else {
|
|
return Ok(());
|
|
};
|
|
let Some(cwd) = cwd else {
|
|
return Err(format!(
|
|
"remote stdio MCP server `{server_name}` requires an absolute cwd"
|
|
));
|
|
};
|
|
if cwd.is_absolute() {
|
|
return Ok(());
|
|
}
|
|
Err(format!(
|
|
"remote stdio MCP server `{server_name}` requires an absolute cwd, got `{}`",
|
|
cwd.display()
|
|
))
|
|
}
|
|
|
|
pub(crate) fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &str)]) {
|
|
if let Some(metrics) = codex_otel::global() {
|
|
let _ = metrics.record_duration(metric, duration, tags);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::collections::HashMap;
|
|
|
|
use codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
|
|
use codex_config::McpServerConfig;
|
|
use codex_config::McpServerTransportConfig;
|
|
use codex_exec_server::EnvironmentManager;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
fn stdio_server(environment_id: &str) -> McpServerConfig {
|
|
McpServerConfig {
|
|
transport: McpServerTransportConfig::Stdio {
|
|
command: "echo".to_string(),
|
|
args: Vec::new(),
|
|
env: None,
|
|
env_vars: Vec::new(),
|
|
cwd: None,
|
|
},
|
|
environment_id: environment_id.to_string(),
|
|
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: None,
|
|
oauth_resource: None,
|
|
tools: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn http_server(environment_id: &str) -> McpServerConfig {
|
|
McpServerConfig {
|
|
transport: McpServerTransportConfig::StreamableHttp {
|
|
url: "http://127.0.0.1:1".to_string(),
|
|
bearer_token_env_var: None,
|
|
http_headers: None,
|
|
env_http_headers: None,
|
|
},
|
|
environment_id: environment_id.to_string(),
|
|
..stdio_server(environment_id)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn local_stdio_requires_local_stdio_availability() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(EnvironmentManager::without_environments()),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
|
|
let error = match runtime_context
|
|
.resolve_server_environment("stdio", &stdio_server(DEFAULT_MCP_SERVER_ENVIRONMENT_ID))
|
|
{
|
|
Ok(_) => panic!("local stdio MCP should require a local environment"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error,
|
|
"local stdio MCP server `stdio` requires a local environment"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn local_http_does_not_require_local_stdio_availability() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(EnvironmentManager::without_environments()),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
|
|
let resolved_runtime = match runtime_context
|
|
.resolve_server_environment("http", &http_server(DEFAULT_MCP_SERVER_ENVIRONMENT_ID))
|
|
{
|
|
Ok(resolved_runtime) => resolved_runtime,
|
|
Err(error) => panic!("local HTTP MCP should resolve: {error}"),
|
|
};
|
|
assert!(resolved_runtime.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_explicit_environment_is_rejected() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(EnvironmentManager::without_environments()),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
|
|
let error =
|
|
match runtime_context.resolve_server_environment("stdio", &stdio_server("remote")) {
|
|
Ok(_) => panic!("unknown MCP environment should fail"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error,
|
|
"MCP server `stdio` references unknown environment id `remote`"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn explicit_remote_stdio_and_http_accept_named_environment() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(
|
|
EnvironmentManager::create_for_tests(
|
|
Some("ws://127.0.0.1:8765".to_string()),
|
|
/*local_runtime_paths*/ None,
|
|
)
|
|
.await,
|
|
),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
|
|
let mut remote_stdio = stdio_server("remote");
|
|
let McpServerTransportConfig::Stdio { cwd, .. } = &mut remote_stdio.transport else {
|
|
unreachable!("stdio helper should build stdio transport");
|
|
};
|
|
*cwd = Some(std::env::temp_dir());
|
|
for resolved_runtime in [
|
|
runtime_context.resolve_server_environment("stdio", &remote_stdio),
|
|
runtime_context.resolve_server_environment("http", &http_server("remote")),
|
|
] {
|
|
let resolved_runtime = match resolved_runtime {
|
|
Ok(resolved_runtime) => resolved_runtime,
|
|
Err(error) => panic!("remote MCP should resolve: {error}"),
|
|
};
|
|
assert!(resolved_runtime.is_some());
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn local_stdio_accepts_local_environment_when_available() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(EnvironmentManager::default_for_tests()),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
|
|
let resolved_runtime = match runtime_context
|
|
.resolve_server_environment("stdio", &stdio_server(DEFAULT_MCP_SERVER_ENVIRONMENT_ID))
|
|
{
|
|
Ok(resolved_runtime) => resolved_runtime,
|
|
Err(error) => panic!("local stdio MCP should resolve: {error}"),
|
|
};
|
|
assert!(resolved_runtime.is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn remote_stdio_requires_absolute_cwd() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(
|
|
EnvironmentManager::create_for_tests(
|
|
Some("ws://127.0.0.1:8765".to_string()),
|
|
/*local_runtime_paths*/ None,
|
|
)
|
|
.await,
|
|
),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
let mut remote_stdio = stdio_server("remote");
|
|
let McpServerTransportConfig::Stdio { cwd, .. } = &mut remote_stdio.transport else {
|
|
unreachable!("stdio helper should build stdio transport");
|
|
};
|
|
*cwd = Some(PathBuf::from("relative"));
|
|
|
|
let error = match runtime_context.resolve_server_environment("stdio", &remote_stdio) {
|
|
Ok(_) => panic!("remote stdio MCP should require absolute cwd"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error,
|
|
"remote stdio MCP server `stdio` requires an absolute cwd, got `relative`"
|
|
);
|
|
}
|
|
}
|