Files
codex/codex-rs/rmcp-client/src/protocol_mode.rs
T
Charlie Marsh 0bc73bbeb1 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
2026-10-26 19:37:06 +00:00

131 lines
4.4 KiB
Rust

use std::ffi::OsStr;
use std::io;
use rmcp::model::ProtocolVersion;
use rmcp::service::ClientLifecycleMode;
/// MCP compatibility policy selected once when a Codex session is created.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum McpProtocolMode {
/// Preserve the existing MCP initialization and OAuth behavior.
#[default]
Legacy,
/// Allow the MCP 2026-07-28 discovery and request lifecycle.
V20260728,
/// Probe an eligible local stdio server even when its launch config omits a version.
DiscoverLocalStdio,
}
impl McpProtocolMode {
/// Returns the newest protocol version this compatibility policy can use.
pub fn preferred_protocol_version(self) -> ProtocolVersion {
match self {
Self::Legacy => ProtocolVersion::V_2025_06_18,
Self::V20260728 | Self::DiscoverLocalStdio => ProtocolVersion::V_2026_07_28,
}
}
pub(crate) fn client_lifecycle(self) -> ClientLifecycleMode {
match self {
Self::Legacy => ClientLifecycleMode::Initialize,
Self::V20260728 | Self::DiscoverLocalStdio => ClientLifecycleMode::Auto {
preferred_versions: vec![ProtocolVersion::V_2026_07_28],
legacy_version: Some(ProtocolVersion::V_2025_06_18),
},
}
}
/// Selects the stdio handshake from the client policy and the server's declared version.
pub fn stdio_mode(self, requested_version: Option<&OsStr>) -> io::Result<Self> {
match (self, requested_version) {
(Self::Legacy, _) => Ok(Self::Legacy),
(Self::DiscoverLocalStdio, None) => Ok(Self::DiscoverLocalStdio),
(Self::V20260728, None) => Ok(Self::Legacy),
(_, Some(version)) if version == OsStr::new("2026-07-28") => Ok(Self::V20260728),
(_, Some(version)) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"unsupported CODEX_MCP_PROTOCOL_VERSION `{}` for stdio MCP server; expected `2026-07-28`",
version.to_string_lossy()
),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn protocol_modes_select_compatible_sdk_lifecycles() {
assert_eq!(
McpProtocolMode::Legacy.preferred_protocol_version(),
ProtocolVersion::V_2025_06_18
);
assert_eq!(
McpProtocolMode::Legacy.client_lifecycle(),
ClientLifecycleMode::Initialize
);
assert_eq!(
McpProtocolMode::V20260728.preferred_protocol_version(),
ProtocolVersion::V_2026_07_28
);
assert_eq!(
McpProtocolMode::V20260728.client_lifecycle(),
ClientLifecycleMode::Auto {
preferred_versions: vec![ProtocolVersion::V_2026_07_28],
legacy_version: Some(ProtocolVersion::V_2025_06_18),
}
);
}
#[test]
fn stdio_requires_both_the_modern_feature_and_a_server_opt_in() {
let modern_version = Some(OsStr::new("2026-07-28"));
assert_eq!(
McpProtocolMode::Legacy
.stdio_mode(/*requested_version*/ None)
.unwrap(),
McpProtocolMode::Legacy
);
assert_eq!(
McpProtocolMode::Legacy.stdio_mode(modern_version).unwrap(),
McpProtocolMode::Legacy
);
assert_eq!(
McpProtocolMode::V20260728
.stdio_mode(/*requested_version*/ None)
.unwrap(),
McpProtocolMode::Legacy
);
assert_eq!(
McpProtocolMode::V20260728
.stdio_mode(modern_version)
.unwrap(),
McpProtocolMode::V20260728
);
}
#[test]
fn stdio_rejects_unknown_protocol_markers() {
let error = McpProtocolMode::V20260728
.stdio_mode(Some(OsStr::new("1999-01-01")))
.expect_err("an unknown protocol marker must fail");
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("1999-01-01"));
}
#[test]
fn legacy_stdio_does_not_interpret_existing_protocol_markers() {
assert_eq!(
McpProtocolMode::Legacy
.stdio_mode(Some(OsStr::new("1999-01-01")))
.unwrap(),
McpProtocolMode::Legacy
);
}
}