Compare commits

..

3 Commits

Author SHA1 Message Date
Ahmed Ibrahim
097be1d05e Rename MCP runtime module
Rename mcp_connection.rs to runtime.rs now that the module only contains runtime and sandbox shared types plus the shared duration metric helper.

Update imports and exports to use the new runtime module name.

Co-authored-by: Codex <noreply@openai.com>
2026-04-27 01:22:48 +03:00
Ahmed Ibrahim
5b3fe94beb Move MCP client constants
Move default MCP startup/tool timeouts and the sandbox-state capability marker from mcp_connection.rs into client.rs.

Keep the public capability export stable by re-exporting it from client.rs.

Co-authored-by: Codex <noreply@openai.com>
2026-04-27 01:20:22 +03:00
Ahmed Ibrahim
ce97879350 Move single-use MCP helpers
Move manager-only startup update, origin, and startup error display helpers from mcp_connection.rs into manager.rs.

Move client-only bearer token resolution and server-name validation helpers into client.rs.

Co-authored-by: Codex <noreply@openai.com>
2026-04-27 01:17:45 +03:00
8 changed files with 205 additions and 233 deletions

View File

@@ -3,7 +3,7 @@ use std::path::PathBuf;
use std::time::Instant;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::mcp_connection::emit_duration;
use crate::runtime::emit_duration;
use crate::tools::MCP_TOOLS_CACHE_WRITE_DURATION_METRIC;
use crate::tools::ToolInfo;
use codex_login::CodexAuth;

View File

