mirror of
https://github.com/openai/codex.git
synced 2026-10-17 17:08:55 +00:00
Update rmcp to 3.1.3 (#39798)
## What changed - Upgrade `rmcp` and `rmcp-macros` from 3.1.2 to 3.1.3. - Preserve authentication-required and retryable transport classifications when modern MCP discovery falls back to legacy initialization. - Prevent unrelated discovery errors from triggering legacy fallback while retaining their diagnostics. ## Testing - Cover authentication challenges and transient initialization failures during legacy fallback. - Verify that a failed OAuth issuer check does not consume the pending authorization state. GitOrigin-RevId: 957c0873fec1148883dd163fbaeb77d7593d8c53
This commit is contained in:
Generated
+2
-2
File diff suppressed because one or more lines are too long
Generated
+5
-4
@@ -11841,9 +11841,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "3.1.2"
|
||||
version = "3.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8dddc5b1924b9a59fba420166160ca2c4663a4e01803e52eda33070f56d63c8"
|
||||
checksum = "5f17072af977b0f86f714dbd64b3d37d0715bb63064f9d13483f0a1775813374"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.23.0",
|
||||
@@ -11853,6 +11853,7 @@ dependencies = [
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"indexmap 2.14.0",
|
||||
"oauth2",
|
||||
"pastey",
|
||||
"pin-project-lite",
|
||||
@@ -11876,9 +11877,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "3.1.2"
|
||||
version = "3.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6898e24cd16342b59bfa8a53c2c04b9cf62fc8a2cfea57b9c038b09984bfc521"
|
||||
checksum = "54817231d63a35330d5c3a5782a4a166fbe3b914a9faab440d3dd8bd37971890"
|
||||
dependencies = [
|
||||
"darling 0.24.0",
|
||||
"proc-macro2",
|
||||
|
||||
+1
-1
@@ -394,7 +394,7 @@ rcgen = { version = "0.14.7", default-features = false, features = [
|
||||
regex = "1.12.3"
|
||||
regex-lite = "0.1.8"
|
||||
reqwest = { version = "0.12", features = ["cookies"] }
|
||||
rmcp = { version = "=3.1.2", default-features = false }
|
||||
rmcp = { version = "=3.1.3", default-features = false }
|
||||
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
|
||||
rustix = { version = "1.1.4", features = ["net"] }
|
||||
rustls = { version = "0.23", default-features = false, features = [
|
||||
|
||||
@@ -802,7 +802,7 @@ fn legacy_discovery_fallback_response(
|
||||
{
|
||||
return ServerJsonRpcMessage::error(
|
||||
ErrorData::new(
|
||||
ErrorCode::INVALID_REQUEST,
|
||||
ErrorCode::HEADER_MISMATCH,
|
||||
"server/discover method-not-found response did not match its request ID",
|
||||
None,
|
||||
),
|
||||
@@ -855,6 +855,20 @@ fn legacy_discovery_fallback_response(
|
||||
Some(request.id.clone()),
|
||||
)
|
||||
} else {
|
||||
let mut response = response;
|
||||
if let JsonRpcMessage::Error(error) = &mut response
|
||||
&& !matches!(
|
||||
error.error.code,
|
||||
ErrorCode::METHOD_NOT_FOUND
|
||||
| ErrorCode::UNSUPPORTED_PROTOCOL_VERSION
|
||||
| ErrorCode::HEADER_MISMATCH
|
||||
| ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY
|
||||
)
|
||||
{
|
||||
// rmcp 3.1.3 falls back on other discovery errors, so mark unproven
|
||||
// rejections as modern failures while preserving their diagnostics.
|
||||
error.error.code = ErrorCode::HEADER_MISMATCH;
|
||||
}
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1111,6 +1111,18 @@ mod tests {
|
||||
expected_token_requests
|
||||
);
|
||||
assert_eq!(result.is_ok(), expected_token_requests == 1);
|
||||
|
||||
if expected_token_requests == 0 {
|
||||
state
|
||||
.handle_callback_with_issuer(
|
||||
"legitimate-code",
|
||||
&csrf_state,
|
||||
Some(authorization_issuer.as_str()),
|
||||
)
|
||||
.await
|
||||
.expect("issuer validation failures must preserve OAuth authorization state");
|
||||
assert_eq!(token_requests.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
server.abort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ pub fn is_authentication_required_error(error: &Error) -> bool {
|
||||
.is_some_and(auth_error_requires_authentication)
|
||||
|| source
|
||||
.downcast_ref::<ClientInitializeError>()
|
||||
.is_some_and(|error| {
|
||||
.is_some_and(|mut error| {
|
||||
while let ClientInitializeError::LegacyFallbackFailed { fallback, .. } = error {
|
||||
error = fallback;
|
||||
}
|
||||
matches!(
|
||||
error,
|
||||
ClientInitializeError::TransportError { error, .. }
|
||||
|
||||
@@ -121,6 +121,9 @@ impl RmcpClient {
|
||||
|
||||
fn is_retryable_client_initialize_error(error: &rmcp::service::ClientInitializeError) -> bool {
|
||||
match error {
|
||||
rmcp::service::ClientInitializeError::LegacyFallbackFailed { fallback, .. } => {
|
||||
Self::is_retryable_client_initialize_error(fallback)
|
||||
}
|
||||
rmcp::service::ClientInitializeError::TransportError { error, context }
|
||||
if matches!(
|
||||
context.as_ref(),
|
||||
|
||||
@@ -823,6 +823,101 @@ async fn modern_mode_falls_back_only_for_a_json_rpc_method_not_found() -> anyhow
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn modern_legacy_fallback_preserves_authentication_required_error() -> anyhow::Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
let observed = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
let recorded = Arc::clone(&observed);
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/mcp"))
|
||||
.respond_with(move |request: &Request| {
|
||||
let body: Value = request.body_json().expect("valid JSON-RPC request");
|
||||
let method = body["method"].as_str().expect("JSON-RPC method");
|
||||
recorded.lock().expect("requests lock").push(method.into());
|
||||
match method {
|
||||
"server/discover" => ResponseTemplate::new(200).set_body_json(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": body["id"],
|
||||
"error": {"code": -32601, "message": "method not found"},
|
||||
})),
|
||||
"initialize" => ResponseTemplate::new(401)
|
||||
.insert_header("www-authenticate", "Bearer error=\"invalid_token\""),
|
||||
other => panic!("unexpected authentication fallback request: {other}"),
|
||||
}
|
||||
})
|
||||
.expect(2)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = create_client(&server, McpProtocolMode::V20260728).await?;
|
||||
let error = initialize_client(&client)
|
||||
.await
|
||||
.expect_err("legacy fallback should surface the authentication challenge");
|
||||
assert!(
|
||||
codex_rmcp_client::is_authentication_required_error(&error),
|
||||
"legacy fallback must preserve the authentication-required classification: {error:#}"
|
||||
);
|
||||
assert_eq!(
|
||||
*observed.lock().expect("requests lock"),
|
||||
vec!["server/discover", "initialize"]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn modern_legacy_fallback_retries_transient_initialize_failures() -> anyhow::Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
let observed = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
let recorded = Arc::clone(&observed);
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/mcp"))
|
||||
.respond_with(move |request: &Request| {
|
||||
let body: Value = request.body_json().expect("valid JSON-RPC request");
|
||||
let method = body["method"].as_str().expect("JSON-RPC method");
|
||||
let initialize_attempt = {
|
||||
let mut requests = recorded.lock().expect("requests lock");
|
||||
requests.push(method.into());
|
||||
requests
|
||||
.iter()
|
||||
.filter(|request| request.as_str() == "initialize")
|
||||
.count()
|
||||
};
|
||||
match method {
|
||||
"server/discover" => ResponseTemplate::new(200).set_body_json(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": body["id"],
|
||||
"error": {"code": -32601, "message": "method not found"},
|
||||
})),
|
||||
"initialize" if initialize_attempt == 1 => {
|
||||
ResponseTemplate::new(503).set_body_string("service unavailable")
|
||||
}
|
||||
"initialize" => legacy_initialize_response(&body),
|
||||
"notifications/initialized" => ResponseTemplate::new(202),
|
||||
other => panic!("unexpected retry fallback request: {other}"),
|
||||
}
|
||||
})
|
||||
.expect(5)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = create_client(&server, McpProtocolMode::V20260728).await?;
|
||||
initialize_client(&client).await?;
|
||||
assert_eq!(
|
||||
*observed.lock().expect("requests lock"),
|
||||
vec![
|
||||
"server/discover",
|
||||
"initialize",
|
||||
"server/discover",
|
||||
"initialize",
|
||||
"notifications/initialized",
|
||||
]
|
||||
);
|
||||
client.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn modern_mode_falls_back_when_discovery_only_advertises_legacy_versions()
|
||||
-> anyhow::Result<()> {
|
||||
|
||||
@@ -100,7 +100,7 @@ async fn assert_expired_token_refresh(scenario: OAuthStartupScenario) -> anyhow:
|
||||
"/.well-known/oauth-authorization-server/mcp",
|
||||
),
|
||||
OAuthStartupScenario::ProtectedResourceMetadata => (
|
||||
format!("{resource_url}/"),
|
||||
format!("{resource_url}/?oauth=initialize"),
|
||||
"/mcp/",
|
||||
"/.well-known/oauth-authorization-server",
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user