turul_http_mcp_server/
protocol.rs

1//! MCP Protocol Version Detection and Features
2//!
3//! This module handles MCP protocol version detection from HTTP headers
4//! and provides feature flags for different protocol versions.
5
6/// Supported MCP protocol versions and features
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum McpProtocolVersion {
9    /// Original protocol without streamable HTTP (introduced 2024-11-05)
10    V2024_11_05,
11    /// Protocol including streamable HTTP (introduced 2025-03-26)
12    V2025_03_26,
13    /// Protocol with structured _meta, cursor, progressToken, and elicitation (introduced 2025-06-18)
14    V2025_06_18,
15}
16
17impl McpProtocolVersion {
18    /// Parses a version string like "2024-11-05", "2025-03-26", or "2025-06-18".
19    pub fn parse_version(s: &str) -> Option<Self> {
20        match s {
21            "2024-11-05" => Some(McpProtocolVersion::V2024_11_05),
22            "2025-03-26" => Some(McpProtocolVersion::V2025_03_26),
23            "2025-06-18" => Some(McpProtocolVersion::V2025_06_18),
24            _ => None,
25        }
26    }
27
28    /// Converts this version to its string representation.
29    pub fn to_string(&self) -> &'static str {
30        match self {
31            McpProtocolVersion::V2024_11_05 => "2024-11-05",
32            McpProtocolVersion::V2025_03_26 => "2025-03-26",
33            McpProtocolVersion::V2025_06_18 => "2025-06-18",
34        }
35    }
36
37    /// Returns whether this version supports streamable HTTP (SSE).
38    pub fn supports_streamable_http(&self) -> bool {
39        matches!(
40            self,
41            McpProtocolVersion::V2025_03_26 | McpProtocolVersion::V2025_06_18
42        )
43    }
44
45    /// Returns whether this version supports `_meta` fields in requests, responses, and notifications.
46    pub fn supports_meta_fields(&self) -> bool {
47        matches!(self, McpProtocolVersion::V2025_06_18)
48    }
49
50    /// Returns whether this version supports the use of `progressToken` and `cursor` in `_meta`.
51    pub fn supports_progress_and_cursor(&self) -> bool {
52        matches!(self, McpProtocolVersion::V2025_06_18)
53    }
54
55    /// Returns whether this version supports structured user elicitation via JSON Schema.
56    pub fn supports_elicitation(&self) -> bool {
57        matches!(self, McpProtocolVersion::V2025_06_18)
58    }
59
60    /// Get a list of supported features for this protocol version
61    pub fn supported_features(&self) -> Vec<&'static str> {
62        let mut features = vec![];
63        if self.supports_streamable_http() {
64            features.push("streamable-http");
65        }
66        if self.supports_meta_fields() {
67            features.push("_meta-fields");
68        }
69        if self.supports_progress_and_cursor() {
70            features.push("progress-token");
71            features.push("cursor");
72        }
73        if self.supports_elicitation() {
74            features.push("elicitation");
75        }
76        features
77    }
78
79    /// The latest protocol version this server implements.
80    pub const LATEST: McpProtocolVersion = McpProtocolVersion::V2025_06_18;
81}
82
83impl Default for McpProtocolVersion {
84    fn default() -> Self {
85        Self::LATEST
86    }
87}
88
89impl std::fmt::Display for McpProtocolVersion {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        write!(f, "{}", self.to_string())
92    }
93}
94
95/// Extract MCP protocol version from HTTP request headers
96pub fn extract_protocol_version(headers: &hyper::HeaderMap) -> McpProtocolVersion {
97    headers
98        .get("MCP-Protocol-Version")
99        .and_then(|h| h.to_str().ok())
100        .and_then(McpProtocolVersion::parse_version)
101        .unwrap_or(McpProtocolVersion::LATEST)
102}
103
104/// Extract MCP session ID from HTTP request headers
105pub fn extract_session_id(headers: &hyper::HeaderMap) -> Option<String> {
106    headers
107        .get("Mcp-Session-Id")
108        .and_then(|h| h.to_str().ok())
109        .map(|s| s.to_string())
110}
111
112/// Extract Last-Event-ID from HTTP request headers for SSE resumability
113pub fn extract_last_event_id(headers: &hyper::HeaderMap) -> Option<u64> {
114    headers
115        .get("Last-Event-ID")
116        .and_then(|h| h.to_str().ok())
117        .and_then(|s| s.parse::<u64>().ok())
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use hyper::HeaderMap;
124
125    #[test]
126    fn test_version_parsing() {
127        assert_eq!(
128            McpProtocolVersion::parse_version("2024-11-05"),
129            Some(McpProtocolVersion::V2024_11_05)
130        );
131        assert_eq!(
132            McpProtocolVersion::parse_version("2025-03-26"),
133            Some(McpProtocolVersion::V2025_03_26)
134        );
135        assert_eq!(
136            McpProtocolVersion::parse_version("2025-06-18"),
137            Some(McpProtocolVersion::V2025_06_18)
138        );
139        assert_eq!(McpProtocolVersion::parse_version("invalid"), None);
140    }
141
142    #[test]
143    fn test_version_features() {
144        let v2024 = McpProtocolVersion::V2024_11_05;
145        assert!(!v2024.supports_streamable_http());
146        assert!(!v2024.supports_meta_fields());
147        assert!(!v2024.supports_elicitation());
148
149        let v2025_03 = McpProtocolVersion::V2025_03_26;
150        assert!(v2025_03.supports_streamable_http());
151        assert!(!v2025_03.supports_meta_fields());
152
153        let v2025_06 = McpProtocolVersion::V2025_06_18;
154        assert!(v2025_06.supports_streamable_http());
155        assert!(v2025_06.supports_meta_fields());
156        assert!(v2025_06.supports_elicitation());
157    }
158
159    #[test]
160    fn test_header_extraction() {
161        let mut headers = HeaderMap::new();
162        headers.insert("MCP-Protocol-Version", "2025-06-18".parse().unwrap());
163        headers.insert("Mcp-Session-Id", "test-session-123".parse().unwrap());
164
165        let version = extract_protocol_version(&headers);
166        assert_eq!(version, McpProtocolVersion::V2025_06_18);
167
168        let session_id = extract_session_id(&headers);
169        assert_eq!(session_id, Some("test-session-123".to_string()));
170    }
171}