Skip to content

Commit a01f83a

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

6 files changed

Lines changed: 89 additions & 22 deletions

File tree

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+
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+
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: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ use async_channel::Sender;
3535
use codex_async_utils::CancelErr;
3636
use codex_async_utils::OrCancelExt;
3737
use codex_config::Constrained;
38+
use codex_config::McpServerEnvironment;
3839
use codex_config::types::OAuthCredentialsStoreMode;
40+
use codex_exec_server::Environment;
3941
use codex_protocol::ToolName;
4042
use codex_protocol::approvals::ElicitationRequest;
4143
use codex_protocol::approvals::ElicitationRequestEvent;
@@ -50,6 +52,7 @@ use codex_protocol::protocol::McpStartupStatus;
5052
use codex_protocol::protocol::McpStartupUpdateEvent;
5153
use codex_protocol::protocol::SandboxPolicy;
5254
use codex_rmcp_client::ElicitationResponse;
55+
use codex_rmcp_client::ExecutorStdioServerLauncher;
5356
use codex_rmcp_client::LocalStdioServerLauncher;
5457
use codex_rmcp_client::RmcpClient;
5558
use codex_rmcp_client::SendElicitation;
@@ -493,6 +496,8 @@ impl AsyncManagedClient {
493496
elicitation_requests: ElicitationRequestManager,
494497
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
495498
tool_plugin_provenance: Arc<ToolPluginProvenance>,
499+
environment: Option<Arc<Environment>>,
500+
remote_stdio_cwd: PathBuf,
496501
) -> Self {
497502
let tool_filter = ToolFilter::from_config(&config);
498503
let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot(
@@ -509,8 +514,16 @@ impl AsyncManagedClient {
509514
return Err(error.into());
510515
}
511516

512-
let client =
513-
Arc::new(make_rmcp_client(&server_name, config.transport, store_mode).await?);
517+
let client = Arc::new(
518+
make_rmcp_client(
519+
&server_name,
520+
config.clone(),
521+
store_mode,
522+
environment,
523+
remote_stdio_cwd,
524+
)
525+
.await?,
526+
);
514527
match start_server_task(
515528
server_name,
516529
client,
@@ -710,6 +723,8 @@ impl McpConnectionManager {
710723
submit_id: String,
711724
tx_event: Sender<Event>,
712725
initial_sandbox_policy: SandboxPolicy,
726+
environment: Option<Arc<Environment>>,
727+
remote_stdio_cwd: PathBuf,
713728
codex_home: PathBuf,
714729
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
715730
tool_plugin_provenance: ToolPluginProvenance,
@@ -754,6 +769,8 @@ impl McpConnectionManager {
754769
elicitation_requests.clone(),
755770
codex_apps_tools_cache_context,
756771
Arc::clone(&tool_plugin_provenance),
772+
environment.clone(),
773+
remote_stdio_cwd.clone(),
757774
);
758775
clients.insert(server_name.clone(), async_managed_client.clone());
759776
let tx_event = tx_event.clone();
@@ -1483,9 +1500,17 @@ struct StartServerTaskParams {
14831500

14841501
async fn make_rmcp_client(
14851502
server_name: &str,
1486-
transport: McpServerTransportConfig,
1503+
config: McpServerConfig,
14871504
store_mode: OAuthCredentialsStoreMode,
1505+
exec_environment: Option<Arc<Environment>>,
1506+
remote_stdio_cwd: PathBuf,
14881507
) -> Result<RmcpClient, StartupOutcomeError> {
1508+
let McpServerConfig {
1509+
transport,
1510+
environment,
1511+
..
1512+
} = config;
1513+
14891514
match transport {
14901515
McpServerTransportConfig::Stdio {
14911516
command,
@@ -1501,7 +1526,26 @@ async fn make_rmcp_client(
15011526
.map(|(key, value)| (key.into(), value.into()))
15021527
.collect::<HashMap<_, _>>()
15031528
});
1504-
let launcher = Arc::new(LocalStdioServerLauncher) as Arc<dyn StdioServerLauncher>;
1529+
let launcher = match environment {
1530+
McpServerEnvironment::Local => {
1531+
Arc::new(LocalStdioServerLauncher) as Arc<dyn StdioServerLauncher>
1532+
}
1533+
McpServerEnvironment::Remote => {
1534+
let exec_environment = exec_environment.ok_or_else(|| {
1535+
StartupOutcomeError::from(anyhow!(
1536+
"remote MCP server `{server_name}` requires an executor environment"
1537+
))
1538+
})?;
1539+
Arc::new(ExecutorStdioServerLauncher::new(
1540+
exec_environment.get_exec_backend(),
1541+
remote_stdio_cwd,
1542+
))
1543+
}
1544+
};
1545+
1546+
// `RmcpClient` always sees a launched MCP stdio server. The
1547+
// launcher hides whether that means a local child process or an
1548+
// executor process whose stdin/stdout bytes cross the process API.
15051549
RmcpClient::new_stdio_client(command_os, args_os, env_os, &env_vars, cwd, launcher)
15061550
.await
15071551
.map_err(|err| StartupOutcomeError::from(anyhow!(err)))
@@ -1511,23 +1555,34 @@ async fn make_rmcp_client(
15111555
http_headers,
15121556
env_http_headers,
15131557
bearer_token_env_var,
1514-
} => {
1515-
let resolved_bearer_token =
1516-
match resolve_bearer_token(server_name, bearer_token_env_var.as_deref()) {
1517-
Ok(token) => token,
1518-
Err(error) => return Err(error.into()),
1519-
};
1520-
RmcpClient::new_streamable_http_client(
1521-
server_name,
1522-
&url,
1523-
resolved_bearer_token,
1524-
http_headers,
1525-
env_http_headers,
1526-
store_mode,
1527-
)
1528-
.await
1529-
.map_err(StartupOutcomeError::from)
1530-
}
1558+
} => match environment {
1559+
McpServerEnvironment::Local => {
1560+
// Local streamable HTTP remains the existing reqwest path from
1561+
// the orchestrator process.
1562+
let resolved_bearer_token =
1563+
match resolve_bearer_token(server_name, bearer_token_env_var.as_deref()) {
1564+
Ok(token) => token,
1565+
Err(error) => return Err(error.into()),
1566+
};
1567+
RmcpClient::new_streamable_http_client(
1568+
server_name,
1569+
&url,
1570+
resolved_bearer_token,
1571+
http_headers,
1572+
env_http_headers,
1573+
store_mode,
1574+
)
1575+
.await
1576+
.map_err(StartupOutcomeError::from)
1577+
}
1578+
McpServerEnvironment::Remote => Err(StartupOutcomeError::from(anyhow!(
1579+
// Remote HTTP needs the future low-level executor
1580+
// `network/request` API so reqwest runs on the executor side.
1581+
// Do not fall back to local HTTP here; the config explicitly
1582+
// asked for remote placement.
1583+
"remote streamable HTTP MCP server `{server_name}` is not implemented yet"
1584+
))),
1585+
},
15311586
}
15321587
}
15331588

codex-rs/core/src/codex.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2143,7 +2143,7 @@ impl Session {
21432143
code_mode_service: crate::tools::code_mode::CodeModeService::new(
21442144
config.js_repl_node_path.clone(),
21452145
),
2146-
environment,
2146+
environment: environment.clone(),
21472147
};
21482148
services
21492149
.model_client
@@ -2233,6 +2233,8 @@ impl Session {
22332233
INITIAL_SUBMIT_ID.to_owned(),
22342234
tx_event.clone(),
22352235
session_configuration.sandbox_policy.get().clone(),
2236+
environment.clone(),
2237+
session_configuration.cwd.to_path_buf(),
22362238
config.codex_home.to_path_buf(),
22372239
codex_apps_tools_cache_key(auth),
22382240
tool_plugin_provenance,
@@ -4564,6 +4566,8 @@ impl Session {
45644566
turn_context.sub_id.clone(),
45654567
self.get_tx_event(),
45664568
turn_context.sandbox_policy.get().clone(),
4569+
turn_context.environment.clone(),
4570+
turn_context.cwd.to_path_buf(),
45674571
config.codex_home.to_path_buf(),
45684572
codex_apps_tools_cache_key(auth.as_ref()),
45694573
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+
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)