mirror of
https://github.com/openai/codex.git
synced 2026-10-25 07:55:34 +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:
@@ -120,7 +120,7 @@ impl Session {
|
||||
windows_sandbox_level,
|
||||
)
|
||||
.await;
|
||||
let mcp_config = self
|
||||
let mcp_projection = self
|
||||
.services
|
||||
.mcp_manager
|
||||
.runtime_config_for_step(
|
||||
@@ -135,6 +135,9 @@ impl Session {
|
||||
&ready_selected_capability_roots,
|
||||
executor_capability_discovery.as_deref(),
|
||||
)
|
||||
.await;
|
||||
let mcp_config = self
|
||||
.project_selected_environment_mcp_servers(config, &environments, mcp_projection)
|
||||
.await
|
||||
.config;
|
||||
let local_process_cwd = environments
|
||||
@@ -144,6 +147,17 @@ impl Session {
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
self.services.turn_environments.environment_manager(),
|
||||
local_process_cwd,
|
||||
)
|
||||
.with_selected_environments(
|
||||
environments
|
||||
.turn_environments()
|
||||
.map(|environment| {
|
||||
(
|
||||
environment.selection.environment_id.clone(),
|
||||
Arc::clone(&environment.environment),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
(mcp_config, runtime_context)
|
||||
}
|
||||
@@ -265,6 +279,13 @@ impl Session {
|
||||
executor_capability_discovery.as_deref(),
|
||||
)
|
||||
.await;
|
||||
let mcp_projection = self
|
||||
.project_selected_environment_mcp_servers(
|
||||
&desired.config,
|
||||
&desired.environments,
|
||||
mcp_projection,
|
||||
)
|
||||
.await;
|
||||
let selected_plugins = mcp_projection.selected_plugins.clone();
|
||||
let input = self.build_mcp_runtime_input(
|
||||
&desired,
|
||||
|
||||
@@ -7,10 +7,18 @@
|
||||
use super::session::SessionConfiguration;
|
||||
use super::*;
|
||||
use crate::mcp::McpRuntimeProjection;
|
||||
use codex_config::McpServerDisabledReason;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ElicitationReviewerHandle;
|
||||
use codex_mcp::McpEnvironmentAuthority;
|
||||
use codex_mcp::McpServerRegistration;
|
||||
use codex_mcp::McpServerSource;
|
||||
use codex_mcp::McpStartupPolicy;
|
||||
use codex_mcp::PreparedMcpCall;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub(super) struct McpDesiredState {
|
||||
pub(super) config: Arc<Config>,
|
||||
@@ -133,6 +141,137 @@ impl Session {
|
||||
self.services.mcp_runtime.validate_required_servers().await
|
||||
}
|
||||
|
||||
/// Adds effective executor-owned configuration from this exact thread snapshot.
|
||||
pub(super) fn project_selected_environment_mcp_servers<'a>(
|
||||
&'a self,
|
||||
config: &'a Config,
|
||||
environments: &'a TurnEnvironmentSnapshot,
|
||||
mut projection: McpRuntimeProjection,
|
||||
) -> BoxFuture<'a, McpRuntimeProjection> {
|
||||
Box::pin(async move {
|
||||
let mut catalog = None;
|
||||
let mut registered = HashSet::new();
|
||||
for selected in environments.turn_environments() {
|
||||
let environment = &selected.environment;
|
||||
if !environment.is_remote() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let environment_id = &selected.selection.environment_id;
|
||||
let servers = match environment
|
||||
.discover_http_mcp_servers(selected.cwd().clone())
|
||||
.await
|
||||
{
|
||||
Ok(servers) => servers,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
environment_id,
|
||||
%error,
|
||||
"failed to discover executor-local MCP servers"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for (name, mut server) in servers {
|
||||
if name == CODEX_APPS_MCP_SERVER_NAME
|
||||
|| !server.is_local_environment()
|
||||
|| projection
|
||||
.config
|
||||
.mcp_server_catalog
|
||||
.server(&name)
|
||||
.is_some_and(|existing| {
|
||||
existing.config().environment_id != *environment_id
|
||||
|| !matches!(
|
||||
existing.source(),
|
||||
McpServerSource::Plugin(_)
|
||||
| McpServerSource::SelectedPlugin(_)
|
||||
)
|
||||
})
|
||||
|| registered.contains(&name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Selected executors can attach after startup, so their MCPs are best effort.
|
||||
server.required = false;
|
||||
let McpServerTransportConfig::StreamableHttp {
|
||||
env_http_headers,
|
||||
http_headers_helper,
|
||||
..
|
||||
} = &server.transport
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if http_headers_helper.is_some()
|
||||
|| env_http_headers
|
||||
.as_ref()
|
||||
.is_some_and(|headers| !headers.is_empty())
|
||||
{
|
||||
tracing::warn!(
|
||||
environment_id,
|
||||
server = name,
|
||||
"executor-local HTTP header helpers are not supported"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
server.environment_id = environment_id.clone();
|
||||
if let Some(requirements) = config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.mcp_servers
|
||||
.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(),
|
||||
});
|
||||
}
|
||||
registered.insert(name.clone());
|
||||
catalog
|
||||
.get_or_insert_with(|| projection.config.mcp_server_catalog.to_builder())
|
||||
.register(McpServerRegistration::from_config(name, server));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(catalog) = catalog {
|
||||
let selections = self.services.turn_environments.selections();
|
||||
projection.config.mcp_server_catalog =
|
||||
catalog.build_with_environment_authority(|environment_id| {
|
||||
let Some(selection) = selections
|
||||
.iter()
|
||||
.find(|selection| selection.environment_id == environment_id)
|
||||
else {
|
||||
return if environment_id
|
||||
== codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID
|
||||
{
|
||||
McpEnvironmentAuthority::Unrestricted
|
||||
} else {
|
||||
McpEnvironmentAuthority::SelectedPluginsOnly
|
||||
};
|
||||
};
|
||||
match &selection.config {
|
||||
EnvironmentConfigState::FromThread => {
|
||||
McpEnvironmentAuthority::Unrestricted
|
||||
}
|
||||
EnvironmentConfigState::Pending | EnvironmentConfigState::Failed(_) => {
|
||||
McpEnvironmentAuthority::Unavailable
|
||||
}
|
||||
EnvironmentConfigState::Ready(config) => config
|
||||
.mcp_policy
|
||||
.as_ref()
|
||||
.map_or(McpEnvironmentAuthority::Unrestricted, |policy| {
|
||||
McpEnvironmentAuthority::Restricted(policy)
|
||||
}),
|
||||
}
|
||||
});
|
||||
}
|
||||
projection
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "mcp.runtime.refresh", skip_all)]
|
||||
pub(super) async fn publish_mcp_runtime(
|
||||
&self,
|
||||
@@ -141,6 +280,13 @@ impl Session {
|
||||
ready_selected_capability_roots: &[SelectedCapabilityRoot],
|
||||
elicitation_reviewer: Option<ElicitationReviewerHandle>,
|
||||
) {
|
||||
let mcp_projection = self
|
||||
.project_selected_environment_mcp_servers(
|
||||
&desired.config,
|
||||
&desired.environments,
|
||||
mcp_projection,
|
||||
)
|
||||
.await;
|
||||
let selected_plugins = mcp_projection.selected_plugins.clone();
|
||||
let input = self.build_mcp_runtime_input(
|
||||
desired,
|
||||
@@ -187,6 +333,18 @@ impl Session {
|
||||
let runtime_context = McpRuntimeContext::new(
|
||||
self.services.turn_environments.environment_manager(),
|
||||
desired.local_process_cwd.clone(),
|
||||
)
|
||||
.with_selected_environments(
|
||||
desired
|
||||
.environments
|
||||
.turn_environments()
|
||||
.map(|environment| {
|
||||
(
|
||||
environment.selection.environment_id.clone(),
|
||||
Arc::clone(&environment.environment),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let codex_apps_auth_manager =
|
||||
codex_mcp::host_owned_codex_apps_enabled(&mcp_config, auth.as_ref())
|
||||
|
||||
Reference in New Issue
Block a user