use serde_json::{Map, Value, json};
use crate::ClientCapabilities;
pub const FINAL_PROTOCOL_VERSION: &str = "2026-07-28";
pub const SUPPORTED_FINAL_PROTOCOL_VERSIONS: &[&str] = &[FINAL_PROTOCOL_VERSION];
pub const MCP_PROTOCOL_VERSION_HEADER: &str = "MCP-Protocol-Version";
pub const MCP_METHOD_HEADER: &str = "Mcp-Method";
pub const MCP_NAME_HEADER: &str = "Mcp-Name";
pub const HEADER_MISMATCH_ERROR_CODE: i32 = -32020;
pub const MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE: i32 = -32021;
pub const UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE: i32 = -32022;
pub const MAX_REQUIRED_CAPABILITIES_ERROR_DATA_BYTES: usize = 64 * 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FinalProtocolVersion;
impl FinalProtocolVersion {
#[must_use]
pub const fn as_str(self) -> &'static str {
FINAL_PROTOCOL_VERSION
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RequestVersionMetadata<'a> {
pub header_version: Option<&'a str>,
pub body_version: Option<&'a str>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FinalRequestAdmission {
version: FinalProtocolVersion,
}
impl FinalRequestAdmission {
#[must_use]
pub const fn protocol_version(self) -> FinalProtocolVersion {
self.version
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FinalHttpRequestMetadata<'a> {
pub version: RequestVersionMetadata<'a>,
pub header_method: Option<&'a str>,
pub body_method: Option<&'a str>,
pub header_name: Option<&'a str>,
pub body_name: Option<&'a str>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HeaderMismatchReason {
MissingHeader,
MissingBodyVersion,
EmptyHeader,
EmptyBodyVersion,
HeaderBodyVersionMismatch,
MissingMethodHeader,
MissingBodyMethod,
EmptyMethodHeader,
EmptyBodyMethod,
HeaderBodyMethodMismatch,
MissingNameHeader,
MissingBodyName,
EmptyNameHeader,
EmptyBodyName,
HeaderBodyNameMismatch,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HeaderMismatchError {
reason: HeaderMismatchReason,
}
impl HeaderMismatchError {
#[must_use]
pub const fn reason(self) -> HeaderMismatchReason {
self.reason
}
#[must_use]
pub const fn jsonrpc_error_code(self) -> i32 {
HEADER_MISMATCH_ERROR_CODE
}
#[must_use]
pub const fn http_status(self) -> u16 {
400
}
#[must_use]
pub fn canonical_error_data(self) -> Option<Value> {
None
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UnsupportedProtocolVersionError {
requested: String,
}
impl UnsupportedProtocolVersionError {
#[must_use]
pub fn requested(&self) -> &str {
&self.requested
}
#[must_use]
pub const fn supported_versions(&self) -> &'static [&'static str] {
SUPPORTED_FINAL_PROTOCOL_VERSIONS
}
#[must_use]
pub const fn jsonrpc_error_code(&self) -> i32 {
UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE
}
#[must_use]
pub const fn http_status(&self) -> u16 {
400
}
#[must_use]
pub fn canonical_error_data(&self) -> Value {
json!({
"supported": self.supported_versions(),
"requested": self.requested(),
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RequiredCapabilitiesError {
NotAnObject,
TooLarge,
Encoding,
}
#[derive(Clone, Debug, PartialEq)]
pub struct MissingRequiredClientCapabilityError {
required_capabilities: Map<String, Value>,
}
impl MissingRequiredClientCapabilityError {
pub fn from_client_capabilities(
required_capabilities: &ClientCapabilities,
) -> Result<Self, RequiredCapabilitiesError> {
let required_capabilities = serde_json::to_value(required_capabilities)
.map_err(|_| RequiredCapabilitiesError::Encoding)?;
Self::new(required_capabilities)
}
pub fn new(required_capabilities: Value) -> Result<Self, RequiredCapabilitiesError> {
let Value::Object(required_capabilities) = required_capabilities else {
return Err(RequiredCapabilitiesError::NotAnObject);
};
let encoded_len = serde_json::to_vec(&required_capabilities)
.map_err(|_| RequiredCapabilitiesError::Encoding)?
.len();
if encoded_len > MAX_REQUIRED_CAPABILITIES_ERROR_DATA_BYTES {
return Err(RequiredCapabilitiesError::TooLarge);
}
Ok(Self {
required_capabilities,
})
}
#[must_use]
pub const fn required_capabilities(&self) -> &Map<String, Value> {
&self.required_capabilities
}
#[must_use]
pub const fn jsonrpc_error_code(&self) -> i32 {
MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE
}
#[must_use]
pub const fn http_status(&self) -> u16 {
400
}
#[must_use]
pub fn canonical_error_data(&self) -> Value {
json!({"requiredCapabilities": self.required_capabilities})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RequestAdmissionError {
HeaderMismatch(HeaderMismatchError),
UnsupportedProtocolVersion(UnsupportedProtocolVersionError),
}
impl RequestAdmissionError {
#[must_use]
pub const fn http_status(&self) -> u16 {
match self {
Self::HeaderMismatch(error) => error.http_status(),
Self::UnsupportedProtocolVersion(error) => error.http_status(),
}
}
#[must_use]
pub const fn jsonrpc_error_code(&self) -> i32 {
match self {
Self::HeaderMismatch(error) => error.jsonrpc_error_code(),
Self::UnsupportedProtocolVersion(error) => error.jsonrpc_error_code(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProtocolVersionError {
HeaderMismatch,
UnsupportedProtocolVersion { requested: String },
}
impl ProtocolVersionError {
#[must_use]
pub const fn http_status(&self) -> u16 {
400
}
#[must_use]
pub const fn jsonrpc_error_code(&self) -> i32 {
match self {
Self::HeaderMismatch => HEADER_MISMATCH_ERROR_CODE,
Self::UnsupportedProtocolVersion { .. } => UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE,
}
}
}
pub fn validate_final_protocol_version(
header_version: Option<&str>,
body_version: Option<&str>,
) -> Result<FinalProtocolVersion, ProtocolVersionError> {
admit_final_request(RequestVersionMetadata {
header_version,
body_version,
})
.map(|admission| admission.protocol_version())
.map_err(|error| match error {
RequestAdmissionError::HeaderMismatch(_) => ProtocolVersionError::HeaderMismatch,
RequestAdmissionError::UnsupportedProtocolVersion(error) => {
ProtocolVersionError::UnsupportedProtocolVersion {
requested: error.requested,
}
}
})
}
pub fn admit_final_request(
metadata: RequestVersionMetadata<'_>,
) -> Result<FinalRequestAdmission, RequestAdmissionError> {
let header_version = metadata
.header_version
.ok_or(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: HeaderMismatchReason::MissingHeader,
}))?;
let body_version = metadata
.body_version
.ok_or(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: HeaderMismatchReason::MissingBodyVersion,
}))?;
if header_version.is_empty() {
return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: HeaderMismatchReason::EmptyHeader,
}));
}
if body_version.is_empty() {
return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: HeaderMismatchReason::EmptyBodyVersion,
}));
}
if header_version != body_version {
return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: HeaderMismatchReason::HeaderBodyVersionMismatch,
}));
}
if header_version != FINAL_PROTOCOL_VERSION {
return Err(RequestAdmissionError::UnsupportedProtocolVersion(
UnsupportedProtocolVersionError {
requested: header_version.to_owned(),
},
));
}
Ok(FinalRequestAdmission {
version: FinalProtocolVersion,
})
}
pub fn admit_final_http_request(
metadata: FinalHttpRequestMetadata<'_>,
) -> Result<FinalRequestAdmission, RequestAdmissionError> {
let admission = admit_final_request(metadata.version)?;
let method = exact_nonempty_mirror(
metadata.header_method,
metadata.body_method,
HeaderMismatchReason::MissingMethodHeader,
HeaderMismatchReason::MissingBodyMethod,
HeaderMismatchReason::EmptyMethodHeader,
HeaderMismatchReason::EmptyBodyMethod,
HeaderMismatchReason::HeaderBodyMethodMismatch,
)?;
if requires_mcp_name(method) {
let _ = exact_nonempty_mirror(
metadata.header_name,
metadata.body_name,
HeaderMismatchReason::MissingNameHeader,
HeaderMismatchReason::MissingBodyName,
HeaderMismatchReason::EmptyNameHeader,
HeaderMismatchReason::EmptyBodyName,
HeaderMismatchReason::HeaderBodyNameMismatch,
)?;
}
Ok(admission)
}
fn exact_nonempty_mirror<'a>(
header: Option<&'a str>,
body: Option<&'a str>,
missing_header: HeaderMismatchReason,
missing_body: HeaderMismatchReason,
empty_header: HeaderMismatchReason,
empty_body: HeaderMismatchReason,
mismatch: HeaderMismatchReason,
) -> Result<&'a str, RequestAdmissionError> {
let header = header.ok_or(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: missing_header,
}))?;
let body = body.ok_or(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: missing_body,
}))?;
if header.is_empty() {
return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: empty_header,
}));
}
if body.is_empty() {
return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: empty_body,
}));
}
if header != body {
return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: mismatch,
}));
}
Ok(header)
}
fn requires_mcp_name(method: &str) -> bool {
matches!(
method,
"tools/call"
| "resources/read"
| "prompts/get"
| "tasks/get"
| "tasks/update"
| "tasks/cancel"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prt_03_a_positive() {
let admission = admit_final_http_request(FinalHttpRequestMetadata {
version: RequestVersionMetadata {
header_version: Some(FINAL_PROTOCOL_VERSION),
body_version: Some(FINAL_PROTOCOL_VERSION),
},
header_method: Some("tools/call"),
body_method: Some("tools/call"),
header_name: Some("weather"),
body_name: Some("weather"),
})
.expect("matching final standard headers and body values must be admitted");
assert_eq!(
admission.protocol_version().as_str(),
FINAL_PROTOCOL_VERSION
);
assert_eq!(MCP_PROTOCOL_VERSION_HEADER, "MCP-Protocol-Version");
assert_eq!(MCP_METHOD_HEADER, "Mcp-Method");
assert_eq!(MCP_NAME_HEADER, "Mcp-Name");
}
#[test]
fn prt_03_a_planted_negative() {
let body_name = Some("weather");
let error = admit_final_http_request(FinalHttpRequestMetadata {
version: RequestVersionMetadata {
header_version: Some(FINAL_PROTOCOL_VERSION),
body_version: Some(FINAL_PROTOCOL_VERSION),
},
header_method: Some("tools/call"),
body_method: Some("tools/call"),
header_name: Some("other-weather"),
body_name,
})
.expect_err("changing only the name header must reject the request");
assert_eq!(
error,
RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: HeaderMismatchReason::HeaderBodyNameMismatch,
})
);
assert_eq!(error.http_status(), 400);
assert_eq!(error.jsonrpc_error_code(), HEADER_MISMATCH_ERROR_CODE);
assert_eq!(body_name, Some("weather"));
}
#[test]
fn official_tasks_methods_require_the_same_mcp_name_mirror() {
for method in ["tasks/get", "tasks/update", "tasks/cancel"] {
let admitted = admit_final_http_request(FinalHttpRequestMetadata {
version: RequestVersionMetadata {
header_version: Some(FINAL_PROTOCOL_VERSION),
body_version: Some(FINAL_PROTOCOL_VERSION),
},
header_method: Some(method),
body_method: Some(method),
header_name: Some("task-42"),
body_name: Some("task-42"),
});
assert!(
admitted.is_ok(),
"{method} accepts an exact task identifier mirror"
);
}
let body_name = Some("task-42");
let error = admit_final_http_request(FinalHttpRequestMetadata {
version: RequestVersionMetadata {
header_version: Some(FINAL_PROTOCOL_VERSION),
body_version: Some(FINAL_PROTOCOL_VERSION),
},
header_method: Some("tasks/get"),
body_method: Some("tasks/get"),
header_name: Some("other-task"),
body_name,
})
.expect_err("changing only a Tasks name mirror rejects final admission");
assert_eq!(
error,
RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: HeaderMismatchReason::HeaderBodyNameMismatch,
})
);
assert_eq!(body_name, Some("task-42"));
}
#[test]
fn matching_unsupported_version_reports_the_requested_value() {
let error = validate_final_protocol_version(Some("2025-11-25"), Some("2025-11-25"))
.expect_err("matching unsupported versions must not be accepted");
assert_eq!(
error,
ProtocolVersionError::UnsupportedProtocolVersion {
requested: "2025-11-25".to_owned(),
}
);
assert_eq!(error.http_status(), 400);
assert_eq!(
error.jsonrpc_error_code(),
UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE
);
}
#[test]
fn missing_or_empty_version_is_a_header_mismatch() {
for (header, body) in [
(None, Some(FINAL_PROTOCOL_VERSION)),
(Some(FINAL_PROTOCOL_VERSION), None),
(Some(""), Some(FINAL_PROTOCOL_VERSION)),
(Some(FINAL_PROTOCOL_VERSION), Some("")),
] {
assert_eq!(
validate_final_protocol_version(header, body),
Err(ProtocolVersionError::HeaderMismatch)
);
}
}
#[test]
fn prt_03_b_positive() {
let admission = admit_final_request(RequestVersionMetadata {
header_version: Some(FINAL_PROTOCOL_VERSION),
body_version: Some(FINAL_PROTOCOL_VERSION),
})
.expect("matching supported header and body versions must admit the request");
assert_eq!(
admission.protocol_version().as_str(),
FINAL_PROTOCOL_VERSION
);
assert_eq!(SUPPORTED_FINAL_PROTOCOL_VERSIONS, [FINAL_PROTOCOL_VERSION]);
}
#[test]
fn prt_03_b_planted_negative() {
let body_version = Some(FINAL_PROTOCOL_VERSION);
let changed_header_version = Some("2025-11-25");
let error = admit_final_request(RequestVersionMetadata {
header_version: changed_header_version,
body_version,
})
.expect_err("changing only the header must retain header-mismatch precedence");
assert_eq!(
error,
RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
reason: HeaderMismatchReason::HeaderBodyVersionMismatch,
})
);
assert_eq!(error.jsonrpc_error_code(), HEADER_MISMATCH_ERROR_CODE);
assert_eq!(error.http_status(), 400);
assert_eq!(body_version, Some(FINAL_PROTOCOL_VERSION));
}
#[test]
fn matching_unsupported_version_is_classified_after_the_mirror_check() {
let error = admit_final_request(RequestVersionMetadata {
header_version: Some("2025-11-25"),
body_version: Some("2025-11-25"),
})
.expect_err("matching unsupported version must reject after mirror validation");
let RequestAdmissionError::UnsupportedProtocolVersion(error) = error else {
panic!("matching values must not use the header-mismatch error");
};
assert_eq!(error.requested(), "2025-11-25");
assert_eq!(error.supported_versions(), [FINAL_PROTOCOL_VERSION]);
assert_eq!(
error.jsonrpc_error_code(),
UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE
);
assert_eq!(error.http_status(), 400);
}
#[test]
fn typed_errors_preserve_only_their_final_peer_data_shapes() {
let mismatch = HeaderMismatchError {
reason: HeaderMismatchReason::MissingHeader,
};
assert_eq!(mismatch.canonical_error_data(), None);
let unsupported = UnsupportedProtocolVersionError {
requested: "2025-11-25".to_owned(),
};
assert_eq!(
unsupported.canonical_error_data(),
json!({"supported": [FINAL_PROTOCOL_VERSION], "requested": "2025-11-25"})
);
let missing = MissingRequiredClientCapabilityError::new(json!({
"roots": {"listChanged": true},
"sampling": {"context": {}}
}))
.expect("bounded capability object is valid typed peer data");
assert_eq!(missing.http_status(), 400);
assert_eq!(
missing.jsonrpc_error_code(),
MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE
);
assert_eq!(
missing.canonical_error_data(),
json!({
"requiredCapabilities": {
"roots": {"listChanged": true},
"sampling": {"context": {}}
}
})
);
let typed_missing =
MissingRequiredClientCapabilityError::from_client_capabilities(&ClientCapabilities {
roots: Some(crate::RootsCapability { list_changed: true }),
..ClientCapabilities::default()
})
.expect("typed capabilities serialize as a bounded required-capabilities object");
assert_eq!(
typed_missing.canonical_error_data(),
json!({"requiredCapabilities": {"roots": {"listChanged": true}}})
);
}
}