Skip to content

Commit fdfe2ca

Browse files
aibrahim-oaicodex
andcommitted
Wire remote MCP stdio through executor
Use the MCP server experimental_environment string to choose local stdio or executor-backed stdio at client startup time. Co-authored-by: Codex <noreply@openai.com>
1 parent 97fd5d4 commit fdfe2ca

File tree

6 files changed

+78
-5
lines changed

6 files changed

+78
-5
lines changed

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/codex-mcp/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ anyhow = { workspace = true }
1616
async-channel = { workspace = true }
1717
codex-async-utils = { workspace = true }
1818
codex-config = { workspace = true }
19+
codex-exec-server = { workspace = true }
1920
codex-login = { workspace = true }
2021
codex-otel = { workspace = true }
2122
codex-plugin = { workspace = true }

codex-rs/codex-mcp/src/mcp/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ pub async fn collect_mcp_snapshot_with_detail(
355355
submit_id,
356356
tx_event,
357357
SandboxPolicy::new_read_only_policy(),
358+
/*environment*/ None,
359+
config.codex_home.clone(),
358360
config.codex_home.clone(),
359361
codex_apps_tools_cache_key(auth),
360362
tool_plugin_provenance,
@@ -421,6 +423,8 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
421423
submit_id,
422424
tx_event,
423425
SandboxPolicy::new_read_only_policy(),
426+
/*environment*/ None,
427+
config.codex_home.clone(),
424428
config.codex_home.clone(),
425429
codex_apps_tools_cache_key(auth),
426430
tool_plugin_provenance,

codex-rs/codex-mcp/src/mcp_connection_manager.rs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use codex_async_utils::CancelErr;
3636
use codex_async_utils::OrCancelExt;
3737
use codex_config::Constrained;
3838
use codex_config::types::OAuthCredentialsStoreMode;
39+
use codex_exec_server::Environment;
3940
use codex_protocol::ToolName;
4041
use codex_protocol::approvals::ElicitationRequest;
4142
use codex_protocol::approvals::ElicitationRequestEvent;
@@ -50,6 +51,7 @@ use codex_protocol::protocol::McpStartupStatus;
5051
use codex_protocol::protocol::McpStartupUpdateEvent;
5152
use codex_protocol::protocol::SandboxPolicy;
5253
use codex_rmcp_client::ElicitationResponse;
54+
use codex_rmcp_client::ExecutorStdioServerLauncher;
5355
use codex_rmcp_client::LocalStdioServerLauncher;
5456
use codex_rmcp_client::RmcpClient;
5557
use codex_rmcp_client::SendElicitation;
@@ -493,6 +495,8 @@ impl AsyncManagedClient {
493495
elicitation_requests: ElicitationRequestManager,
494496
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
495497
tool_plugin_provenance: Arc<ToolPluginProvenance>,
498+
environment: Option<Arc<Environment>>,
499+
remote_stdio_cwd: PathBuf,
496500
) -> Self {
497501
let tool_filter = ToolFilter::from_config(&config);
498502
let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot(
@@ -509,8 +513,16 @@ impl AsyncManagedClient {
509513
return Err(error.into());
510514
}
511515

512-
let client =
513-
Arc::new(make_rmcp_client(&server_name, config.transport, store_mode).await?);
516+
let client = Arc::new(
517+
make_rmcp_client(
518+
&server_name,
519+
config.clone(),
520+
store_mode,
521+
environment,
522+
remote_stdio_cwd,
523+
)
524+
.await?,
525+
);
514526
match start_server_task(
515527
server_name,
516528
client,
@@ -710,6 +722,8 @@ impl McpConnectionManager {
710722
submit_id: String,
711723
tx_event: Sender<Event>,
712724
initial_sandbox_policy: SandboxPolicy,
725+
environment: Option<Arc<Environment>>,
726+
remote_stdio_cwd: PathBuf,
713727
codex_home: PathBuf,
714728
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
715729
tool_plugin_provenance: ToolPluginProvenance,
@@ -754,6 +768,8 @@ impl McpConnectionManager {
754768
elicitation_requests.clone(),
755769
codex_apps_tools_cache_context,
756770
Arc::clone(&tool_plugin_provenance),
771+
environment.clone(),
772+
remote_stdio_cwd.clone(),
757773
);
758774
clients.insert(server_name.clone(), async_managed_client.clone());
759775
let tx_event = tx_event.clone();
@@ -1484,9 +1500,26 @@ struct StartServerTaskParams {
14841500

14851501
async fn make_rmcp_client(
14861502
server_name: &str,
1487-
transport: McpServerTransportConfig,
1503+
config: McpServerConfig,
14881504
store_mode: OAuthCredentialsStoreMode,
1505+
exec_environment: Option<Arc<Environment>>,
1506+
remote_stdio_cwd: PathBuf,
14891507
) -> Result<RmcpClient, StartupOutcomeError> {
1508+
let McpServerConfig {
1509+
transport,
1510+
experimental_environment,
1511+
..
1512+
} = config;
1513+
let remote_environment = match experimental_environment.as_deref() {
1514+
None | Some("local") => false,
1515+
Some("remote") => true,
1516+
Some(environment) => {
1517+
return Err(StartupOutcomeError::from(anyhow!(
1518+
"unsupported experimental_environment `{environment}` for MCP server `{server_name}`"
1519+
)));
1520+
}
1521+
};
1522+
14901523
match transport {
14911524
McpServerTransportConfig::Stdio {
14921525
command,
@@ -1502,7 +1535,23 @@ async fn make_rmcp_client(
15021535
.map(|(key, value)| (key.into(), value.into()))
15031536
.collect::<HashMap<_, _>>()
15041537
});
1505-
let launcher = Arc::new(LocalStdioServerLauncher) as Arc<dyn StdioServerLauncher>;
1538+
let launcher = if remote_environment {
1539+
let exec_environment = exec_environment.ok_or_else(|| {
1540+
StartupOutcomeError::from(anyhow!(
1541+
"remote MCP server `{server_name}` requires an executor environment"
1542+
))
1543+
})?;
1544+
Arc::new(ExecutorStdioServerLauncher::new(
1545+
exec_environment.get_exec_backend(),
1546+
remote_stdio_cwd,
1547+
))
1548+
} else {
1549+
Arc::new(LocalStdioServerLauncher) as Arc<dyn StdioServerLauncher>
1550+
};
1551+
1552+
// `RmcpClient` always sees a launched MCP stdio server. The
1553+
// launcher hides whether that means a local child process or an
1554+
// executor process whose stdin/stdout bytes cross the process API.
15061555
RmcpClient::new_stdio_client(command_os, args_os, env_os, &env_vars, cwd, launcher)
15071556
.await
15081557
.map_err(|err| StartupOutcomeError::from(anyhow!(err)))
@@ -1513,6 +1562,18 @@ async fn make_rmcp_client(
15131562
env_http_headers,
15141563
bearer_token_env_var,
15151564
} => {
1565+
if remote_environment {
1566+
return Err(StartupOutcomeError::from(anyhow!(
1567+
// Remote HTTP needs the future low-level executor
1568+
// `network/request` API so reqwest runs on the executor side.
1569+
// Do not fall back to local HTTP here; the config explicitly
1570+
// asked for remote placement.
1571+
"remote streamable HTTP MCP server `{server_name}` is not implemented yet"
1572+
)));
1573+
}
1574+
1575+
// Local streamable HTTP remains the existing reqwest path from
1576+
// the orchestrator process.
15161577
let resolved_bearer_token =
15171578
match resolve_bearer_token(server_name, bearer_token_env_var.as_deref()) {
15181579
Ok(token) => token,

codex-rs/core/src/codex.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2154,7 +2154,7 @@ impl Session {
21542154
code_mode_service: crate::tools::code_mode::CodeModeService::new(
21552155
config.js_repl_node_path.clone(),
21562156
),
2157-
environment,
2157+
environment: environment.clone(),
21582158
};
21592159
services
21602160
.model_client
@@ -2248,6 +2248,8 @@ impl Session {
22482248
INITIAL_SUBMIT_ID.to_owned(),
22492249
tx_event.clone(),
22502250
session_configuration.sandbox_policy.get().clone(),
2251+
environment.clone(),
2252+
session_configuration.cwd.to_path_buf(),
22512253
config.codex_home.to_path_buf(),
22522254
codex_apps_tools_cache_key(auth),
22532255
tool_plugin_provenance,
@@ -4584,6 +4586,8 @@ impl Session {
45844586
turn_context.sub_id.clone(),
45854587
self.get_tx_event(),
45864588
turn_context.sandbox_policy.get().clone(),
4589+
turn_context.environment.clone(),
4590+
turn_context.cwd.to_path_buf(),
45874591
config.codex_home.to_path_buf(),
45884592
codex_apps_tools_cache_key(auth.as_ref()),
45894593
tool_plugin_provenance,

codex-rs/core/src/connectors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
233233
INITIAL_SUBMIT_ID.to_owned(),
234234
tx_event,
235235
SandboxPolicy::new_read_only_policy(),
236+
/*environment*/ None,
237+
config.codex_home.to_path_buf(),
236238
config.codex_home.to_path_buf(),
237239
codex_apps_tools_cache_key(auth.as_ref()),
238240
ToolPluginProvenance::default(),

0 commit comments

Comments
 (0)