@@ -20,13 +20,8 @@ use crate::apps::write_cached_codex_apps_tools_if_needed;
use crate::elicitation::ElicitationRequestManager;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::mcp::ToolPluginProvenance;
use crate::mcp_connection::DEFAULT_STARTUP_TIMEOUT;
use crate::mcp_connection::DEFAULT_TOOL_TIMEOUT;
use crate::mcp_connection::MCP_SANDBOX_STATE_META_CAPABILITY;
use crate::mcp_connection::McpRuntimeEnvironment;
use crate::mcp_connection::emit_duration;
use crate::mcp_connection::resolve_bearer_token;
use crate::mcp_connection::validate_mcp_server_name;
use crate::runtime::McpRuntimeEnvironment;
use crate::runtime::emit_duration;
use crate::tools::ToolFilter;
use crate::tools::ToolInfo;
use crate::tools::filter_tools;
@@ -61,6 +56,12 @@ use tokio_util::sync::CancellationToken;
pub(crate) const MCP_TOOLS_LIST_DURATION_METRIC: &str = "codex.mcp.tools.list.duration_ms";
pub(crate) const MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC: &str =
"codex.mcp.tools.fetch_uncached.duration_ms";
pub(crate) const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) const DEFAULT_TOOL_TIMEOUT: Duration = Duration::from_secs(120);
/// MCP server capability indicating that Codex should include [`SandboxState`]
/// in tool-call request `_meta` under this key.
pub const MCP_SANDBOX_STATE_META_CAPABILITY: &str = "codex/sandbox-state-meta";
#[derive(Clone)]
pub(crate) struct ManagedClient {
@@ -298,6 +299,44 @@ pub(crate) fn elicitation_capability_for_server(
})
}
fn resolve_bearer_token(
server_name: &str,
bearer_token_env_var: Option<&str>,
) -> Result<Option<String>> {
let Some(env_var) = bearer_token_env_var else {
return Ok(None);
};
match env::var(env_var) {
Ok(value) => {
if value.is_empty() {
Err(anyhow!(
"Environment variable {env_var} for MCP server '{server_name}' is empty"
))
} else {
Ok(Some(value))
}
}
Err(env::VarError::NotPresent) => Err(anyhow!(
"Environment variable {env_var} for MCP server '{server_name}' is not set"
)),
Err(env::VarError::NotUnicode(_)) => Err(anyhow!(
"Environment variable {env_var} for MCP server '{server_name}' contains invalid Unicode"
)),
}
}
fn validate_mcp_server_name(server_name: &str) -> Result<()> {
let re = regex_lite::Regex::new(r"^[a-zA-Z0-9_-]+$")?;
if !re.is_match(server_name) {
return Err(anyhow!(
"Invalid MCP server name '{server_name}': must match pattern {pattern}",
pattern = re.as_str()
));
}
Ok(())
}
async fn start_server_task(
server_name: String,
client: Arc<RmcpClient>,

View File

@@ -1,7 +1,7 @@
pub use client::MCP_SANDBOX_STATE_META_CAPABILITY;
pub use manager::McpConnectionManager;
pub use mcp_connection::MCP_SANDBOX_STATE_META_CAPABILITY;
pub use mcp_connection::McpRuntimeEnvironment;
pub use mcp_connection::SandboxState;
pub use runtime::McpRuntimeEnvironment;
pub use runtime::SandboxState;
pub use tools::ToolInfo;
pub use mcp::CODEX_APPS_MCP_SERVER_NAME;
@@ -43,5 +43,5 @@ pub(crate) mod client;
pub(crate) mod elicitation;
pub(crate) mod manager;
pub(crate) mod mcp;
pub(crate) mod mcp_connection;
pub(crate) mod runtime;
pub(crate) mod tools;

View File

@@ -9,6 +9,7 @@ use crate::apps::CodexAppsToolsCacheContext;
use crate::apps::CodexAppsToolsCacheKey;
use crate::apps::write_cached_codex_apps_tools_if_needed;
use crate::client::AsyncManagedClient;
use crate::client::DEFAULT_STARTUP_TIMEOUT;
use crate::client::MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC;
use crate::client::MCP_TOOLS_LIST_DURATION_METRIC;
use crate::client::ManagedClient;
@@ -17,12 +18,8 @@ use crate::client::list_tools_for_client_uncached;
use crate::elicitation::ElicitationRequestManager;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::mcp::ToolPluginProvenance;
use crate::mcp_connection::McpRuntimeEnvironment;
use crate::mcp_connection::emit_duration;
use crate::mcp_connection::emit_update;
use crate::mcp_connection::mcp_init_error_display;
use crate::mcp_connection::startup_outcome_error_message;
use crate::mcp_connection::transport_origin;
use crate::runtime::McpRuntimeEnvironment;
use crate::runtime::emit_duration;
use crate::tools::ToolInfo;
use crate::tools::filter_tools;
use crate::tools::qualify_tools;
@@ -59,6 +56,7 @@ use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use tracing::warn;
use url::Url;
/// A thin wrapper around a set of running [`RmcpClient`] instances.
pub struct McpConnectionManager {
@@ -67,6 +65,92 @@ pub struct McpConnectionManager {
elicitation_requests: ElicitationRequestManager,
}
async fn emit_update(
submit_id: &str,
tx_event: &Sender<Event>,
update: McpStartupUpdateEvent,
) -> Result<(), async_channel::SendError<Event>> {
tx_event
.send(Event {
id: submit_id.to_string(),
msg: EventMsg::McpStartupUpdate(update),
})
.await
}
fn transport_origin(transport: &McpServerTransportConfig) -> Option<String> {
match transport {
McpServerTransportConfig::StreamableHttp { url, .. } => {
let parsed = Url::parse(url).ok()?;
Some(parsed.origin().ascii_serialization())
}
McpServerTransportConfig::Stdio { .. } => Some("stdio".to_string()),
}
}
fn mcp_init_error_display(
server_name: &str,
entry: Option<&McpAuthStatusEntry>,
err: &StartupOutcomeError,
) -> String {
if let Some(McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
http_headers,
..
}) = &entry.map(|entry| &entry.config.transport)
&& url == "https://api.githubcopilot.com/mcp/"
&& bearer_token_env_var.is_none()
&& http_headers.as_ref().map(HashMap::is_empty).unwrap_or(true)
{
format!(
"GitHub MCP does not support OAuth. Log in by adding a personal access token (https://github.com/settings/personal-access-tokens) to your environment and config.toml:\n[mcp_servers.{server_name}]\nbearer_token_env_var = CODEX_GITHUB_PERSONAL_ACCESS_TOKEN"
)
} else if is_mcp_client_auth_required_error(err) {
format!(
"The {server_name} MCP server is not logged in. Run `codex mcp login {server_name}`."
)
} else if is_mcp_client_startup_timeout_error(err) {
let startup_timeout_secs = match entry {
Some(entry) => match entry.config.startup_timeout_sec {
Some(timeout) => timeout,
None => DEFAULT_STARTUP_TIMEOUT,
},
None => DEFAULT_STARTUP_TIMEOUT,
}
.as_secs();
format!(
"MCP client for `{server_name}` timed out after {startup_timeout_secs} seconds. Add or adjust `startup_timeout_sec` in your config.toml:\n[mcp_servers.{server_name}]\nstartup_timeout_sec = XX"
)
} else {
format!("MCP client for `{server_name}` failed to start: {err:#}")
}
}
fn is_mcp_client_auth_required_error(error: &StartupOutcomeError) -> bool {
match error {
StartupOutcomeError::Failed { error } => error.contains("Auth required"),
_ => false,
}
}
fn is_mcp_client_startup_timeout_error(error: &StartupOutcomeError) -> bool {
match error {
StartupOutcomeError::Failed { error } => {
error.contains("request timed out")
|| error.contains("timed out handshaking with MCP server")
}
_ => false,
}
}
fn startup_outcome_error_message(error: StartupOutcomeError) -> String {
match error {
StartupOutcomeError::Cancelled => "MCP startup cancelled".to_string(),
StartupOutcomeError::Failed { error } => error,
}
}
impl McpConnectionManager {
pub fn new_uninitialized(
approval_policy: &Constrained<AskForApproval>,

View File

@@ -36,7 +36,7 @@ use serde_json::Value;
use crate::apps::codex_apps_tools_cache_key;
use crate::manager::McpConnectionManager;
use crate::mcp_connection::McpRuntimeEnvironment;
use crate::runtime::McpRuntimeEnvironment;
pub const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps";
const MCP_TOOL_NAME_PREFIX: &str = "mcp";

View File

@@ -1,213 +0,0 @@
//! Connection support for Model Context Protocol (MCP) servers.
//!
//! This module contains shared types and helpers used by [`McpConnectionManager`].
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use crate::McpAuthStatusEntry;
use crate::client::StartupOutcomeError;
use anyhow::Result;
use anyhow::anyhow;
use async_channel::Sender;
use codex_exec_server::Environment;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::McpStartupUpdateEvent;
use codex_protocol::protocol::SandboxPolicy;
use serde::Deserialize;
use serde::Serialize;
use url::Url;
use codex_config::McpServerTransportConfig;
/// Default timeout for initializing MCP server & initially listing tools.
pub(crate) const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
/// Default timeout for individual tool calls.
pub(crate) const DEFAULT_TOOL_TIMEOUT: Duration = Duration::from_secs(120);
/// MCP server capability indicating that Codex should include [`SandboxState`]
/// in tool-call request `_meta` under this key.
pub const MCP_SANDBOX_STATE_META_CAPABILITY: &str = "codex/sandbox-state-meta";
#[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 placement information used when starting MCP server transports.
///
/// `McpConfig` describes what servers exist. This value describes where those
/// servers should run for the current caller. Keep it explicit at manager
/// construction time so status/snapshot paths and real sessions make the same
/// local-vs-remote decision. `fallback_cwd` is not a per-server override; it is
/// used when a stdio server omits `cwd` and the launcher needs a concrete
/// process working directory.
#[derive(Clone)]
pub struct McpRuntimeEnvironment {
environment: Arc<Environment>,
fallback_cwd: PathBuf,
}
impl McpRuntimeEnvironment {
pub fn new(environment: Arc<Environment>, fallback_cwd: PathBuf) -> Self {
Self {
environment,
fallback_cwd,
}
}
pub(crate) fn environment(&self) -> Arc<Environment> {
Arc::clone(&self.environment)
}
pub(crate) fn fallback_cwd(&self) -> PathBuf {
self.fallback_cwd.clone()
}
}
pub(crate) async fn emit_update(
submit_id: &str,
tx_event: &Sender<Event>,
update: McpStartupUpdateEvent,
) -> Result<(), async_channel::SendError<Event>> {
tx_event
.send(Event {
id: submit_id.to_string(),
msg: EventMsg::McpStartupUpdate(update),
})
.await
}
pub(crate) fn resolve_bearer_token(
server_name: &str,
bearer_token_env_var: Option<&str>,
) -> Result<Option<String>> {
let Some(env_var) = bearer_token_env_var else {
return Ok(None);
};
match env::var(env_var) {
Ok(value) => {
if value.is_empty() {
Err(anyhow!(
"Environment variable {env_var} for MCP server '{server_name}' is empty"
))
} else {
Ok(Some(value))
}
}
Err(env::VarError::NotPresent) => Err(anyhow!(
"Environment variable {env_var} for MCP server '{server_name}' is not set"
)),
Err(env::VarError::NotUnicode(_)) => Err(anyhow!(
"Environment variable {env_var} for MCP server '{server_name}' contains invalid Unicode"
)),
}
}
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);
}
}
pub(crate) fn transport_origin(transport: &McpServerTransportConfig) -> Option<String> {
match transport {
McpServerTransportConfig::StreamableHttp { url, .. } => {
let parsed = Url::parse(url).ok()?;
Some(parsed.origin().ascii_serialization())
}
McpServerTransportConfig::Stdio { .. } => Some("stdio".to_string()),
}
}
pub(crate) fn validate_mcp_server_name(server_name: &str) -> Result<()> {
let re = regex_lite::Regex::new(r"^[a-zA-Z0-9_-]+$")?;
if !re.is_match(server_name) {
return Err(anyhow!(
"Invalid MCP server name '{server_name}': must match pattern {pattern}",
pattern = re.as_str()
));
}
Ok(())
}
pub(crate) fn mcp_init_error_display(
server_name: &str,
entry: Option<&McpAuthStatusEntry>,
err: &StartupOutcomeError,
) -> String {
if let Some(McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
http_headers,
..
}) = &entry.map(|entry| &entry.config.transport)
&& url == "https://api.githubcopilot.com/mcp/"
&& bearer_token_env_var.is_none()
&& http_headers.as_ref().map(HashMap::is_empty).unwrap_or(true)
{
format!(
"GitHub MCP does not support OAuth. Log in by adding a personal access token (https://github.com/settings/personal-access-tokens) to your environment and config.toml:\n[mcp_servers.{server_name}]\nbearer_token_env_var = CODEX_GITHUB_PERSONAL_ACCESS_TOKEN"
)
} else if is_mcp_client_auth_required_error(err) {
format!(
"The {server_name} MCP server is not logged in. Run `codex mcp login {server_name}`."
)
} else if is_mcp_client_startup_timeout_error(err) {
let startup_timeout_secs = match entry {
Some(entry) => match entry.config.startup_timeout_sec {
Some(timeout) => timeout,
None => DEFAULT_STARTUP_TIMEOUT,
},
None => DEFAULT_STARTUP_TIMEOUT,
}
.as_secs();
format!(
"MCP client for `{server_name}` timed out after {startup_timeout_secs} seconds. Add or adjust `startup_timeout_sec` in your config.toml:\n[mcp_servers.{server_name}]\nstartup_timeout_sec = XX"
)
} else {
format!("MCP client for `{server_name}` failed to start: {err:#}")
}
}
fn is_mcp_client_auth_required_error(error: &StartupOutcomeError) -> bool {
match error {
StartupOutcomeError::Failed { error } => error.contains("Auth required"),
_ => false,
}
}
fn is_mcp_client_startup_timeout_error(error: &StartupOutcomeError) -> bool {
match error {
StartupOutcomeError::Failed { error } => {
error.contains("request timed out")
|| error.contains("timed out handshaking with MCP server")
}
_ => false,
}
}
pub(crate) fn startup_outcome_error_message(error: StartupOutcomeError) -> String {
match error {
StartupOutcomeError::Cancelled => "MCP startup cancelled".to_string(),
StartupOutcomeError::Failed { error } => error,
}
}
#[cfg(test)]
mod mcp_init_error_display_tests {}

