Files
codex/codex-rs/rmcp-client/tests/mcp_2026_stdio_discovery.rs
T
thomas be2e4afcd7 Add MCP 2026-07-28 discovery support (#35724)
## What changed

- Add an opt-in `mcp_2026_07_28` protocol mode while preserving the legacy
  lifecycle by default.
- Negotiate the new protocol over streamable HTTP with `server/discover`,
  including bounded responses, redirect protection, and fallback only when a
  response establishes that the endpoint is legacy-only.
- Require stdio servers to opt in with
  `CODEX_MCP_PROTOCOL_VERSION=2026-07-28`, and add a bounded local stdio
  transport for the modern lifecycle.
- Consume paginated tool, resource, and resource-template catalogs in modern
  mode, reject repeated cursors, and retain discovered server identity.
- Reconnect reusable MCP clients when their selected protocol mode changes.

## Testing

- Cover HTTP JSON and SSE discovery, legacy fallback and rejection cases,
  redirects, retries, response limits, and pagination.
- Cover local and executor stdio discovery, protocol markers, message limits,
  and legacy compatibility.

GitOrigin-RevId: f6a78816e127d2a482292d63b91c8384f1595903
2026-07-28 05:52:12 +00:00

160 lines
5.7 KiB
Rust

use std::collections::HashMap;
use std::ffi::OsString;
use std::sync::Arc;
use std::time::Duration;
use codex_exec_server::Environment;
use codex_rmcp_client::ElicitationAction;
use codex_rmcp_client::ElicitationResponse;
use codex_rmcp_client::ExecutorStdioServerLauncher;
use codex_rmcp_client::LocalStdioServerLauncher;
use codex_rmcp_client::McpProtocolMode;
use codex_rmcp_client::RmcpClient;
use codex_rmcp_client::StdioServerLauncher;
use futures::FutureExt;
use rmcp::model::ClientCapabilities;
use rmcp::model::Implementation;
use rmcp::model::InitializeRequestParams;
use rmcp::model::ProtocolVersion;
use serde_json::json;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn modern_local_and_executor_stdio_discover_metadata_identity_and_catalogs()
-> anyhow::Result<()> {
let server = codex_utils_cargo_bin::cargo_bin("test_mcp_2026_discovery_stdio_server")?;
for executor in [false, true] {
let launcher: Arc<dyn StdioServerLauncher> = if executor {
Arc::new(ExecutorStdioServerLauncher::new(
Environment::default_for_tests().get_exec_backend(),
))
} else {
Arc::new(LocalStdioServerLauncher::new(std::env::current_dir()?))
};
let client = RmcpClient::new_stdio_client_with_protocol_mode(
server.clone().into(),
Vec::new(),
Some(HashMap::from([(
OsString::from("CODEX_MCP_PROTOCOL_VERSION"),
OsString::from("2026-07-28"),
)])),
&[],
Some(std::env::current_dir()?.to_string_lossy().into_owned()),
launcher,
McpProtocolMode::V20260728,
)
.await?;
client
.initialize(
InitializeRequestParams::new(
ClientCapabilities::default(),
Implementation::new("stdio-discovery-test", "1.0.0"),
)
.with_protocol_version(ProtocolVersion::V_2025_06_18),
Some(Duration::from_secs(10)),
Box::new(|_, _| {
async {
Ok(ElicitationResponse {
action: ElicitationAction::Decline,
content: None,
meta: None,
})
}
.boxed()
}),
)
.await?;
let tools = client
.list_tools(/*params*/ None, Some(Duration::from_secs(10)))
.await?;
assert_eq!(tools.tools[0].name.as_ref(), "stdio_echo");
let resources = client
.list_resources(/*params*/ None, Some(Duration::from_secs(10)))
.await?;
assert_eq!(resources.resources[0].uri, "test://stdio/resource");
let templates = client
.list_resource_templates(/*params*/ None, Some(Duration::from_secs(10)))
.await?;
assert_eq!(
templates.resource_templates[0].name,
"stdio resource template"
);
client.shutdown().await;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn legacy_stdio_preserves_existing_protocol_marker_environment() -> anyhow::Result<()> {
let server = codex_utils_cargo_bin::cargo_bin("test_stdio_server")?;
for executor in [false, true] {
for version in ["2026-07-28", "1999-01-01"] {
let launcher: Arc<dyn StdioServerLauncher> = if executor {
Arc::new(ExecutorStdioServerLauncher::new(
Environment::default_for_tests().get_exec_backend(),
))
} else {
Arc::new(LocalStdioServerLauncher::new(std::env::current_dir()?))
};
let client = RmcpClient::new_stdio_client_with_protocol_mode(
server.clone().into(),
Vec::new(),
Some(HashMap::from([(
OsString::from("CODEX_MCP_PROTOCOL_VERSION"),
OsString::from(version),
)])),
&[],
Some(std::env::current_dir()?.to_string_lossy().into_owned()),
launcher,
McpProtocolMode::Legacy,
)
.await?;
client
.initialize(
InitializeRequestParams::new(
ClientCapabilities::default(),
Implementation::new("stdio-legacy-environment-test", "1.0.0"),
)
.with_protocol_version(ProtocolVersion::V_2025_06_18),
Some(Duration::from_secs(10)),
Box::new(|_, _| {
async {
Ok(ElicitationResponse {
action: ElicitationAction::Decline,
content: None,
meta: None,
})
}
.boxed()
}),
)
.await?;
let result = client
.call_tool(
"echo".to_string(),
Some(json!({
"message": "legacy environment",
"env_var": "CODEX_MCP_PROTOCOL_VERSION",
})),
/*meta*/ None,
Some(Duration::from_secs(10)),
)
.await?;
assert_eq!(
result.structured_content,
Some(json!({
"echo": "ECHOING: legacy environment",
"env": version,
}))
);
client.shutdown().await;
}
}
Ok(())
}