mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
## What changed - Read `mcp_servers` configuration and requirements from each selected remote executor and add eligible HTTP servers to the thread's MCP runtime. - Bind discovered servers to the thread's concrete executor snapshot and apply environment MCP policy and requirements. Discovery is best effort, and executor-local servers are not treated as required at startup. - Ignore unsupported stdio servers and HTTP configurations that depend on environment-provided headers or header helpers. ## Testing - Added an app-server integration test covering discovery, authenticated HTTP tool invocation, requirements enforcement, and exclusion of stdio servers. GitOrigin-RevId: 6e1cdcebdbb1cc21a5a2285fbc5617d8d5997182
182 lines
6.8 KiB
Rust
182 lines
6.8 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use codex_config::CONFIG_TOML_FILE;
|
|
use codex_config::McpServerConfig;
|
|
use codex_config::McpServerDisabledReason;
|
|
use codex_config::RequirementSource;
|
|
use codex_config::RequirementsLayerEntry;
|
|
use codex_config::compose_requirements_for_hostname;
|
|
use codex_config::format_config_layer_source;
|
|
use codex_config::host_name;
|
|
use codex_config::loader::LocalTomlLayerStack;
|
|
use codex_config::loader::load_local_config_layers;
|
|
use codex_exec_server_protocol::EnvironmentConfigLayer;
|
|
use codex_exec_server_protocol::EnvironmentConfigLayerStack;
|
|
use codex_exec_server_protocol::EnvironmentConfigReadParams;
|
|
use codex_exec_server_protocol::EnvironmentConfigReadResponse;
|
|
use codex_file_system::ExecutorFileSystem;
|
|
use codex_utils_home_dir::find_codex_home;
|
|
use codex_utils_path_uri::PathUri;
|
|
|
|
use crate::Environment;
|
|
use crate::ExecServerError;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub(crate) enum ReadEnvironmentConfigError {
|
|
#[error("{0}")]
|
|
InvalidParams(String),
|
|
#[error("{0}")]
|
|
Internal(String),
|
|
}
|
|
|
|
pub(crate) async fn read_environment_config(
|
|
file_system: &dyn ExecutorFileSystem,
|
|
params: EnvironmentConfigReadParams,
|
|
) -> Result<EnvironmentConfigReadResponse, ReadEnvironmentConfigError> {
|
|
validate_paths(¶ms)?;
|
|
let cwd = params
|
|
.cwd
|
|
.to_abs_path()
|
|
.map_err(|error| ReadEnvironmentConfigError::InvalidParams(error.to_string()))?;
|
|
let codex_home = find_codex_home().map_err(|error| {
|
|
ReadEnvironmentConfigError::Internal(format!("failed to find Codex home: {error}"))
|
|
})?;
|
|
let layers = load_local_config_layers(file_system, codex_home.as_path(), &cwd)
|
|
.await
|
|
.map_err(|error| {
|
|
ReadEnvironmentConfigError::Internal(format!(
|
|
"failed to load executor-local config: {error}"
|
|
))
|
|
})?
|
|
.project(¶ms.config_paths, ¶ms.requirements_paths);
|
|
|
|
Ok(EnvironmentConfigReadResponse {
|
|
user_home_dir: dirs::home_dir()
|
|
.and_then(|home_dir| PathUri::from_host_native_path(home_dir).ok()),
|
|
codex_home_dir: PathUri::from_abs_path(&codex_home),
|
|
hostname: host_name(),
|
|
config: serialize_layer_stack(layers.config, |source| {
|
|
format_config_layer_source(source, CONFIG_TOML_FILE)
|
|
})?,
|
|
requirements: serialize_layer_stack(layers.requirements, ToString::to_string)?,
|
|
})
|
|
}
|
|
|
|
impl Environment {
|
|
/// Reads executor-owned HTTP MCP servers from the selected project's current config.
|
|
pub async fn discover_http_mcp_servers(
|
|
&self,
|
|
cwd: PathUri,
|
|
) -> Result<Vec<(String, McpServerConfig)>, ExecServerError> {
|
|
let response = tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
|
if !self.info().await?.capabilities.environment_config_read {
|
|
return Ok(None);
|
|
}
|
|
self.read_environment_config(EnvironmentConfigReadParams {
|
|
cwd,
|
|
config_paths: vec![vec!["mcp_servers".to_string()]],
|
|
requirements_paths: vec![vec!["mcp_servers".to_string()]],
|
|
})
|
|
.await
|
|
.map(Some)
|
|
})
|
|
.await
|
|
.map_err(|_| ExecServerError::Protocol("executor MCP discovery timed out".to_string()))??;
|
|
let Some(response) = response else {
|
|
return Ok(Vec::new());
|
|
};
|
|
let requirements = compose_requirements_for_hostname(
|
|
response.requirements.layers.into_iter().map(|layer| {
|
|
RequirementsLayerEntry::from_toml(RequirementSource::Unknown, layer.toml)
|
|
}),
|
|
response.hostname.as_deref(),
|
|
)
|
|
.map_err(|error| {
|
|
ExecServerError::Protocol(format!("invalid executor-local MCP requirements: {error}"))
|
|
})?
|
|
.and_then(|requirements| requirements.mcp_servers);
|
|
let mut merged = toml::Value::Table(toml::map::Map::new());
|
|
for layer in response.config.layers {
|
|
let config = toml::from_str::<toml::Value>(&layer.toml).map_err(|error| {
|
|
ExecServerError::Protocol(format!("invalid executor-local MCP config: {error}"))
|
|
})?;
|
|
codex_config::merge_toml_values(&mut merged, &config);
|
|
}
|
|
let mut servers = merged
|
|
.get("mcp_servers")
|
|
.cloned()
|
|
.unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
|
|
if let Some(servers) = servers.as_table_mut() {
|
|
servers.retain(|_, server| server.get("url").is_some());
|
|
}
|
|
let servers = servers
|
|
.try_into::<HashMap<String, McpServerConfig>>()
|
|
.map_err(|error| {
|
|
ExecServerError::Protocol(format!("invalid executor-local MCP servers: {error}"))
|
|
})?;
|
|
|
|
Ok(servers
|
|
.into_iter()
|
|
.map(|(name, mut server)| {
|
|
if let Some(requirements) = requirements.as_ref()
|
|
&& !requirements
|
|
.value
|
|
.get(&name)
|
|
.is_some_and(|requirement| server.matches_requirement(requirement))
|
|
{
|
|
server.enabled = false;
|
|
server.disabled_reason = Some(McpServerDisabledReason::Requirements {
|
|
source: requirements.source.clone(),
|
|
});
|
|
}
|
|
(name, server)
|
|
})
|
|
.collect())
|
|
}
|
|
}
|
|
|
|
fn validate_paths(params: &EnvironmentConfigReadParams) -> Result<(), ReadEnvironmentConfigError> {
|
|
if params.config_paths.is_empty() && params.requirements_paths.is_empty() {
|
|
return Err(ReadEnvironmentConfigError::InvalidParams(
|
|
"at least one config or requirements path is required".to_string(),
|
|
));
|
|
}
|
|
if params
|
|
.config_paths
|
|
.iter()
|
|
.chain(¶ms.requirements_paths)
|
|
.any(Vec::is_empty)
|
|
{
|
|
return Err(ReadEnvironmentConfigError::InvalidParams(
|
|
"TOML paths must contain at least one key segment".to_string(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn serialize_layer_stack<S>(
|
|
stack: LocalTomlLayerStack<S>,
|
|
source_name: impl Fn(&S) -> String,
|
|
) -> Result<EnvironmentConfigLayerStack, ReadEnvironmentConfigError> {
|
|
let layers = stack
|
|
.layers
|
|
.into_iter()
|
|
.map(|layer| {
|
|
let toml = toml::to_string(&layer.toml).map_err(|error| {
|
|
ReadEnvironmentConfigError::Internal(format!(
|
|
"failed to serialize executor-local config: {error}"
|
|
))
|
|
})?;
|
|
Ok(EnvironmentConfigLayer {
|
|
source: source_name(&layer.source),
|
|
base_dir: PathUri::from_abs_path(&layer.base_dir),
|
|
toml,
|
|
})
|
|
})
|
|
.collect::<Result<Vec<_>, ReadEnvironmentConfigError>>()?;
|
|
Ok(EnvironmentConfigLayerStack {
|
|
layers,
|
|
cloud_insertion_index: stack.cloud_insertion_index,
|
|
})
|
|
}
|