fix: keep initialize on legacy protocol versions (#1228)

Claude-Session: https://claude.ai/code/session_01EPcBfiUWJJueJn5cve1sjp
This commit is contained in:
Dale Seo 2026-09-01 07:38:57 +09:00 • committed by GitHub
commit ad9832ec21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 369 additions and 120 deletions

View file

@ -322,12 +322,15 @@ macro_rules! server_handler_methods {
) -> impl Future<Output = Result<InitializeResult, McpError>> + MaybeSendFuture + '_ {
context.peer.set_peer_info(request.clone());
let mut info = self.get_info();
info.protocol_version = negotiate_protocol_version(
let negotiated = negotiate_protocol_version(
&request.protocol_version,
info.protocol_version,
std::mem::take(&mut info.protocol_version),
&self.supported_protocol_versions(),
);
std::future::ready(Ok(info))
std::future::ready(negotiated.map(|version| {
info.protocol_version = version;
info
}))
}
/// Return the protocol versions supported by this server.
///

View file

@ -193,12 +193,17 @@ pub enum PeerRequestAssociation {
Unknown { has_pending_outbound_request: bool },
}
/// Whether `version` predates `2026-07-28`, the revision that replaced the
/// `initialize` handshake with per-request metadata.
pub(crate) fn is_legacy_version(version: &ProtocolVersion) -> bool {
version.as_str() < ProtocolVersion::V_2026_07_28.as_str()
}
pub(crate) fn uses_legacy_lifecycle(
protocol_version: Option<&ProtocolVersion>,
uses_discover_lifecycle: bool,
) -> bool {
!uses_discover_lifecycle
&& protocol_version.is_none_or(|version| version < &ProtocolVersion::V_2026_07_28)
!uses_discover_lifecycle && protocol_version.is_none_or(is_legacy_version)
}
pub(crate) fn peer_request_association<Req: crate::model::GetExtensions, V>(

View file

@ -460,27 +460,60 @@ where
}
}
/// Echoes the client-requested version if the server supports it; otherwise
/// returns `server_fallback`.
/// Echoes the client-requested version if the server can serve it over the
/// `initialize` handshake; otherwise returns a legacy version the server does
/// support.
///
/// `server_supported` comes from [`Service::supported_protocol_versions`], so a
/// server that narrows that list is never made to answer `initialize` with a
/// version it cannot serve.
/// version it cannot serve. `2026-07-28` replaced the handshake with
/// per-request metadata, so a client naming that revision or later is answered
/// with the server's newest legacy version instead.
///
/// # Errors
///
/// Returns [`ErrorCode::UNSUPPORTED_PROTOCOL_VERSION`] when the server supports
/// no version that still has an `initialize` handshake.
///
/// [`ErrorCode::UNSUPPORTED_PROTOCOL_VERSION`]: crate::model::ErrorCode::UNSUPPORTED_PROTOCOL_VERSION
pub(crate) fn negotiate_protocol_version(
client_requested: &ProtocolVersion,
server_fallback: ProtocolVersion,
server_supported: &[ProtocolVersion],
) -> ProtocolVersion {
if server_supported.contains(client_requested) {
client_requested.clone()
) -> Result<ProtocolVersion, ErrorData> {
if is_legacy_version(client_requested) && server_supported.contains(client_requested) {
return Ok(client_requested.clone());
}
let legacy_fallback = if is_legacy_version(&server_fallback) {
Some(server_fallback)
} else {
newest_legacy_version(server_supported)
};
let Some(legacy_fallback) = legacy_fallback else {
tracing::warn!(
client_requested = %client_requested,
server_fallback = %server_fallback,
"client requested unsupported protocol version; falling back to server default"
"server supports no protocol version with an initialize handshake; rejecting"
);
server_fallback
}
return Err(ErrorData::unsupported_protocol_version(
client_requested.clone(),
server_supported,
));
};
tracing::warn!(
client_requested = %client_requested,
server_fallback = %legacy_fallback,
"client requested a protocol version unavailable over initialize; falling back to server default"
);
Ok(legacy_fallback)
}
/// The newest of `versions` that still has an `initialize` handshake.
fn newest_legacy_version(versions: &[ProtocolVersion]) -> Option<ProtocolVersion> {
versions
.iter()
.filter(|version| is_legacy_version(version))
.max_by(|left, right| left.as_str().cmp(right.as_str()))
.cloned()
}
fn missing_request_metadata_error(missing: &[&str]) -> ErrorData {
@ -493,6 +526,27 @@ fn missing_request_metadata_error(missing: &[&str]) -> ErrorData {
)
}
/// Sends `error` as the response to the `initialize` request and reports it as
/// the reason the handshake failed.
async fn report_initialize_failure<T>(
transport: &mut T,
error: ErrorData,
id: RequestId,
) -> ServerInitializeError
where
T: Transport<RoleServer> + 'static,
{
match transport
.send(ServerJsonRpcMessage::error(error.clone(), Some(id)))
.await
{
Ok(()) => ServerInitializeError::InitializeFailed(error),
Err(send_error) => {
ServerInitializeError::transport::<T>(send_error, "sending error response")
}
}
}
async fn serve_server_with_ct_inner<S, T>(
service: S,
transport: T,
@ -589,27 +643,21 @@ where
peer: peer.clone(),
};
// Send initialize response
let init_response = service.handle_request(request, context).await;
let mut init_response = match init_response {
let mut init_response = match service.handle_request(request, context).await {
Ok(ServerResult::InitializeResult(init_response)) => init_response,
Ok(result) => {
return Err(ServerInitializeError::UnexpectedInitializeResponse(result));
}
Err(e) => {
transport
.send(ServerJsonRpcMessage::error(e.clone(), Some(id)))
.await
.map_err(|error| {
ServerInitializeError::transport::<T>(error, "sending error response")
})?;
return Err(ServerInitializeError::InitializeFailed(e));
}
Err(e) => return Err(report_initialize_failure(&mut transport, e, id).await),
};
init_response.protocol_version = negotiate_protocol_version(
init_response.protocol_version = match negotiate_protocol_version(
&requested_protocol_version,
init_response.protocol_version,
&service.supported_protocol_versions(),
);
) {
Ok(version) => version,
Err(e) => return Err(report_initialize_failure(&mut transport, e, id).await),
};
// Update peer_info so context.protocol_version() reflects the negotiated
// version in all subsequent request handlers.
negotiated_peer_info.protocol_version = init_response.protocol_version.clone();

View file

@ -322,7 +322,7 @@ impl<S: Service<RoleServer>> Service<RoleServer> for NegotiatingStatelessHttpSer
&requested,
result.protocol_version.clone(),
&self.0.supported_protocol_versions(),
);
)?;
if let Some(peer_info) = peer.peer_info() {
let mut peer_info = (*peer_info).clone();
peer_info.protocol_version = result.protocol_version.clone();
@ -375,22 +375,30 @@ fn is_legacy_request(
validate_request_protocol_version_meta(headers, message)?;
}
// An `initialize` request selects legacy semantics whatever version it names:
// the handshake exists only in the revisions before 2026-07-28, so the
// version in its params never routes it to the stateless path. The
// handshake itself answers with a legacy version the server supports.
if matches!(
message,
Some(ClientJsonRpcMessage::Request(req))
if matches!(&req.request, ClientRequest::InitializeRequest(_))
) {
return Ok(true);
}
let uses_discover_lifecycle = matches!(
message,
Some(ClientJsonRpcMessage::Request(req))
if !matches!(&req.request, ClientRequest::InitializeRequest(_))
&& req
.request
.get_meta()
.missing_required_keys(&ProtocolVersion::V_2026_07_28)
.is_empty()
if req
.request
.get_meta()
.missing_required_keys(&ProtocolVersion::V_2026_07_28)
.is_empty()
);
let from_body = match message {
Some(ClientJsonRpcMessage::Request(req)) => match &req.request {
ClientRequest::InitializeRequest(init) => Some(init.params.protocol_version.clone()),
_ => req.request.get_meta().protocol_version(),
},
Some(ClientJsonRpcMessage::Request(req)) => req.request.get_meta().protocol_version(),
_ => None,
};
let version = from_body