fix: keep initialize on legacy protocol versions (#1228)
Claude-Session: https://claude.ai/code/session_01EPcBfiUWJJueJn5cve1sjp
This commit is contained in:
parent
4e74499783
commit
ad9832ec21
4 changed files with 369 additions and 120 deletions
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue