feat: send and validate MCP-Protocol-Version header (#675)

This commit is contained in:
Dale Seo 2026-02-24 11:08:49 -05:00 • committed by GitHub
commit 5fa012d163
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -11,14 +11,15 @@ use tokio_util::sync::CancellationToken;
use super::session::SessionManager;
use crate::{
RoleServer,
model::{ClientJsonRpcMessage, ClientRequest, GetExtensions},
model::{ClientJsonRpcMessage, ClientRequest, GetExtensions, ProtocolVersion},
serve_server,
service::serve_directly,
transport::{
OneshotTransport, TransportAdapterIdentity,
common::{
http_header::{
EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_SESSION_ID, JSON_MIME_TYPE,
EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_MCP_PROTOCOL_VERSION,
HEADER_SESSION_ID, JSON_MIME_TYPE,
},
server_side_http::{
BoxResponse, ServerSseMessage, accepted_response, expect_json,
@ -55,6 +56,46 @@ impl Default for StreamableHttpServerConfig {
}
}
#[expect(
clippy::result_large_err,
reason = "BoxResponse is intentionally large; matches other handlers in this file"
)]
/// Validates the `MCP-Protocol-Version` header on incoming HTTP requests.
///
/// Per the MCP 2025-06-18 spec:
/// - If the header is present but contains an unsupported version, return 400 Bad Request.
/// - If the header is absent, assume `2025-03-26` for backwards compatibility (no error).
fn validate_protocol_version_header(headers: &http::HeaderMap) -> Result<(), BoxResponse> {
if let Some(value) = headers.get(HEADER_MCP_PROTOCOL_VERSION) {
let version_str = value.to_str().map_err(|_| {
Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.body(
Full::new(Bytes::from(
"Bad Request: Invalid MCP-Protocol-Version header encoding",
))
.boxed(),
)
.expect("valid response")
})?;
let is_known = ProtocolVersion::KNOWN_VERSIONS
.iter()
.any(|v| v.as_str() == version_str);
if !is_known {
return Err(Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.body(
Full::new(Bytes::from(format!(
"Bad Request: Unsupported MCP-Protocol-Version: {version_str}"
)))
.boxed(),
)
.expect("valid response"));
}
}
Ok(())
}
/// # Streamable Http Server
///
/// ## Extract information from raw http request
@ -207,6 +248,8 @@ where
.body(Full::new(Bytes::from("Unauthorized: Session not found")).boxed())
.expect("valid response"));
}
// Validate MCP-Protocol-Version header (per 2025-06-18 spec)
validate_protocol_version_header(request.headers())?;
// check if last event id is provided
let last_event_id = request
.headers()
@ -320,6 +363,9 @@ where
.expect("valid response"));
}
// Validate MCP-Protocol-Version header (per 2025-06-18 spec)
validate_protocol_version_header(&part.headers)?;
// inject request part to extensions
match &mut message {
ClientJsonRpcMessage::Request(req) => {
@ -455,6 +501,14 @@ where
Ok(response)
}
} else {
// Stateless mode: validate MCP-Protocol-Version on non-init requests
let is_init = matches!(
&message,
ClientJsonRpcMessage::Request(req) if matches!(req.request, ClientRequest::InitializeRequest(_))
);
if !is_init {
validate_protocol_version_header(&part.headers)?;
}
let service = self
.get_service()
.map_err(internal_error_response("get service"))?;
@ -511,6 +565,8 @@ where
.body(Full::new(Bytes::from("Unauthorized: Session ID is required")).boxed())
.expect("valid response"));
};
// Validate MCP-Protocol-Version header (per 2025-06-18 spec)
validate_protocol_version_header(request.headers())?;
// close session
self.session_manager
.close_session(&session_id)