Codex Handbook
codex-mcp/src/runtime.rs 296 lines
//! 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`"        );    }}