View File

@@ -11,7 +11,6 @@ use crate::client::elicitation_capability_for_server;
use crate::declared_openai_file_input_param_names;
use crate::elicitation::ElicitationRequestManager;
use crate::elicitation::elicitation_is_rejected_by_policy;
use crate::mcp_connection::transport_origin;
use crate::tools::ToolFilter;
use crate::tools::ToolInfo;
use crate::tools::filter_tools;

View File

@@ -0,0 +1,63 @@
//! Runtime support for Model Context Protocol (MCP) servers.
//!
//! This module contains shared types and helpers used by [`McpConnectionManager`].
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use codex_exec_server::Environment;
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 placement information used when starting MCP server transports.
///
/// `McpConfig` describes what servers exist. This value describes where those
/// servers should run for the current caller. Keep it explicit at manager
/// construction time so status/snapshot paths and real sessions make the same
/// local-vs-remote decision. `fallback_cwd` is not a per-server override; it is
/// used when a stdio server omits `cwd` and the launcher needs a concrete
/// process working directory.
#[derive(Clone)]
pub struct McpRuntimeEnvironment {
environment: Arc<Environment>,
fallback_cwd: PathBuf,
}
impl McpRuntimeEnvironment {
pub fn new(environment: Arc<Environment>, fallback_cwd: PathBuf) -> Self {
Self {
environment,
fallback_cwd,
}
}
pub(crate) fn environment(&self) -> Arc<Environment> {
Arc::clone(&self.environment)
}
pub(crate) fn fallback_cwd(&self) -> PathBuf {
self.fallback_cwd.clone()
}
}
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);
}
}