Route MCP servers through explicit environments (#23583)

## Summary
- route each configured MCP server through an explicit per-server
`environment_id` instead of a manager-wide remote toggle
- default omitted `environment_id` to `local`, resolve named ids through
`EnvironmentManager`, and fail only the affected MCP server when an
explicit id is unknown
- keep local stdio on the existing local launcher path for now, while
named-environment stdio uses the selected environment backend and
requires an absolute `cwd`
- allow local HTTP MCP servers to keep using the ambient HTTP client
when no local `Environment` is configured; named-environment HTTP MCPs
use that environment's HTTP client

## Validation
- devbox Bazel build: `bazel build --bes_backend= --bes_results_url=
//codex-rs/cli:codex //codex-rs/rmcp-client:test_stdio_server
//codex-rs/rmcp-client:test_streamable_http_server`
- devbox app-server config matrix with real `config.toml` /
`environments.toml` files covering omitted local, explicit local,
omitted local under remote default, explicit remote stdio, local HTTP
without local env, explicit remote HTTP, local stdio without local env,
unknown explicit env, and remote stdio without `cwd`
This commit is contained in:
starr-openai authored and GitHub committed 2026-05-21 17:19:54 +02:00
1 parent 97b390fbd4
commit 298e5cfce1
36 files changed
+463 -431

No files matched your search

@@ -205,24 +205,20 @@ impl McpRequestProcessor {
.await;
let auth = self.auth_manager.auth().await;
let environment_manager = self.thread_manager.environment_manager();
// Status listing has no turn cwd. Prefer the configured default env,
// then configured local if present; do not manufacture a hidden local
// env in no-local modes.
let runtime_environment = McpRuntimeEnvironment::new(
environment_manager.default_or_local_environment(),
environment_manager.try_local_environment(),
config.cwd.to_path_buf(),
);
// This threadless status path has no turn cwd or turn-selected
// environment. Use config cwd only as the local stdio fallback; named
// environment stdio MCPs must declare their own absolute cwd.
let runtime_context =
McpRuntimeContext::new(Arc::clone(&environment_manager), config.cwd.to_path_buf());
tokio::spawn(async move {
Self::list_mcp_server_status_task(
outgoing,
request,
params,
config,
mcp_config,
auth,
runtime_environment,
runtime_context,
)
.await;
});
@@ -233,18 +229,16 @@ impl McpRequestProcessor {
outgoing: Arc<OutgoingMessageSender>,
request_id: ConnectionRequestId,
params: ListMcpServerStatusParams,
config: Config,
mcp_config: codex_mcp::McpConfig,
auth: Option<CodexAuth>,
runtime_environment: McpRuntimeEnvironment,
runtime_context: McpRuntimeContext,
) {
let result = Self::list_mcp_server_status_response(
request_id.request_id.to_string(),
params,
config,
mcp_config,
auth,
runtime_environment,
runtime_context,
)
.await;
outgoing.send_result(request_id, result).await;
@@ -253,10 +247,9 @@ impl McpRequestProcessor {
async fn list_mcp_server_status_response(
request_id: String,
params: ListMcpServerStatusParams,
config: Config,
mcp_config: codex_mcp::McpConfig,
auth: Option<CodexAuth>,
runtime_environment: McpRuntimeEnvironment,
runtime_context: McpRuntimeContext,
) -> Result<ListMcpServerStatusResponse, JSONRPCErrorError> {
let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) {
McpServerStatusDetail::Full => McpSnapshotDetail::Full,
@@ -267,31 +260,25 @@ impl McpRequestProcessor {
&mcp_config,
auth.as_ref(),
request_id,
runtime_environment,
runtime_context,
detail,
)
.await;
let effective_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
let McpServerStatusSnapshot {
tools_by_server,
resources,
resource_templates,
auth_statuses,
mut server_names,
} = snapshot;
let mut server_names: Vec<String> = config
.mcp_servers
.keys()
.cloned()
// Include runtime-added/plugin MCP servers that are present in the
// effective runtime config even when they are not user-declared in
// `config.mcp_servers`.
.chain(effective_servers.keys().cloned())
.chain(auth_statuses.keys().cloned())
.chain(resources.keys().cloned())
.chain(resource_templates.keys().cloned())
.collect();
server_names.extend(
auth_statuses
.keys()
.cloned()
.chain(resources.keys().cloned())
.chain(resource_templates.keys().cloned()),
);
server_names.sort();
server_names.dedup();
@@ -367,21 +354,18 @@ impl McpRequestProcessor {
.await;
let auth = self.auth_manager.auth().await;
let environment_manager = self.thread_manager.environment_manager();
// Resource reads without a thread have no turn cwd. Prefer the
// configured default env, then configured local if present; do not
// manufacture a hidden local env in no-local modes.
let runtime_environment = McpRuntimeEnvironment::new(
environment_manager.default_or_local_environment(),
environment_manager.try_local_environment(),
config.cwd.to_path_buf(),
);
// This threadless resource-read path has no turn cwd or turn-selected
// environment. Use config cwd only as the local stdio fallback; named
// environment stdio MCPs must declare their own absolute cwd.
let runtime_context =
McpRuntimeContext::new(Arc::clone(&environment_manager), config.cwd.to_path_buf());
let request_id = request_id.clone();
tokio::spawn(async move {
let result = read_mcp_resource_without_thread(
&mcp_config,
auth.as_ref(),
runtime_environment,
runtime_context,
&server,
&uri,
)