mcpkit_core/protocol_version.rs
1//! Protocol version types and negotiation.
2//!
3//! This module provides a type-safe representation of MCP protocol versions
4//! with capability detection methods for version-specific features.
5//!
6//! # Protocol Version History
7//!
8//! | Version | Date | Key Changes |
9//! |---------|------|-------------|
10//! | 2024-11-05 | Nov 2024 | Initial MCP specification, HTTP+SSE transport |
11//! | 2025-03-26 | Mar 2025 | OAuth 2.1, Streamable HTTP, batching, tool annotations, audio |
12//! | 2025-06-18 | Jun 2025 | Elicitation, structured output, resource links, removed batching |
13//! | 2025-11-25 | Nov 2025 | Tasks, parallel tools, server-side agent loops |
14//!
15//! # Example
16//!
17//! ```rust
18//! use mcpkit_core::protocol_version::ProtocolVersion;
19//!
20//! // Parse version from string
21//! let version: ProtocolVersion = "2025-03-26".parse().unwrap();
22//!
23//! // Check feature support
24//! assert!(version.supports_oauth());
25//! assert!(version.supports_tool_annotations());
26//! assert!(!version.supports_elicitation()); // Added in 2025-06-18
27//!
28//! // Version comparison
29//! assert!(ProtocolVersion::V2025_11_25 > ProtocolVersion::V2024_11_05);
30//! ```
31
32use std::fmt;
33use std::str::FromStr;
34
35use serde::{Deserialize, Serialize};
36
37/// MCP protocol versions in chronological order.
38///
39/// The ordering is: `V2024_11_05 < V2025_03_26 < V2025_06_18 < V2025_11_25`
40///
41/// This enum implements `Ord`, so you can compare versions:
42///
43/// ```rust
44/// use mcpkit_core::protocol_version::ProtocolVersion;
45///
46/// assert!(ProtocolVersion::V2025_11_25 > ProtocolVersion::V2024_11_05);
47/// assert!(ProtocolVersion::V2025_03_26 >= ProtocolVersion::V2024_11_05);
48/// ```
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
50#[serde(into = "String", try_from = "String")]
51pub enum ProtocolVersion {
52 /// Original MCP specification (November 2024).
53 ///
54 /// Features: HTTP+SSE transport, tools, resources, prompts.
55 V2024_11_05,
56
57 /// OAuth 2.1 and Streamable HTTP update (March 2025).
58 ///
59 /// New features:
60 /// - OAuth 2.1 authorization framework
61 /// - Streamable HTTP transport (replaces HTTP+SSE)
62 /// - JSON-RPC batching (removed in 2025-06-18)
63 /// - Tool annotations (readOnly, destructive)
64 /// - Audio content type
65 /// - Completions capability
66 V2025_03_26,
67
68 /// Security and elicitation update (June 2025).
69 ///
70 /// New features:
71 /// - Elicitation (server requesting user input)
72 /// - Structured tool output
73 /// - Resource links in tool results
74 /// - Protected resource metadata
75 /// - `_meta` field in messages
76 /// - `title` field for display names
77 ///
78 /// Breaking changes:
79 /// - Removed JSON-RPC batching
80 /// - MCP-Protocol-Version header required
81 V2025_06_18,
82
83 /// Tasks and parallel tools update (November 2025).
84 ///
85 /// New features:
86 /// - Tasks with async status tracking
87 /// - Parallel tool calls
88 /// - Server-side agent loops
89 /// - Tool calling in sampling requests
90 V2025_11_25,
91}
92
93impl ProtocolVersion {
94 /// The latest supported protocol version.
95 pub const LATEST: Self = Self::V2025_11_25;
96
97 /// The default version used when not specified.
98 pub const DEFAULT: Self = Self::LATEST;
99
100 /// All supported versions in chronological order.
101 pub const ALL: &'static [Self] = &[
102 Self::V2024_11_05,
103 Self::V2025_03_26,
104 Self::V2025_06_18,
105 Self::V2025_11_25,
106 ];
107
108 /// Returns the string representation used in the protocol.
109 ///
110 /// This matches the format expected in MCP messages (e.g., `"2025-11-25"`).
111 #[must_use]
112 pub const fn as_str(&self) -> &'static str {
113 match self {
114 Self::V2024_11_05 => "2024-11-05",
115 Self::V2025_03_26 => "2025-03-26",
116 Self::V2025_06_18 => "2025-06-18",
117 Self::V2025_11_25 => "2025-11-25",
118 }
119 }
120
121 // =========================================================================
122 // Transport Features
123 // =========================================================================
124
125 /// Whether this version uses HTTP+SSE transport (original spec).
126 ///
127 /// Only `V2024_11_05` uses the original HTTP+SSE transport.
128 /// Later versions use Streamable HTTP.
129 #[must_use]
130 pub const fn supports_sse_transport(&self) -> bool {
131 matches!(self, Self::V2024_11_05)
132 }
133
134 /// Whether this version supports Streamable HTTP transport.
135 ///
136 /// Added in 2025-03-26, replacing HTTP+SSE.
137 #[must_use]
138 pub const fn supports_streamable_http(&self) -> bool {
139 matches!(
140 self,
141 Self::V2025_03_26 | Self::V2025_06_18 | Self::V2025_11_25
142 )
143 }
144
145 /// Whether this version supports JSON-RPC batching.
146 ///
147 /// Only available in 2025-03-26. Removed in 2025-06-18.
148 #[must_use]
149 pub const fn supports_batching(&self) -> bool {
150 matches!(self, Self::V2025_03_26)
151 }
152
153 /// Whether the MCP-Protocol-Version header is required in HTTP requests.
154 ///
155 /// Required from 2025-06-18 onwards.
156 #[must_use]
157 pub const fn requires_version_header(&self) -> bool {
158 matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
159 }
160
161 // =========================================================================
162 // Content Types
163 // =========================================================================
164
165 /// Whether this version supports audio content type.
166 ///
167 /// Added in 2025-03-26.
168 #[must_use]
169 pub const fn supports_audio_content(&self) -> bool {
170 matches!(
171 self,
172 Self::V2025_03_26 | Self::V2025_06_18 | Self::V2025_11_25
173 )
174 }
175
176 // =========================================================================
177 // Tool Features
178 // =========================================================================
179
180 /// Whether this version supports tool annotations.
181 ///
182 /// Tool annotations describe behavior like `readOnly`, `destructive`, `idempotent`.
183 /// Added in 2025-03-26.
184 #[must_use]
185 pub const fn supports_tool_annotations(&self) -> bool {
186 matches!(
187 self,
188 Self::V2025_03_26 | Self::V2025_06_18 | Self::V2025_11_25
189 )
190 }
191
192 /// Whether this version supports structured tool output.
193 ///
194 /// Allows tools to return structured data alongside text.
195 /// Added in 2025-06-18.
196 #[must_use]
197 pub const fn supports_structured_tool_output(&self) -> bool {
198 matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
199 }
200
201 /// Whether this version supports resource links in tool results.
202 ///
203 /// Allows tool results to reference resources.
204 /// Added in 2025-06-18.
205 #[must_use]
206 pub const fn supports_resource_links(&self) -> bool {
207 matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
208 }
209
210 /// Whether this version supports parallel tool calls.
211 ///
212 /// Allows multiple tools to be called concurrently.
213 /// Added in 2025-11-25.
214 #[must_use]
215 pub const fn supports_parallel_tools(&self) -> bool {
216 matches!(self, Self::V2025_11_25)
217 }
218
219 // =========================================================================
220 // Authorization Features
221 // =========================================================================
222
223 /// Whether this version supports OAuth 2.1 authorization.
224 ///
225 /// Added in 2025-03-26.
226 #[must_use]
227 pub const fn supports_oauth(&self) -> bool {
228 matches!(
229 self,
230 Self::V2025_03_26 | Self::V2025_06_18 | Self::V2025_11_25
231 )
232 }
233
234 /// Whether this version supports protected resource metadata.
235 ///
236 /// Enables discovery of OAuth authorization servers.
237 /// Added in 2025-06-18.
238 #[must_use]
239 pub const fn supports_protected_resources(&self) -> bool {
240 matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
241 }
242
243 // =========================================================================
244 // Server Features
245 // =========================================================================
246
247 /// Whether this version supports elicitation.
248 ///
249 /// Elicitation allows servers to request additional information from users.
250 /// Added in 2025-06-18.
251 #[must_use]
252 pub const fn supports_elicitation(&self) -> bool {
253 matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
254 }
255
256 /// Whether this version supports tasks with async status tracking.
257 ///
258 /// Tasks allow tracking long-running operations.
259 /// Added in 2025-11-25.
260 #[must_use]
261 pub const fn supports_tasks(&self) -> bool {
262 matches!(self, Self::V2025_11_25)
263 }
264
265 /// Whether this version supports server-side agent loops.
266 ///
267 /// Enables sophisticated multi-step reasoning on the server.
268 /// Added in 2025-11-25.
269 #[must_use]
270 pub const fn supports_agent_loops(&self) -> bool {
271 matches!(self, Self::V2025_11_25)
272 }
273
274 /// Whether this version supports tool calling in sampling requests.
275 ///
276 /// Allows servers to include tool definitions in sampling.
277 /// Added in 2025-11-25.
278 #[must_use]
279 pub const fn supports_sampling_tools(&self) -> bool {
280 matches!(self, Self::V2025_11_25)
281 }
282
283 // =========================================================================
284 // Schema Features
285 // =========================================================================
286
287 /// Whether this version supports the `_meta` field in messages.
288 ///
289 /// Provides metadata for messages.
290 /// Added in 2025-06-18.
291 #[must_use]
292 pub const fn supports_meta_field(&self) -> bool {
293 matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
294 }
295
296 /// Whether this version supports the `title` field for display names.
297 ///
298 /// Separate from `name` which is the programmatic identifier.
299 /// Added in 2025-06-18.
300 #[must_use]
301 pub const fn supports_title_field(&self) -> bool {
302 matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
303 }
304
305 /// Whether this version supports the `context` field in completion requests.
306 ///
307 /// Provides previously-resolved variable values.
308 /// Added in 2025-06-18.
309 #[must_use]
310 pub const fn supports_completion_context(&self) -> bool {
311 matches!(self, Self::V2025_06_18 | Self::V2025_11_25)
312 }
313
314 /// Whether this version supports the `completions` capability.
315 ///
316 /// Explicitly indicates support for argument autocompletion.
317 /// Added in 2025-03-26.
318 #[must_use]
319 pub const fn supports_completions_capability(&self) -> bool {
320 matches!(
321 self,
322 Self::V2025_03_26 | Self::V2025_06_18 | Self::V2025_11_25
323 )
324 }
325
326 // =========================================================================
327 // Version Negotiation
328 // =========================================================================
329
330 /// Negotiate the best protocol version between requested and supported versions.
331 ///
332 /// Returns the highest version that is:
333 /// 1. Less than or equal to the requested version
334 /// 2. In the supported versions list
335 ///
336 /// Returns `None` if no compatible version exists.
337 ///
338 /// # Arguments
339 ///
340 /// * `requested` - The version string requested by the client
341 /// * `supported` - List of versions supported by the server
342 ///
343 /// # Example
344 ///
345 /// ```rust
346 /// use mcpkit_core::protocol_version::ProtocolVersion;
347 ///
348 /// // Server supports all versions, client requests latest
349 /// let negotiated = ProtocolVersion::negotiate(
350 /// "2025-11-25",
351 /// ProtocolVersion::ALL
352 /// );
353 /// assert_eq!(negotiated, Some(ProtocolVersion::V2025_11_25));
354 ///
355 /// // Client requests older version
356 /// let negotiated = ProtocolVersion::negotiate(
357 /// "2024-11-05",
358 /// ProtocolVersion::ALL
359 /// );
360 /// assert_eq!(negotiated, Some(ProtocolVersion::V2024_11_05));
361 ///
362 /// // Client requests unknown future version
363 /// let negotiated = ProtocolVersion::negotiate(
364 /// "2026-01-01",
365 /// ProtocolVersion::ALL
366 /// );
367 /// // Returns latest supported version
368 /// assert_eq!(negotiated, Some(ProtocolVersion::V2025_11_25));
369 /// ```
370 #[must_use]
371 pub fn negotiate(requested: &str, supported: &[Self]) -> Option<Self> {
372 // Try to parse the requested version
373 if let Ok(requested_version) = Self::from_str(requested) {
374 // Find the highest supported version <= requested
375 supported
376 .iter()
377 .filter(|v| **v <= requested_version)
378 .max()
379 .copied()
380 } else {
381 // Unknown version string - return latest supported
382 // This handles future versions gracefully
383 supported.iter().max().copied()
384 }
385 }
386
387 /// Check if this version can communicate with another version.
388 ///
389 /// Newer servers can communicate with older clients using backward compatibility.
390 ///
391 /// # Arguments
392 ///
393 /// * `client_version` - The version the client supports
394 ///
395 /// # Example
396 ///
397 /// ```rust
398 /// use mcpkit_core::protocol_version::ProtocolVersion;
399 ///
400 /// let server = ProtocolVersion::V2025_11_25;
401 ///
402 /// // Server can talk to older clients
403 /// assert!(server.is_compatible_with(ProtocolVersion::V2024_11_05));
404 ///
405 /// // Server can talk to same version
406 /// assert!(server.is_compatible_with(ProtocolVersion::V2025_11_25));
407 /// ```
408 #[must_use]
409 pub const fn is_compatible_with(&self, client_version: Self) -> bool {
410 // Server can communicate with clients at same or older version
411 // Client version must be <= server version
412 (client_version as u8) <= (*self as u8)
413 }
414}
415
416impl Default for ProtocolVersion {
417 fn default() -> Self {
418 Self::DEFAULT
419 }
420}
421
422impl fmt::Display for ProtocolVersion {
423 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424 f.write_str(self.as_str())
425 }
426}
427
428/// Error returned when parsing an unknown protocol version string.
429#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct VersionParseError {
431 /// The unknown version string.
432 pub version: String,
433}
434
435impl fmt::Display for VersionParseError {
436 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437 write!(
438 f,
439 "unknown protocol version '{}', supported versions: {:?}",
440 self.version,
441 ProtocolVersion::ALL
442 .iter()
443 .map(ProtocolVersion::as_str)
444 .collect::<Vec<_>>()
445 )
446 }
447}
448
449impl std::error::Error for VersionParseError {}
450
451impl FromStr for ProtocolVersion {
452 type Err = VersionParseError;
453
454 fn from_str(s: &str) -> Result<Self, Self::Err> {
455 match s {
456 "2024-11-05" => Ok(Self::V2024_11_05),
457 "2025-03-26" => Ok(Self::V2025_03_26),
458 "2025-06-18" => Ok(Self::V2025_06_18),
459 "2025-11-25" => Ok(Self::V2025_11_25),
460 _ => Err(VersionParseError {
461 version: s.to_string(),
462 }),
463 }
464 }
465}
466
467impl From<ProtocolVersion> for String {
468 fn from(version: ProtocolVersion) -> Self {
469 version.as_str().to_string()
470 }
471}
472
473impl TryFrom<String> for ProtocolVersion {
474 type Error = VersionParseError;
475
476 fn try_from(s: String) -> Result<Self, Self::Error> {
477 s.parse()
478 }
479}
480
481impl TryFrom<&str> for ProtocolVersion {
482 type Error = VersionParseError;
483
484 fn try_from(s: &str) -> Result<Self, Self::Error> {
485 s.parse()
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn test_version_ordering() {
495 assert!(ProtocolVersion::V2024_11_05 < ProtocolVersion::V2025_03_26);
496 assert!(ProtocolVersion::V2025_03_26 < ProtocolVersion::V2025_06_18);
497 assert!(ProtocolVersion::V2025_06_18 < ProtocolVersion::V2025_11_25);
498 }
499
500 #[test]
501 fn test_version_parse() -> Result<(), Box<dyn std::error::Error>> {
502 assert_eq!(
503 "2024-11-05".parse::<ProtocolVersion>()?,
504 ProtocolVersion::V2024_11_05
505 );
506 assert_eq!(
507 "2025-03-26".parse::<ProtocolVersion>()?,
508 ProtocolVersion::V2025_03_26
509 );
510 assert_eq!(
511 "2025-06-18".parse::<ProtocolVersion>()?,
512 ProtocolVersion::V2025_06_18
513 );
514 assert_eq!(
515 "2025-11-25".parse::<ProtocolVersion>()?,
516 ProtocolVersion::V2025_11_25
517 );
518 assert!("unknown".parse::<ProtocolVersion>().is_err());
519 Ok(())
520 }
521
522 #[test]
523 fn test_version_display() {
524 assert_eq!(ProtocolVersion::V2024_11_05.to_string(), "2024-11-05");
525 assert_eq!(ProtocolVersion::V2025_11_25.to_string(), "2025-11-25");
526 }
527
528 #[test]
529 fn test_feature_support() {
530 // V2024_11_05 - Original features only
531 let v1 = ProtocolVersion::V2024_11_05;
532 assert!(v1.supports_sse_transport());
533 assert!(!v1.supports_streamable_http());
534 assert!(!v1.supports_oauth());
535 assert!(!v1.supports_elicitation());
536 assert!(!v1.supports_tasks());
537
538 // V2025_03_26 - OAuth, Streamable HTTP, etc.
539 let v2 = ProtocolVersion::V2025_03_26;
540 assert!(!v2.supports_sse_transport());
541 assert!(v2.supports_streamable_http());
542 assert!(v2.supports_oauth());
543 assert!(v2.supports_tool_annotations());
544 assert!(v2.supports_batching());
545 assert!(!v2.supports_elicitation());
546
547 // V2025_06_18 - Elicitation, no batching
548 let v3 = ProtocolVersion::V2025_06_18;
549 assert!(v3.supports_oauth());
550 assert!(!v3.supports_batching());
551 assert!(v3.supports_elicitation());
552 assert!(v3.supports_meta_field());
553 assert!(!v3.supports_tasks());
554
555 // V2025_11_25 - Tasks, parallel tools
556 let v4 = ProtocolVersion::V2025_11_25;
557 assert!(v4.supports_elicitation());
558 assert!(v4.supports_tasks());
559 assert!(v4.supports_parallel_tools());
560 assert!(v4.supports_agent_loops());
561 }
562
563 #[test]
564 fn test_negotiate() {
565 let all = ProtocolVersion::ALL;
566
567 // Exact match
568 assert_eq!(
569 ProtocolVersion::negotiate("2024-11-05", all),
570 Some(ProtocolVersion::V2024_11_05)
571 );
572 assert_eq!(
573 ProtocolVersion::negotiate("2025-11-25", all),
574 Some(ProtocolVersion::V2025_11_25)
575 );
576
577 // Unknown future version - returns latest
578 assert_eq!(
579 ProtocolVersion::negotiate("2026-01-01", all),
580 Some(ProtocolVersion::V2025_11_25)
581 );
582
583 // Server only supports old version
584 let old_only = &[ProtocolVersion::V2024_11_05];
585 assert_eq!(
586 ProtocolVersion::negotiate("2025-11-25", old_only),
587 Some(ProtocolVersion::V2024_11_05)
588 );
589
590 // Empty supported list
591 assert_eq!(ProtocolVersion::negotiate("2025-11-25", &[]), None);
592 }
593
594 #[test]
595 fn test_is_compatible_with() {
596 let latest = ProtocolVersion::V2025_11_25;
597 let oldest = ProtocolVersion::V2024_11_05;
598
599 // Latest server is compatible with all older versions
600 assert!(latest.is_compatible_with(oldest));
601 assert!(latest.is_compatible_with(ProtocolVersion::V2025_03_26));
602 assert!(latest.is_compatible_with(ProtocolVersion::V2025_06_18));
603 assert!(latest.is_compatible_with(latest));
604
605 // Old server can only handle same or older
606 assert!(oldest.is_compatible_with(oldest));
607 assert!(!oldest.is_compatible_with(latest));
608 }
609
610 #[test]
611 fn test_serde() -> Result<(), Box<dyn std::error::Error>> {
612 let v = ProtocolVersion::V2025_11_25;
613
614 // Serialize to string
615 let json = serde_json::to_string(&v)?;
616 assert_eq!(json, "\"2025-11-25\"");
617
618 // Deserialize from string
619 let parsed: ProtocolVersion = serde_json::from_str("\"2024-11-05\"")?;
620 assert_eq!(parsed, ProtocolVersion::V2024_11_05);
621 Ok(())
622 }
623}