feat: send and validate MCP-Protocol-Version header (#675)
This commit is contained in:
parent
91e208efb7
commit
5fa012d163
1 changed files with 541 additions and 60 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue