Repository navigation
streamable_http transport does not perform MCP protocol version negotiation #916
Description
Summary
The streamable_http server transport returns the ServerHandler's InitializeResult.protocol_version to the client verbatim, without the version-negotiation downgrade that the stdio (serve_server) path performs. As a result, a server whose initialize response advertises a protocol version newer than the client supports will have its connection refused over streamable_http, even though the same handler negotiates correctly over stdio.
This appears to violate the MCP lifecycle spec, which requires negotiation regardless of transport:
If the server supports the requested protocol version, it MUST respond with the same version. Otherwise, the server MUST respond with another protocol version it supports. This SHOULD be the latest version supported by the server.
Confirmed on rmcp 1.6.0 and 1.7.0.
Expected behavior
When the client sends initialize with protocolVersion: X and the handler's response advertises version Y where Y > X and X is a known/supported version, the server should respond with X (or otherwise the highest mutually supported version), the same way stdio does. The transport should not return a higher version than the client requested verbatim.
Actual behavior
Over streamable_http, the client receives Y (the handler's advertised version) unchanged. A client that does not support Y disconnects, e.g. Server's protocol version is not supported: 2025-11-25.
Root cause
The stdio path negotiates in crates/rmcp/src/service/server.rs (around lines 230-238): it compares the client's requested version against the handler's response and downgrades to the client's version when the response is higher.
let peer_protocol_version = peer_info.params.protocol_version.clone();
let protocol_version = match peer_protocol_version
.partial_cmp(&init_response.protocol_version)
.ok_or(ServerInitializeError::UnsupportedProtocolVersion(peer_protocol_version))?
{
std::cmp::Ordering::Less => peer_info.params.protocol_version.clone(),
_ => init_response.protocol_version,
};
init_response.protocol_version = protocol_version;The streamable_http session worker has no equivalent step. In crates/rmcp/src/transport/streamable_http_server/session/local.rs, the worker's run() forwards the handler's initialize response straight back to the client:
context.send_to_handler(request).await?;
let send_initialize_response = context.recv_from_handler().await?;
responder.send(Ok(send_initialize_response.message)) // ...A search of crates/rmcp/src/transport/streamable_http_server/ for partial_cmp, Ordering, or any negotiation logic returns nothing. Note also that ProtocolVersion::default() resolves to LATEST (2025-11-25), so any handler that uses the default (directly or via ServerHandler::get_info) and serves over streamable_http will hand the latest version to every client regardless of what the client requested.
Reproduction
- Implement a
ServerHandlerwhoseget_info()returnsInitializeResultwithprotocol_version = ProtocolVersion::default()(=2025-11-25). - Serve it over
streamable_http. - Send an
initializerequest with"protocolVersion": "2025-06-18". - Observe the response
protocolVersionis2025-11-25, not2025-06-18. The same handler over stdio responds with2025-06-18.
Suggested fix
Apply the same negotiation step used by serve_server in the streamable_http initialize path (in the session worker, after receiving the handler's initialize response and before sending it to the client). Sharing a single negotiation helper between the stdio and HTTP paths would keep the two transports consistent and spec-compliant.
Workaround
Servers can negotiate in their own initialize handler by reading request.protocol_version and setting the response's protocol_version to the client's requested version when supported, otherwise the server's latest. This composes correctly with the stdio path (the subsequent min comparison is a no-op) and is forward-compatible with a fix here.
Activity
fix: negotiate protocol version in handler (fixes modelcontextprotoco…
fix: negotiate protocol version in handler (#930)