mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
## 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
76 lines
2.7 KiB
Rust
76 lines
2.7 KiB
Rust
use std::any::TypeId;
|
|
|
|
use codex_exec_server::ExecServerError;
|
|
use pretty_assertions::assert_eq;
|
|
use rmcp::transport::DynamicTransportError;
|
|
use rmcp::transport::streamable_http_client::StreamableHttpError;
|
|
|
|
use crate::http_client_adapter::StreamableHttpClientAdapterError;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn retryable_initialize_error_includes_discovery_and_initialized_notification_context() {
|
|
let contexts = [
|
|
"send discover request",
|
|
"send initialize request",
|
|
"send initialized notification",
|
|
"receive initialize response",
|
|
];
|
|
|
|
assert_eq!(
|
|
contexts.map(|context| {
|
|
RmcpClient::is_retryable_client_initialize_error(&retryable_initialize_error(context))
|
|
}),
|
|
[true, true, true, false],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn retryable_streamable_http_error_includes_remote_body_stream_failure() {
|
|
let errors = [
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
|
ExecServerError::HttpRequest("error sending request for url".to_string()),
|
|
)),
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
|
ExecServerError::Server {
|
|
code: JSON_RPC_INTERNAL_ERROR_CODE,
|
|
message: "http/request failed: error sending request for url".to_string(),
|
|
},
|
|
)),
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
|
ExecServerError::Protocol(
|
|
"http response stream `http-1` failed: exec-server transport disconnected"
|
|
.to_string(),
|
|
),
|
|
)),
|
|
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
|
ExecServerError::Protocol(
|
|
"http response stream `http-1` received seq 2, expected 1".to_string(),
|
|
),
|
|
)),
|
|
StreamableHttpError::UnexpectedServerResponse("HTTP 502: upstream failure".into()),
|
|
StreamableHttpError::UnexpectedServerResponse("HTTP 400: bad request".into()),
|
|
];
|
|
|
|
assert_eq!(
|
|
errors.map(|error| RmcpClient::is_retryable_streamable_http_error(&error)),
|
|
[true, true, true, false, true, false],
|
|
);
|
|
}
|
|
|
|
fn retryable_initialize_error(context: &'static str) -> rmcp::service::ClientInitializeError {
|
|
rmcp::service::ClientInitializeError::TransportError {
|
|
error: DynamicTransportError::from_parts(
|
|
"streamable_http",
|
|
TypeId::of::<()>(),
|
|
Box::new(StreamableHttpError::Client(
|
|
StreamableHttpClientAdapterError::HttpRequest(ExecServerError::HttpRequest(
|
|
"error sending request for url".to_string(),
|
|
)),
|
|
)),
|
|
),
|
|
context: context.into(),
|
|
}
|
|
}
|