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:
stevenlee-oai
2026-08-20 20:53:17 +00:00
committed by copyberry
parent cc801d7048
commit a3bce23f3b
9 changed files with 138 additions and 10 deletions
+2 -2
View File
File diff suppressed because one or more lines are too long
+5 -4
View File
@@ -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
View File
@@ -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();
}
}
+4 -1
View File
@@ -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",
),