mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Discover HTTP MCP servers from selected executors (#39941)
## 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
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
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;
|
||||
@@ -11,6 +18,9 @@ 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}")]
|
||||
@@ -52,6 +62,79 @@ pub(crate) async fn read_environment_config(
|
||||
})
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user