Add opt-in protocol discovery for local stdio MCP servers (#57210)

## Why

Stdio servers without `CODEX_MCP_PROTOCOL_VERSION` use the legacy handshake, even when they support MCP `2026-07-28`.

## What changed

- Add the disabled-by-default `mcp_connection_sharing` feature to enable discovery for eligible local stdio servers.
- Recover from failed or late discovery with a fresh handshake when needed, waiting for the previous helper to release resources before restarting.
- Retain a working legacy connection when an upgrade attempt fails for the same account, except on authentication errors.
- Keep tool catalogs separate by protocol and invalidate legacy catalogs when discovery negotiates the modern protocol.

## Testing

Add regression coverage for failed and late discovery, legacy fallback, protocol-specific catalog reuse, message limits, and Windows descendant cleanup.

GitOrigin-RevId: 36c675ef3b81d32fccf06ccbddda2312cfe602c9
This commit is contained in:
Charlie Marsh
2026-10-26 19:37:06 +00:00
committed by copyberry
parent 33b16a9306
commit 0bc73bbeb1
30 changed files with 1645 additions and 316 deletions
+6
View File
@@ -1143,6 +1143,9 @@
"mcp_2026_07_28": {
"type": "boolean"
},
"mcp_connection_sharing": {
"type": "boolean"
},
"mcp_oauth_refresh_coordination": {
"type": "boolean"
},
@@ -7751,6 +7754,9 @@
"mcp_2026_07_28": {
"type": "boolean"
},
"mcp_connection_sharing": {
"type": "boolean"
},
"mcp_oauth_refresh_coordination": {
"type": "boolean"
},
@@ -124,6 +124,7 @@ impl ManagedFeatures {
let mut values = self.get().clone();
for feature in [
Feature::EnableMcpApps,
Feature::McpConnectionSharing,
Feature::SecretAuthStorage,
Feature::McpOAuthRefreshCoordination,
] {
+1
View File
@@ -1957,6 +1957,7 @@ impl Config {
Vec::new()
},
protocol_mode: self.mcp_protocol_mode(),
discover_local_stdio: self.features.enabled(Feature::McpConnectionSharing),
host_owned_apps_protocol_mode: if self.features.enabled(Feature::CodexAppsMcp20260728) {
McpProtocolMode::V20260728
} else {
@@ -67,6 +67,7 @@ impl Config {
// These operational controls accept host defaults beneath explicit layer values.
// Enterprise opt-in always comes from trusted layers instead.
for feature in [
Feature::McpConnectionSharing,
Feature::SecretAuthStorage,
Feature::McpOAuthRefreshCoordination,
] {
+2 -1
View File
@@ -40,7 +40,8 @@ async fn wait_for_process_exit_inner(pid: String) -> anyhow::Result<()> {
pub async fn wait_for_process_exit(pid: &str) -> anyhow::Result<()> {
let pid = pid.to_string();
tokio::time::timeout(Duration::from_secs(2), wait_for_process_exit_inner(pid))
// A stdio process group can keep its leader's PID for the two-second shutdown grace.
tokio::time::timeout(Duration::from_secs(5), wait_for_process_exit_inner(pid))
.await
.context("timed out waiting for process to exit")??;
+7 -1
View File
@@ -19,6 +19,7 @@ use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::McpServerContribution;
use codex_extension_api::McpServerContributionContext;
use codex_extension_api::McpServerContributor;
use codex_features::Feature;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_protocol::mcp::McpServerConnectionStatus;
use codex_protocol::models::PermissionProfile;
@@ -700,6 +701,11 @@ async fn cached_mcp_startup_is_eager_for_root_and_lazy_for_subagents() -> anyhow
/*sandbox*/ None,
)
.await?;
// The subagent can reuse the parent's legacy catalog even when discovery is enabled.
let mut subagent_config = fixture.config.clone();
subagent_config
.features
.enable(Feature::McpConnectionSharing)?;
let NewThread {
thread: second_thread,
..
@@ -713,7 +719,7 @@ async fn cached_mcp_startup_is_eager_for_root_and_lazy_for_subagents() -> anyhow
agent_nickname: None,
agent_role: None,
})),
..StartThreadOptions::new(fixture.config.clone())
..StartThreadOptions::new(subagent_config)
})
.await?;
second_thread.submit(Op::Interrupt).await?;