Skip to content

Commit 6778f3a

Browse files
aibrahim-oaicodex
andcommitted
Prototype remote MCP stdio transport
Add MCP server environment selection and executor-backed stdio transport for remote MCP servers. Co-authored-by: Codex <noreply@openai.com>
1 parent dae5699 commit 6778f3a

42 files changed

Lines changed: 893 additions & 170 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

codex-rs/Cargo.lock

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

codex-rs/cli/src/mcp_cmd.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
297297

298298
let new_entry = McpServerConfig {
299299
transport: transport.clone(),
300+
environment: Default::default(),
300301
enabled: true,
301302
required: false,
302303
supports_parallel_tool_calls: false,

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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ fn codex_apps_mcp_server_config(config: &McpConfig, auth: Option<&CodexAuth>) ->
270270
http_headers,
271271
env_http_headers: None,
272272
},
273+
environment: Default::default(),
273274
enabled: true,
274275
required: false,
275276
supports_parallel_tool_calls: false,
@@ -363,6 +364,7 @@ pub async fn collect_mcp_snapshot_with_detail(
363364
submit_id,
364365
tx_event,
365366
sandbox_state,
367+
None,
366368
config.codex_home.clone(),
367369
codex_apps_tools_cache_key(auth),
368370
tool_plugin_provenance,
@@ -436,6 +438,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
436438
submit_id,
437439
tx_event,
438440
sandbox_state,
441+
None,
439442
config.codex_home.clone(),
440443
codex_apps_tools_cache_key(auth),
441444
tool_plugin_provenance,

codex-rs/codex-mcp/src/mcp/mod_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ async fn effective_mcp_servers_preserve_user_servers_and_add_codex_apps() {
193193
http_headers: None,
194194
env_http_headers: None,
195195
},
196+
environment: Default::default(),
196197
enabled: true,
197198
required: false,
198199
supports_parallel_tool_calls: false,
@@ -215,6 +216,7 @@ async fn effective_mcp_servers_preserve_user_servers_and_add_codex_apps() {
215216
http_headers: None,
216217
env_http_headers: None,
217218
},
219+
environment: Default::default(),
218220
enabled: true,
219221
required: false,
220222
supports_parallel_tool_calls: false,

codex-rs/codex-mcp/src/mcp/skill_dependencies.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ fn mcp_dependency_to_server_config(
119119
http_headers: None,
120120
env_http_headers: None,
121121
},
122+
environment: Default::default(),
122123
enabled: true,
123124
required: false,
124125
supports_parallel_tool_calls: false,
@@ -146,6 +147,7 @@ fn mcp_dependency_to_server_config(
146147
env_vars: Vec::new(),
147148
cwd: None,
148149
},
150+
environment: Default::default(),
149151
enabled: true,
150152
required: false,
151153
supports_parallel_tool_calls: false,

codex-rs/codex-mcp/src/mcp/skill_dependencies_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ fn collect_missing_respects_canonical_installed_key() {
3939
http_headers: None,
4040
env_http_headers: None,
4141
},
42+
environment: Default::default(),
4243
enabled: true,
4344
required: false,
4445
supports_parallel_tool_calls: false,
@@ -90,6 +91,7 @@ fn collect_missing_dedupes_by_canonical_key_but_preserves_original_name() {
9091
http_headers: None,
9192
env_http_headers: None,
9293
},
94+
environment: Default::default(),
9395
enabled: true,
9496
required: false,
9597
supports_parallel_tool_calls: false,

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

Lines changed: 89 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::approvals::ElicitationRequest;
4042
use codex_protocol::approvals::ElicitationRequestEvent;
4143
use codex_protocol::mcp::CallToolResult;
@@ -49,8 +51,11 @@ use codex_protocol::protocol::McpStartupStatus;
4951
use codex_protocol::protocol::McpStartupUpdateEvent;
5052
use codex_protocol::protocol::SandboxPolicy;
5153
use codex_rmcp_client::ElicitationResponse;
54+
use codex_rmcp_client::ExecutorStdioTransportRuntime;
55+
use codex_rmcp_client::LocalStdioTransportRuntime;
5256
use codex_rmcp_client::RmcpClient;
5357
use codex_rmcp_client::SendElicitation;
58+
use codex_rmcp_client::StdioTransportRuntime;
5459
use futures::future::BoxFuture;
5560
use futures::future::FutureExt;
5661
use futures::future::Shared;
@@ -479,6 +484,24 @@ impl ManagedClient {
479484
}
480485
}
481486

