@@ -36,6 +36,7 @@ use codex_async_utils::CancelErr;
3636use codex_async_utils:: OrCancelExt ;
3737use codex_config:: Constrained ;
3838use codex_config:: types:: OAuthCredentialsStoreMode ;
39+ use codex_exec_server:: Environment ;
3940use codex_protocol:: ToolName ;
4041use codex_protocol:: approvals:: ElicitationRequest ;
4142use codex_protocol:: approvals:: ElicitationRequestEvent ;
@@ -50,6 +51,7 @@ use codex_protocol::protocol::McpStartupStatus;
5051use codex_protocol:: protocol:: McpStartupUpdateEvent ;
5152use codex_protocol:: protocol:: SandboxPolicy ;
5253use codex_rmcp_client:: ElicitationResponse ;
54+ use codex_rmcp_client:: ExecutorStdioServerLauncher ;
5355use codex_rmcp_client:: LocalStdioServerLauncher ;
5456use codex_rmcp_client:: RmcpClient ;
5557use codex_rmcp_client:: SendElicitation ;
@@ -493,6 +495,7 @@ impl AsyncManagedClient {
493495 elicitation_requests : ElicitationRequestManager ,
494496 codex_apps_tools_cache_context : Option < CodexAppsToolsCacheContext > ,
495497 tool_plugin_provenance : Arc < ToolPluginProvenance > ,
498+ runtime_environment : McpRuntimeEnvironment ,
496499 ) -> Self {
497500 let tool_filter = ToolFilter :: from_config ( & config) ;
498501 let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot (
@@ -509,8 +512,15 @@ impl AsyncManagedClient {
509512 return Err ( error. into ( ) ) ;
510513 }
511514
512- let client =
513- Arc :: new ( make_rmcp_client ( & server_name, config. transport , store_mode) . await ?) ;
515+ let client = Arc :: new (
516+ make_rmcp_client (
517+ & server_name,
518+ config. clone ( ) ,
519+ store_mode,
520+ runtime_environment,
521+ )
522+ . await ?,
523+ ) ;
514524 match start_server_task (
515525 server_name,
516526 client,
@@ -650,6 +660,37 @@ pub struct McpConnectionManager {
650660 elicitation_requests : ElicitationRequestManager ,
651661}
652662
663+ /// Runtime placement information used when starting MCP server transports.
664+ ///
665+ /// `McpConfig` describes what servers exist. This value describes where those
666+ /// servers should run for the current caller. Keep it explicit at manager
667+ /// construction time so status/snapshot paths and real sessions make the same
668+ /// local-vs-remote decision. `fallback_cwd` is not a per-server override; it is
669+ /// used only when an executor-backed stdio server omits `cwd` and the executor
670+ /// API still needs a concrete process working directory.
671+ #[ derive( Clone ) ]
672+ pub struct McpRuntimeEnvironment {
673+ environment : Arc < Environment > ,
674+ fallback_cwd : PathBuf ,
675+ }
676+
677+ impl McpRuntimeEnvironment {
678+ pub fn new ( environment : Arc < Environment > , fallback_cwd : PathBuf ) -> Self {
679+ Self {
680+ environment,
681+ fallback_cwd,
682+ }
683+ }
684+
685+ fn environment ( & self ) -> Arc < Environment > {
686+ Arc :: clone ( & self . environment )
687+ }
688+
689+ fn fallback_cwd ( & self ) -> PathBuf {
690+ self . fallback_cwd . clone ( )
691+ }
692+ }
693+
653694impl McpConnectionManager {
654695 pub fn configured_servers ( & self , config : & McpConfig ) -> HashMap < String , McpServerConfig > {
655696 configured_mcp_servers ( config)
@@ -710,6 +751,7 @@ impl McpConnectionManager {
710751 submit_id : String ,
711752 tx_event : Sender < Event > ,
712753 initial_sandbox_policy : SandboxPolicy ,
754+ runtime_environment : McpRuntimeEnvironment ,
713755 codex_home : PathBuf ,
714756 codex_apps_tools_cache_key : CodexAppsToolsCacheKey ,
715757 tool_plugin_provenance : ToolPluginProvenance ,
@@ -754,6 +796,7 @@ impl McpConnectionManager {
754796 elicitation_requests. clone ( ) ,
755797 codex_apps_tools_cache_context,
756798 Arc :: clone ( & tool_plugin_provenance) ,
799+ runtime_environment. clone ( ) ,
757800 ) ;
758801 clients. insert ( server_name. clone ( ) , async_managed_client. clone ( ) ) ;
759802 let tx_event = tx_event. clone ( ) ;
@@ -1484,9 +1527,25 @@ struct StartServerTaskParams {
14841527
14851528async fn make_rmcp_client (
14861529 server_name : & str ,
1487- transport : McpServerTransportConfig ,
1530+ config : McpServerConfig ,
14881531 store_mode : OAuthCredentialsStoreMode ,
1532+ runtime_environment : McpRuntimeEnvironment ,
14891533) -> Result < RmcpClient , StartupOutcomeError > {
1534+ let McpServerConfig {
1535+ transport,
1536+ experimental_environment,
1537+ ..
1538+ } = config;
1539+ let remote_environment = match experimental_environment. as_deref ( ) {
1540+ None | Some ( "local" ) => false ,
1541+ Some ( "remote" ) => true ,
1542+ Some ( environment) => {
1543+ return Err ( StartupOutcomeError :: from ( anyhow ! (
1544+ "unsupported experimental_environment `{environment}` for MCP server `{server_name}`"
1545+ ) ) ) ;
1546+ }
1547+ } ;
1548+
14901549 match transport {
14911550 McpServerTransportConfig :: Stdio {
14921551 command,
@@ -1502,7 +1561,24 @@ async fn make_rmcp_client(
15021561 . map ( |( key, value) | ( key. into ( ) , value. into ( ) ) )
15031562 . collect :: < HashMap < _ , _ > > ( )
15041563 } ) ;
1505- let launcher = Arc :: new ( LocalStdioServerLauncher ) as Arc < dyn StdioServerLauncher > ;
1564+ let launcher = if remote_environment {
1565+ let exec_environment = runtime_environment. environment ( ) ;
1566+ if !exec_environment. is_remote ( ) {
1567+ return Err ( StartupOutcomeError :: from ( anyhow ! (
1568+ "remote MCP server `{server_name}` requires a remote executor environment"
1569+ ) ) ) ;
1570+ }
1571+ Arc :: new ( ExecutorStdioServerLauncher :: new (
1572+ exec_environment. get_exec_backend ( ) ,
1573+ runtime_environment. fallback_cwd ( ) ,
1574+ ) )
1575+ } else {
1576+ Arc :: new ( LocalStdioServerLauncher ) as Arc < dyn StdioServerLauncher >
1577+ } ;
1578+
1579+ // `RmcpClient` always sees a launched MCP stdio server. The
1580+ // launcher hides whether that means a local child process or an
1581+ // executor process whose stdin/stdout bytes cross the process API.
15061582 RmcpClient :: new_stdio_client ( command_os, args_os, env_os, & env_vars, cwd, launcher)
15071583 . await
15081584 . map_err ( |err| StartupOutcomeError :: from ( anyhow ! ( err) ) )
@@ -1513,6 +1589,23 @@ async fn make_rmcp_client(
15131589 env_http_headers,
15141590 bearer_token_env_var,
15151591 } => {
1592+ if remote_environment {
1593+ if !runtime_environment. environment ( ) . is_remote ( ) {
1594+ return Err ( StartupOutcomeError :: from ( anyhow ! (
1595+ "remote MCP server `{server_name}` requires a remote executor environment"
1596+ ) ) ) ;
1597+ }
1598+ return Err ( StartupOutcomeError :: from ( anyhow ! (
1599+ // Remote HTTP needs the future low-level executor
1600+ // `network/request` API so reqwest runs on the executor side.
1601+ // Do not fall back to local HTTP here; the config explicitly
1602+ // asked for remote placement.
1603+ "remote streamable HTTP MCP server `{server_name}` is not implemented yet"
1604+ ) ) ) ;
1605+ }
1606+
1607+ // Local streamable HTTP remains the existing reqwest path from
1608+ // the orchestrator process.
15161609 let resolved_bearer_token =
15171610 match resolve_bearer_token ( server_name, bearer_token_env_var. as_deref ( ) ) {
15181611 Ok ( token) => token,
0 commit comments