#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum McpProtocolVersion {
V2024_11_05,
V2025_03_26,
V2025_06_18,
V2025_11_25,
}
impl McpProtocolVersion {
pub fn parse_version(s: &str) -> Option<Self> {
match s {
"2024-11-05" => Some(McpProtocolVersion::V2024_11_05),
"2025-03-26" => Some(McpProtocolVersion::V2025_03_26),
"2025-06-18" => Some(McpProtocolVersion::V2025_06_18),
"2025-11-25" => Some(McpProtocolVersion::V2025_11_25),
_ => None,
}
}
pub fn to_string(&self) -> &'static str {
match self {
McpProtocolVersion::V2024_11_05 => "2024-11-05",
McpProtocolVersion::V2025_03_26 => "2025-03-26",
McpProtocolVersion::V2025_06_18 => "2025-06-18",
McpProtocolVersion::V2025_11_25 => "2025-11-25",
}
}
pub fn supports_streamable_http(&self) -> bool {
matches!(
self,
McpProtocolVersion::V2025_03_26
| McpProtocolVersion::V2025_06_18
| McpProtocolVersion::V2025_11_25
)
}
pub fn supports_meta_fields(&self) -> bool {
matches!(
self,
McpProtocolVersion::V2025_06_18 | McpProtocolVersion::V2025_11_25
)
}
pub fn supports_progress_and_cursor(&self) -> bool {
matches!(
self,
McpProtocolVersion::V2025_06_18 | McpProtocolVersion::V2025_11_25
)
}
pub fn supports_elicitation(&self) -> bool {
matches!(
self,
McpProtocolVersion::V2025_06_18 | McpProtocolVersion::V2025_11_25
)
}
pub fn supports_tasks(&self) -> bool {
matches!(self, McpProtocolVersion::V2025_11_25)
}
pub fn supports_icons(&self) -> bool {
matches!(self, McpProtocolVersion::V2025_11_25)
}
pub fn supported_features(&self) -> Vec<&'static str> {
let mut features = vec![];
if self.supports_streamable_http() {
features.push("streamable-http");
}
if self.supports_meta_fields() {
features.push("_meta-fields");
}
if self.supports_progress_and_cursor() {
features.push("progress-token");
features.push("cursor");
}
if self.supports_elicitation() {
features.push("elicitation");
}
if self.supports_tasks() {
features.push("tasks");
}
if self.supports_icons() {
features.push("icons");
}
features
}
pub const LATEST: McpProtocolVersion = McpProtocolVersion::V2025_11_25;
}
impl Default for McpProtocolVersion {
fn default() -> Self {
Self::LATEST
}
}
impl std::fmt::Display for McpProtocolVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_string())
}
}
pub fn extract_protocol_version(headers: &hyper::HeaderMap) -> McpProtocolVersion {
headers
.get("MCP-Protocol-Version")
.and_then(|h| h.to_str().ok())
.and_then(McpProtocolVersion::parse_version)
.unwrap_or(McpProtocolVersion::LATEST)
}
pub fn extract_session_id(headers: &hyper::HeaderMap) -> Option<String> {
headers
.get("Mcp-Session-Id")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string())
}
pub fn extract_last_event_id(headers: &hyper::HeaderMap) -> Option<u64> {
headers
.get("Last-Event-ID")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
}
pub fn normalize_header_value(value: &str) -> String {
value.trim().to_ascii_lowercase()
}
#[cfg(test)]
mod tests {
use super::*;
use hyper::HeaderMap;
#[test]
fn test_version_parsing() {
assert_eq!(
McpProtocolVersion::parse_version("2024-11-05"),
Some(McpProtocolVersion::V2024_11_05)
);
assert_eq!(
McpProtocolVersion::parse_version("2025-03-26"),
Some(McpProtocolVersion::V2025_03_26)
);
assert_eq!(
McpProtocolVersion::parse_version("2025-06-18"),
Some(McpProtocolVersion::V2025_06_18)
);
assert_eq!(
McpProtocolVersion::parse_version("2025-11-25"),
Some(McpProtocolVersion::V2025_11_25)
);
assert_eq!(McpProtocolVersion::parse_version("invalid"), None);
}
#[test]
fn test_version_features() {
let v2024 = McpProtocolVersion::V2024_11_05;
assert!(!v2024.supports_streamable_http());
assert!(!v2024.supports_meta_fields());
assert!(!v2024.supports_elicitation());
assert!(!v2024.supports_tasks());
let v2025_03 = McpProtocolVersion::V2025_03_26;
assert!(v2025_03.supports_streamable_http());
assert!(!v2025_03.supports_meta_fields());
assert!(!v2025_03.supports_tasks());
let v2025_06 = McpProtocolVersion::V2025_06_18;
assert!(v2025_06.supports_streamable_http());
assert!(v2025_06.supports_meta_fields());
assert!(v2025_06.supports_elicitation());
assert!(!v2025_06.supports_tasks());
let v2025_11 = McpProtocolVersion::V2025_11_25;
assert!(v2025_11.supports_streamable_http());
assert!(v2025_11.supports_meta_fields());
assert!(v2025_11.supports_elicitation());
assert!(v2025_11.supports_tasks());
assert!(v2025_11.supports_icons());
}
#[test]
fn test_normalize_header_value() {
assert_eq!(
normalize_header_value("application/json"),
"application/json"
);
assert_eq!(
normalize_header_value(" application/json "),
"application/json"
);
assert_eq!(
normalize_header_value("Application/JSON"),
"application/json"
);
assert_eq!(
normalize_header_value("Application/Json; Charset=UTF-8"),
"application/json; charset=utf-8"
);
assert_eq!(
normalize_header_value(" TEXT/EVENT-STREAM "),
"text/event-stream"
);
assert_eq!(normalize_header_value(""), "");
}
#[test]
fn test_header_extraction() {
let mut headers = HeaderMap::new();
headers.insert("MCP-Protocol-Version", "2025-11-25".parse().unwrap());
headers.insert("Mcp-Session-Id", "test-session-123".parse().unwrap());
let version = extract_protocol_version(&headers);
assert_eq!(version, McpProtocolVersion::V2025_11_25);
let session_id = extract_session_id(&headers);
assert_eq!(session_id, Some("test-session-123".to_string()));
}
}