487+
/// Builds the stdio runtime for MCP servers that run in the executor
488+
/// environment.
489+
///
490+
/// The connection manager only decides placement. The returned trait object
491+
/// hides the implementation detail that remote stdio is backed by
492+
/// `process/start` plus process read/write calls, while local stdio is backed by
493+
/// a direct child process.
494+
fn build_remote_stdio_runtime(
495+
environment: Option<Arc<Environment>>,
496+
session_cwd: PathBuf,
497+
) -> Option<Arc<dyn StdioTransportRuntime>> {
498+
let environment = environment?;
499+
Some(Arc::new(ExecutorStdioTransportRuntime::new(
500+
environment.get_exec_backend(),
501+
session_cwd,
502+
)))
503+
}
504+
482505
#[derive(Clone)]
483506
struct AsyncManagedClient {
484507
client: Shared<BoxFuture<'static, Result<ManagedClient, StartupOutcomeError>>>,
@@ -500,6 +523,7 @@ impl AsyncManagedClient {
500523
elicitation_requests: ElicitationRequestManager,
501524
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
502525
tool_plugin_provenance: Arc<ToolPluginProvenance>,
526+
remote_stdio_runtime: Option<Arc<dyn StdioTransportRuntime>>,
503527
) -> Self {
504528
let tool_filter = ToolFilter::from_config(&config);
505529
let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot(
@@ -516,8 +540,15 @@ impl AsyncManagedClient {
516540
return Err(error.into());
517541
}
518542

519-
let client =
520-
Arc::new(make_rmcp_client(&server_name, config.transport, store_mode).await?);
543+
let client = Arc::new(
544+
make_rmcp_client(
545+
&server_name,
546+
config.clone(),
547+
store_mode,
548+
remote_stdio_runtime,
549+
)
550+
.await?,
551+
);
521552
match start_server_task(
522553
server_name,
523554
client,
@@ -724,6 +755,7 @@ impl McpConnectionManager {
724755
submit_id: String,
725756
tx_event: Sender<Event>,
726757
initial_sandbox_state: SandboxState,
758+
environment: Option<Arc<Environment>>,
727759
codex_home: PathBuf,
728760
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
729761
tool_plugin_provenance: ToolPluginProvenance,
@@ -738,6 +770,8 @@ impl McpConnectionManager {
738770
);
739771
let tool_plugin_provenance = Arc::new(tool_plugin_provenance);
740772
let startup_submit_id = submit_id.clone();
773+
let remote_stdio_runtime =
774+
build_remote_stdio_runtime(environment, initial_sandbox_state.sandbox_cwd.clone());
741775
let mcp_servers = mcp_servers.clone();
742776
for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) {
743777
if let Some(origin) = transport_origin(&cfg.transport) {
@@ -770,6 +804,7 @@ impl McpConnectionManager {
770804
elicitation_requests.clone(),
771805
codex_apps_tools_cache_context,
772806
Arc::clone(&tool_plugin_provenance),
807+
remote_stdio_runtime.clone(),
773808
);
774809
clients.insert(server_name.clone(), async_managed_client.clone());
775810
let tx_event = tx_event.clone();
@@ -1532,9 +1567,16 @@ struct StartServerTaskParams {
15321567

15331568
async fn make_rmcp_client(
15341569
server_name: &str,
1535-
transport: McpServerTransportConfig,
1570+
config: McpServerConfig,
15361571
store_mode: OAuthCredentialsStoreMode,
1572+
remote_stdio_runtime: Option<Arc<dyn StdioTransportRuntime>>,
15371573
) -> Result<RmcpClient, StartupOutcomeError> {
1574+
let McpServerConfig {
1575+
transport,
1576+
environment,
1577+
..
1578+
} = config;
1579+
15381580
match transport {
15391581
McpServerTransportConfig::Stdio {
15401582
command,
@@ -1550,7 +1592,22 @@ async fn make_rmcp_client(
15501592
.map(|(key, value)| (key.into(), value.into()))
15511593
.collect::<HashMap<_, _>>()
15521594
});
1553-
RmcpClient::new_stdio_client(command_os, args_os, env_os, &env_vars, cwd)
1595+
let runtime = match environment {
1596+
McpServerEnvironment::Local => {
1597+
Arc::new(LocalStdioTransportRuntime) as Arc<dyn StdioTransportRuntime>
1598+
}
1599+
McpServerEnvironment::Remote => remote_stdio_runtime.ok_or_else(|| {
1600+
StartupOutcomeError::from(anyhow!(
1601+
"remote MCP server `{server_name}` requires an executor environment"
1602+
))
1603+
})?,
1604+
};
1605+
1606+
// `RmcpClient` always sees an MCP stdio transport. The runtime
1607+
// trait hides whether that transport was created by spawning a
1608+
// local child process or by asking the executor to start the child
1609+
// and stream its stdin/stdout bytes over the process API.
1610+
RmcpClient::new_stdio_client(command_os, args_os, env_os, &env_vars, cwd, runtime)
15541611
.await
15551612
.map_err(|err| StartupOutcomeError::from(anyhow!(err)))
15561613
}
@@ -1559,23 +1616,34 @@ async fn make_rmcp_client(
15591616
http_headers,
15601617
env_http_headers,
15611618
bearer_token_env_var,
1562-
} => {
1563-
let resolved_bearer_token =
1564-
match resolve_bearer_token(server_name, bearer_token_env_var.as_deref()) {
1565-
Ok(token) => token,
1566-
Err(error) => return Err(error.into()),
1567-
};
1568-
RmcpClient::new_streamable_http_client(
1569-
server_name,
1570-
&url,
1571-
resolved_bearer_token,
1572-
http_headers,
1573-
env_http_headers,
1574-
store_mode,
1575-
)
1576-
.await
1577-
.map_err(StartupOutcomeError::from)
1578-
}
1619+
} => match environment {
1620+
McpServerEnvironment::Local => {
1621+
// Local streamable HTTP remains the existing reqwest path from
1622+
// the orchestrator process.
1623+
let resolved_bearer_token =
1624+
match resolve_bearer_token(server_name, bearer_token_env_var.as_deref()) {
1625+
Ok(token) => token,
1626+
Err(error) => return Err(error.into()),
1627+
};
1628+
RmcpClient::new_streamable_http_client(
1629+
server_name,
1630+
&url,
1631+
resolved_bearer_token,
1632+
http_headers,
1633+
env_http_headers,
1634+
store_mode,
1635+
)
1636+
.await
1637+
.map_err(StartupOutcomeError::from)
1638+
}
1639+
McpServerEnvironment::Remote => Err(StartupOutcomeError::from(anyhow!(
1640+
// Remote HTTP needs the future low-level executor
1641+
// `network/request` API so reqwest runs on the executor side.
1642+
// Do not fall back to local HTTP here; the config explicitly
1643+
// asked for remote placement.
1644+
"remote streamable HTTP MCP server `{server_name}` is not implemented yet"
1645+
))),
1646+
},
15791647
}
15801648
}
15811649

codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,7 @@ fn mcp_init_error_display_prompts_for_github_pat() {
755755
http_headers: None,
756756
env_http_headers: None,
757757
},
758+
environment: Default::default(),
758759
enabled: true,
759760
required: false,
760761
supports_parallel_tool_calls: false,
@@ -805,6 +806,7 @@ fn mcp_init_error_display_reports_generic_errors() {
805806
http_headers: None,
806807
env_http_headers: None,
807808
},
809+
environment: Default::default(),
808810
enabled: true,
809811
required: false,
810812
supports_parallel_tool_calls: false,

codex-rs/config/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub use mcp_edit::load_global_mcp_servers;
6666
pub use mcp_types::AppToolApproval;
6767
pub use mcp_types::McpServerConfig;
6868
pub use mcp_types::McpServerDisabledReason;
69+
pub use mcp_types::McpServerEnvironment;
6970
pub use mcp_types::McpServerToolConfig;
7071
pub use mcp_types::McpServerTransportConfig;
7172
pub use mcp_types::RawMcpServerConfig;

0 commit comments

Comments
 (0)