fix: negotiate protocol version in handler (#930)

* fix: negotiate protocol version in handler (fixes #916)

* fix: use server pinned version as fallback in default initialize handler
This commit is contained in:
actsalan 2026-06-29 10:29:06 -07:00 • committed by GitHub
commit 80a74795e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 247 additions and 6 deletions

View file

@ -7,6 +7,7 @@ use crate::{
model::{TaskSupport, *},
service::{
MaybeSendFuture, NotificationContext, RequestContext, RoleServer, Service, ServiceRole,
negotiate_protocol_version,
},
};
@ -202,8 +203,13 @@ macro_rules! server_handler_methods {
request: InitializeRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<InitializeResult, McpError>> + MaybeSendFuture + '_ {
context.peer.set_peer_info(request);
std::future::ready(Ok(self.get_info()))
context.peer.set_peer_info(request.clone());
let mut info = self.get_info();
info.protocol_version = negotiate_protocol_version(
&request.protocol_version,
info.protocol_version,
);
std::future::ready(Ok(info))
}
fn complete(
&self,

View file

@ -162,7 +162,7 @@ where
}
/// Echoes the client-requested version if known; otherwise returns `server_fallback`.
fn negotiate_protocol_version(
pub(crate) fn negotiate_protocol_version(
client_requested: &ProtocolVersion,
server_fallback: ProtocolVersion,
) -> ProtocolVersion {
@ -254,6 +254,11 @@ where
&peer_info.params.protocol_version,
init_response.protocol_version,
);
// Update peer_info so context.protocol_version() reflects the negotiated
// version in all subsequent request handlers.
let mut negotiated_peer_info = peer_info.params.clone();
negotiated_peer_info.protocol_version = init_response.protocol_version.clone();
peer.set_peer_info(negotiated_peer_info);
transport
.send(ServerJsonRpcMessage::response(
ServerResult::InitializeResult(init_response),

View file

@ -16,8 +16,9 @@ use super::session::{
use crate::{
RoleServer,
model::{
ClientJsonRpcMessage, ClientNotification, ClientRequest, ErrorData, GetExtensions,
InitializeRequest, InitializedNotification, JsonRpcError, ProtocolVersion, RequestId,
ClientCapabilities, ClientJsonRpcMessage, ClientNotification, ClientRequest, ErrorData,
GetExtensions, Implementation, InitializeRequest, InitializeRequestParams,
InitializedNotification, JsonRpcError, ProtocolVersion, RequestId,
},
serve_server,
service::serve_directly,
@ -1239,10 +1240,17 @@ where
.map_err(internal_error_response("get service"))?;
match message {
ClientJsonRpcMessage::Request(mut request) => {
// Build a peer_info so context.protocol_version() works inside handlers.
// serve_directly skips the handshake and receives None by default, making
// protocol_version() always return None in stateless mode. We reconstruct it:
// - initialize requests: version comes from the request body params
// - all other requests: version comes from the MCP-Protocol-Version header
// (already validated above; absent header defaults to 2025-03-26)
let peer_info = Self::peer_info_for_stateless_request(&request, &part.headers);
request.request.extensions_mut().insert(part);
let (transport, mut receiver) =
OneshotTransport::<RoleServer>::new(ClientJsonRpcMessage::Request(request));
let service = serve_directly(service, transport, None);
let service = serve_directly(service, transport, peer_info);
tokio::spawn(async move {
// on service created
let _ = service.waiting().await;
@ -1331,4 +1339,34 @@ where
}
Ok(accepted_response())
}
/// Build a `ClientInfo` (peer_info) for a stateless request so that
/// `context.protocol_version()` returns the correct value inside handlers.
///
/// `serve_directly` skips the MCP handshake and accepts `peer_info = None`,
/// which means `context.protocol_version()` is always `None` in stateless mode.
/// We reconstruct the protocol version from the available signal per request type:
/// - initialize: version is in the request body params (authoritative)
/// - all other requests: version is in the MCP-Protocol-Version header
/// (validated before this point; absent header defaults to 2025-03-26)
fn peer_info_for_stateless_request(
request: &crate::model::JsonRpcRequest<ClientRequest>,
headers: &HeaderMap,
) -> Option<InitializeRequestParams> {
let version = if let ClientRequest::InitializeRequest(ref init) = request.request {
init.params.protocol_version.clone()
} else {
headers
.get(HEADER_MCP_PROTOCOL_VERSION)
.and_then(|v| v.to_str().ok())
.and_then(|s| serde_json::from_value(serde_json::Value::String(s.to_owned())).ok())
.unwrap_or(ProtocolVersion::V_2025_03_26)
};
Some(InitializeRequestParams {
meta: None,
protocol_version: version,
capabilities: ClientCapabilities::default(),
client_info: Implementation::default(),
})
}
}