Skip to main content

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    /// Protocol with tasks, icons, URL elicitation, and sampling tools (introduced 2025-11-25)
16    V2025_11_25,
17}
18
19impl McpProtocolVersion {
20    /// Parses a version string like "2024-11-05", "2025-03-26", "2025-06-18", or "2025-11-25".
21    pub fn parse_version(s: &str) -> Option<Self> {
22        match s {
23            "2024-11-05" => Some(McpProtocolVersion::V2024_11_05),
24            "2025-03-26" => Some(McpProtocolVersion::V2025_03_26),
25            "2025-06-18" => Some(McpProtocolVersion::V2025_06_18),
26            "2025-11-25" => Some(McpProtocolVersion::V2025_11_25),
27            _ => None,
28        }
29    }
30
31    /// Converts this version to its string representation.
32    pub fn to_string(&self) -> &'static str {
33        match self {
34            McpProtocolVersion::V2024_11_05 => "2024-11-05",
35            McpProtocolVersion::V2025_03_26 => "2025-03-26",
36            McpProtocolVersion::V2025_06_18 => "2025-06-18",
37            McpProtocolVersion::V2025_11_25 => "2025-11-25",
38        }
39    }
40
41    /// Returns whether this version supports streamable HTTP (SSE).
42    pub fn supports_streamable_http(&self) -> bool {
43        matches!(
44            self,
45            McpProtocolVersion::V2025_03_26
46                | McpProtocolVersion::V2025_06_18
47                | McpProtocolVersion::V2025_11_25
48        )
49    }
50
51    /// Returns whether this version supports `_meta` fields in requests, responses, and notifications.
52    pub fn supports_meta_fields(&self) -> bool {
53        matches!(
54            self,
55            McpProtocolVersion::V2025_06_18 | McpProtocolVersion::V2025_11_25
56        )
57    }
58
59    /// Returns whether this version supports the use of `progressToken` and `cursor` in `_meta`.
60    pub fn supports_progress_and_cursor(&self) -> bool {
61        matches!(
62            self,
63            McpProtocolVersion::V2025_06_18 | McpProtocolVersion::V2025_11_25
64        )
65    }
66
67    /// Returns whether this version supports structured user elicitation via JSON Schema.
68    pub fn supports_elicitation(&self) -> bool {
69        matches!(
70            self,
71            McpProtocolVersion::V2025_06_18 | McpProtocolVersion::V2025_11_25
72        )
73    }
74
75    /// Returns whether this version supports the task system (experimental).
76    pub fn supports_tasks(&self) -> bool {
77        matches!(self, McpProtocolVersion::V2025_11_25)
78    }
79
80    /// Returns whether this version supports icons.
81    pub fn supports_icons(&self) -> bool {
82        matches!(self, McpProtocolVersion::V2025_11_25)
83    }
84
85    /// Get a list of supported features for this protocol version
86    pub fn supported_features(&self) -> Vec<&'static str> {
87        let mut features = vec![];
88        if self.supports_streamable_http() {
89            features.push("streamable-http");
90        }
91        if self.supports_meta_fields() {
92            features.push("_meta-fields");
93        }
94        if self.supports_progress_and_cursor() {
95            features.push("progress-token");
96            features.push("cursor");
97        }
98        if self.supports_elicitation() {
99            features.push("elicitation");
100        }
101        if self.supports_tasks() {
102            features.push("tasks");
103        }
104        if self.supports_icons() {
105            features.push("icons");
106        }
107        features
108    }
109
110    /// The latest protocol version this server implements.
111    pub const LATEST: McpProtocolVersion = McpProtocolVersion::V2025_11_25;
112}
113
114impl Default for McpProtocolVersion {
115    fn default() -> Self {
116        Self::LATEST
117    }
118}
119
120impl std::fmt::Display for McpProtocolVersion {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "{}", self.to_string())
123    }
124}
125
126/// Extract MCP protocol version from HTTP request headers
127pub fn extract_protocol_version(headers: &hyper::HeaderMap) -> McpProtocolVersion {
128    headers
129        .get("MCP-Protocol-Version")
130        .and_then(|h| h.to_str().ok())
131        .and_then(McpProtocolVersion::parse_version)
132        .unwrap_or(McpProtocolVersion::LATEST)
133}
134
135/// Extract MCP session ID from HTTP request headers
136pub fn extract_session_id(headers: &hyper::HeaderMap) -> Option<String> {
137    headers
138        .get("Mcp-Session-Id")
139        .and_then(|h| h.to_str().ok())
140        .map(|s| s.to_string())
141}
142
143/// Extract Last-Event-ID from HTTP request headers for SSE resumability
144pub fn extract_last_event_id(headers: &hyper::HeaderMap) -> Option<u64> {
145    headers
146        .get("Last-Event-ID")
147        .and_then(|h| h.to_str().ok())
148        .and_then(|s| s.parse::<u64>().ok())
149}
150
151/// Normalize an HTTP header value by trimming whitespace and lowercasing.
152///
153/// HTTP media types are case-insensitive (RFC 7231 ยง3.1.1.1).
154/// This function ensures consistent comparison regardless of client formatting.
155pub fn normalize_header_value(value: &str) -> String {
156    value.trim().to_ascii_lowercase()
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use hyper::HeaderMap;
163
164    #[test]
165    fn test_version_parsing() {
166        assert_eq!(
167            McpProtocolVersion::parse_version("2024-11-05"),
168            Some(McpProtocolVersion::V2024_11_05)
169        );
170        assert_eq!(
171            McpProtocolVersion::parse_version("2025-03-26"),
172            Some(McpProtocolVersion::V2025_03_26)
173        );
174        assert_eq!(
175            McpProtocolVersion::parse_version("2025-06-18"),
176            Some(McpProtocolVersion::V2025_06_18)
177        );
178        assert_eq!(
179            McpProtocolVersion::parse_version("2025-11-25"),
180            Some(McpProtocolVersion::V2025_11_25)
181        );
182        assert_eq!(McpProtocolVersion::parse_version("invalid"), None);
183    }
184
185    #[test]
186    fn test_version_features() {
187        let v2024 = McpProtocolVersion::V2024_11_05;
188        assert!(!v2024.supports_streamable_http());
189        assert!(!v2024.supports_meta_fields());
190        assert!(!v2024.supports_elicitation());
191        assert!(!v2024.supports_tasks());
192
193        let v2025_03 = McpProtocolVersion::V2025_03_26;
194        assert!(v2025_03.supports_streamable_http());
195        assert!(!v2025_03.supports_meta_fields());
196        assert!(!v2025_03.supports_tasks());
197
198        let v2025_06 = McpProtocolVersion::V2025_06_18;
199        assert!(v2025_06.supports_streamable_http());
200        assert!(v2025_06.supports_meta_fields());
201        assert!(v2025_06.supports_elicitation());
202        assert!(!v2025_06.supports_tasks());
203
204        let v2025_11 = McpProtocolVersion::V2025_11_25;
205        assert!(v2025_11.supports_streamable_http());
206        assert!(v2025_11.supports_meta_fields());
207        assert!(v2025_11.supports_elicitation());
208        assert!(v2025_11.supports_tasks());
209        assert!(v2025_11.supports_icons());
210    }
211
212    #[test]
213    fn test_normalize_header_value() {
214        assert_eq!(
215            normalize_header_value("application/json"),
216            "application/json"
217        );
218        assert_eq!(
219            normalize_header_value("  application/json  "),
220            "application/json"
221        );
222        assert_eq!(
223            normalize_header_value("Application/JSON"),
224            "application/json"
225        );
226        assert_eq!(
227            normalize_header_value("Application/Json; Charset=UTF-8"),
228            "application/json; charset=utf-8"
229        );
230        assert_eq!(
231            normalize_header_value("  TEXT/EVENT-STREAM "),
232            "text/event-stream"
233        );
234        assert_eq!(normalize_header_value(""), "");
235    }
236
237    #[test]
238    fn test_header_extraction() {
239        let mut headers = HeaderMap::new();
240        headers.insert("MCP-Protocol-Version", "2025-11-25".parse().unwrap());
241        headers.insert("Mcp-Session-Id", "test-session-123".parse().unwrap());
242
243        let version = extract_protocol_version(&headers);
244        assert_eq!(version, McpProtocolVersion::V2025_11_25);
245
246        let session_id = extract_session_id(&headers);
247        assert_eq!(session_id, Some("test-session-123".to_string()));
248    }
249}