turbomcp_protocol/
versioning.rs

1//! # Protocol Versioning and Compatibility
2//!
3//! This module provides comprehensive protocol version management and compatibility
4//! checking for MCP implementations.
5
6use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8use std::fmt;
9use std::str::FromStr;
10
11/// Version manager for handling protocol versions
12#[derive(Debug, Clone)]
13pub struct VersionManager {
14    /// Supported versions in order of preference
15    supported_versions: Vec<Version>,
16    /// Current protocol version
17    current_version: Version,
18}
19
20/// Semantic version representation
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct Version {
23    /// Year component
24    pub year: u16,
25    /// Month component  
26    pub month: u8,
27    /// Day component
28    pub day: u8,
29}
30
31/// Version compatibility result
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum VersionCompatibility {
34    /// Versions are fully compatible
35    Compatible,
36    /// Versions are compatible with warnings
37    CompatibleWithWarnings(Vec<String>),
38    /// Versions are incompatible
39    Incompatible(String),
40}
41
42/// Version requirement specification
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum VersionRequirement {
45    /// Exact version match
46    Exact(Version),
47    /// Minimum version required
48    Minimum(Version),
49    /// Maximum version supported
50    Maximum(Version),
51    /// Version range (inclusive)
52    Range(Version, Version),
53    /// Any version from the list
54    Any(Vec<Version>),
55}
56
57impl Version {
58    /// Create a new version
59    ///
60    /// # Errors
61    ///
62    /// Returns [`VersionError::InvalidMonth`] if month is not in range 1-12.
63    /// Returns [`VersionError::InvalidDay`] if day is invalid for the given month.
64    pub fn new(year: u16, month: u8, day: u8) -> Result<Self, VersionError> {
65        if !(1..=12).contains(&month) {
66            return Err(VersionError::InvalidMonth(month.to_string()));
67        }
68
69        if !(1..=31).contains(&day) {
70            return Err(VersionError::InvalidDay(day.to_string()));
71        }
72
73        // Basic month/day validation
74        if month == 2 && day > 29 {
75            return Err(VersionError::InvalidDay(format!(
76                "{} (invalid for February)",
77                day
78            )));
79        }
80
81        if matches!(month, 4 | 6 | 9 | 11) && day > 30 {
82            return Err(VersionError::InvalidDay(format!(
83                "{} (month {} only has 30 days)",
84                day, month
85            )));
86        }
87
88        Ok(Self { year, month, day })
89    }
90
91    /// Get the current MCP protocol version
92    pub fn current() -> Self {
93        Self {
94            year: 2025,
95            month: 6,
96            day: 18,
97        }
98    }
99
100    /// Check if this version is newer than another
101    pub fn is_newer_than(&self, other: &Version) -> bool {
102        self > other
103    }
104
105    /// Check if this version is older than another
106    pub fn is_older_than(&self, other: &Version) -> bool {
107        self < other
108    }
109
110    /// Check if this version is compatible with another
111    pub fn is_compatible_with(&self, other: &Version) -> bool {
112        // For MCP, we consider versions compatible if they're the same
113        // or if the difference is minor (same year)
114        self.year == other.year
115    }
116
117    /// Get version as a date string (YYYY-MM-DD)
118    pub fn to_date_string(&self) -> String {
119        format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
120    }
121
122    /// Parse version from date string
123    ///
124    /// # Errors
125    ///
126    /// Returns [`VersionError`] if the string is not in `YYYY-MM-DD` format
127    /// or contains invalid date components.
128    pub fn from_date_string(s: &str) -> Result<Self, VersionError> {
129        s.parse()
130    }
131
132    /// Get all known MCP versions
133    pub fn known_versions() -> Vec<Version> {
134        vec![
135            Version::new(2025, 6, 18).unwrap(), // Current
136            Version::new(2024, 11, 5).unwrap(), // Previous
137            Version::new(2024, 6, 25).unwrap(), // Older
138        ]
139    }
140}
141
142impl fmt::Display for Version {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "{}", self.to_date_string())
145    }
146}
147
148impl FromStr for Version {
149    type Err = VersionError;
150
151    fn from_str(s: &str) -> Result<Self, Self::Err> {
152        let parts: Vec<&str> = s.split('-').collect();
153
154        if parts.len() != 3 {
155            return Err(VersionError::InvalidFormat(s.to_string()));
156        }
157
158        let year = parts[0]
159            .parse::<u16>()
160            .map_err(|_| VersionError::InvalidYear(parts[0].to_string()))?;
161
162        let month = parts[1]
163            .parse::<u8>()
164            .map_err(|_| VersionError::InvalidMonth(parts[1].to_string()))?;
165
166        let day = parts[2]
167            .parse::<u8>()
168            .map_err(|_| VersionError::InvalidDay(parts[2].to_string()))?;
169
170        Self::new(year, month, day)
171    }
172}
173
174impl PartialOrd for Version {
175    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
176        Some(self.cmp(other))
177    }
178}
179
180impl Ord for Version {
181    fn cmp(&self, other: &Self) -> Ordering {
182        (self.year, self.month, self.day).cmp(&(other.year, other.month, other.day))
183    }
184}
185
186impl VersionManager {
187    /// Create a new version manager
188    ///
189    /// # Errors
190    ///
191    /// Returns [`VersionError::NoSupportedVersions`] if the provided vector is empty.
192    pub fn new(supported_versions: Vec<Version>) -> Result<Self, VersionError> {
193        if supported_versions.is_empty() {
194            return Err(VersionError::NoSupportedVersions);
195        }
196
197        let mut versions = supported_versions;
198        versions.sort_by(|a, b| b.cmp(a)); // Sort newest first
199
200        let current_version = versions[0].clone();
201
202        Ok(Self {
203            supported_versions: versions,
204            current_version,
205        })
206    }
207
208    /// Create a version manager with default MCP versions
209    pub fn with_default_versions() -> Self {
210        Self::new(Version::known_versions()).unwrap()
211    }
212    /// Get the current version
213    pub fn current_version(&self) -> &Version {
214        &self.current_version
215    }
216
217    /// Get all supported versions
218    pub fn supported_versions(&self) -> &[Version] {
219        &self.supported_versions
220    }
221
222    /// Check if a version is supported
223    pub fn is_version_supported(&self, version: &Version) -> bool {
224        self.supported_versions.contains(version)
225    }
226
227    /// Find the best compatible version for a client request
228    pub fn negotiate_version(&self, client_versions: &[Version]) -> Option<Version> {
229        // Find the newest version that both client and server support
230        for server_version in &self.supported_versions {
231            if client_versions.contains(server_version) {
232                return Some(server_version.clone());
233            }
234        }
235
236        None
237    }
238
239    /// Check compatibility between two versions
240    pub fn check_compatibility(
241        &self,
242        client_version: &Version,
243        server_version: &Version,
244    ) -> VersionCompatibility {
245        if client_version == server_version {
246            return VersionCompatibility::Compatible;
247        }
248
249        // Check if versions are in the same year (considered compatible)
250        if client_version.year == server_version.year {
251            let warning = format!(
252                "Version mismatch but compatible: client={client_version}, server={server_version}"
253            );
254            return VersionCompatibility::CompatibleWithWarnings(vec![warning]);
255        }
256
257        // Major version difference
258        let reason =
259            format!("Incompatible versions: client={client_version}, server={server_version}");
260        VersionCompatibility::Incompatible(reason)
261    }
262
263    /// Get the minimum supported version
264    pub fn minimum_version(&self) -> &Version {
265        // SAFETY: Constructor ensures non-empty via Result<T, VersionError::NoSupportedVersions>
266        self.supported_versions
267            .last()
268            .expect("BUG: VersionManager has no versions (constructor should prevent this)")
269    }
270
271    /// Get the maximum supported version  
272    pub fn maximum_version(&self) -> &Version {
273        &self.supported_versions[0] // First because sorted newest first
274    }
275
276    /// Check if a version requirement is satisfied
277    pub fn satisfies_requirement(
278        &self,
279        version: &Version,
280        requirement: &VersionRequirement,
281    ) -> bool {
282        match requirement {
283            VersionRequirement::Exact(required) => version == required,
284            VersionRequirement::Minimum(min) => version >= min,
285            VersionRequirement::Maximum(max) => version <= max,
286            VersionRequirement::Range(min, max) => version >= min && version <= max,
287            VersionRequirement::Any(versions) => versions.contains(version),
288        }
289    }
290}
291
292impl Default for VersionManager {
293    fn default() -> Self {
294        Self::with_default_versions()
295    }
296}
297
298impl VersionRequirement {
299    /// Create an exact version requirement
300    pub fn exact(version: Version) -> Self {
301        Self::Exact(version)
302    }
303
304    /// Create a minimum version requirement
305    pub fn minimum(version: Version) -> Self {
306        Self::Minimum(version)
307    }
308
309    /// Create a maximum version requirement
310    pub fn maximum(version: Version) -> Self {
311        Self::Maximum(version)
312    }
313
314    /// Create a version range requirement
315    ///
316    /// # Errors
317    ///
318    /// Returns [`VersionError::InvalidRange`] if `min` is greater than `max`.
319    pub fn range(min: Version, max: Version) -> Result<Self, VersionError> {
320        if min > max {
321            return Err(VersionError::InvalidRange(min, max));
322        }
323        Ok(Self::Range(min, max))
324    }
325
326    /// Create an "any of" requirement
327    ///
328    /// # Errors
329    ///
330    /// Returns [`VersionError::EmptyVersionList`] if the provided vector is empty.
331    pub fn any(versions: Vec<Version>) -> Result<Self, VersionError> {
332        if versions.is_empty() {
333            return Err(VersionError::EmptyVersionList);
334        }
335        Ok(Self::Any(versions))
336    }
337
338    /// Check if a version satisfies this requirement
339    pub fn is_satisfied_by(&self, version: &Version) -> bool {
340        match self {
341            Self::Exact(required) => version == required,
342            Self::Minimum(min) => version >= min,
343            Self::Maximum(max) => version <= max,
344            Self::Range(min, max) => version >= min && version <= max,
345            Self::Any(versions) => versions.contains(version),
346        }
347    }
348}
349
350/// Version-related errors
351#[derive(Debug, Clone, thiserror::Error)]
352pub enum VersionError {
353    /// Invalid version format
354    #[error("Invalid version format: {0}")]
355    InvalidFormat(String),
356    /// Invalid year
357    #[error("Invalid year: {0}")]
358    InvalidYear(String),
359    /// Invalid month
360    #[error("Invalid month: {0} (must be 1-12)")]
361    InvalidMonth(String),
362    /// Invalid day
363    #[error("Invalid day: {0} (must be 1-31)")]
364    InvalidDay(String),
365    /// No supported versions
366    #[error("No supported versions provided")]
367    NoSupportedVersions,
368    /// Invalid version range
369    #[error("Invalid version range: {0} > {1}")]
370    InvalidRange(Version, Version),
371    /// Empty version list
372    #[error("Empty version list")]
373    EmptyVersionList,
374}
375
376/// Utility functions for version management
377pub mod utils {
378    use super::*;
379
380    /// Parse multiple versions from strings
381    ///
382    /// # Errors
383    ///
384    /// Returns [`VersionError`] if any version string cannot be parsed.
385    pub fn parse_versions(version_strings: &[&str]) -> Result<Vec<Version>, VersionError> {
386        version_strings.iter().map(|s| s.parse()).collect()
387    }
388
389    /// Find the newest version in a list
390    pub fn newest_version(versions: &[Version]) -> Option<&Version> {
391        versions.iter().max()
392    }
393
394    /// Find the oldest version in a list
395    pub fn oldest_version(versions: &[Version]) -> Option<&Version> {
396        versions.iter().min()
397    }
398
399    /// Check if all versions in a list are compatible with each other
400    pub fn are_all_compatible(versions: &[Version]) -> bool {
401        if versions.len() < 2 {
402            return true;
403        }
404
405        let first = &versions[0];
406        versions.iter().all(|v| first.is_compatible_with(v))
407    }
408
409    /// Get a human-readable description of version compatibility
410    pub fn compatibility_description(compatibility: &VersionCompatibility) -> String {
411        match compatibility {
412            VersionCompatibility::Compatible => "Fully compatible".to_string(),
413            VersionCompatibility::CompatibleWithWarnings(warnings) => {
414                format!("Compatible with warnings: {}", warnings.join(", "))
415            }
416            VersionCompatibility::Incompatible(reason) => {
417                format!("Incompatible: {reason}")
418            }
419        }
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use proptest::prelude::*;
427
428    #[test]
429    fn test_version_creation() {
430        let version = Version::new(2025, 6, 18).unwrap();
431        assert_eq!(version.year, 2025);
432        assert_eq!(version.month, 6);
433        assert_eq!(version.day, 18);
434
435        // Invalid month should fail
436        assert!(Version::new(2025, 13, 18).is_err());
437
438        // Invalid day should fail
439        assert!(Version::new(2025, 6, 32).is_err());
440    }
441
442    #[test]
443    fn test_version_parsing() {
444        let version: Version = "2025-06-18".parse().unwrap();
445        assert_eq!(version, Version::new(2025, 6, 18).unwrap());
446
447        // Invalid format should fail
448        assert!("2025/06/18".parse::<Version>().is_err());
449        assert!("invalid".parse::<Version>().is_err());
450    }
451
452    #[test]
453    fn test_version_comparison() {
454        let v1 = Version::new(2025, 6, 18).unwrap();
455        let v2 = Version::new(2024, 11, 5).unwrap();
456        let v3 = Version::new(2025, 6, 18).unwrap();
457
458        assert!(v1 > v2);
459        assert!(v1.is_newer_than(&v2));
460        assert!(v2.is_older_than(&v1));
461        assert_eq!(v1, v3);
462    }
463
464    #[test]
465    fn test_version_compatibility() {
466        let v1 = Version::new(2025, 6, 18).unwrap();
467        let v2 = Version::new(2025, 12, 1).unwrap(); // Same year
468        let v3 = Version::new(2024, 6, 18).unwrap(); // Different year
469
470        assert!(v1.is_compatible_with(&v2));
471        assert!(!v1.is_compatible_with(&v3));
472    }
473
474    #[test]
475    fn test_version_manager() {
476        let versions = vec![
477            Version::new(2025, 6, 18).unwrap(),
478            Version::new(2024, 11, 5).unwrap(),
479        ];
480
481        let manager = VersionManager::new(versions).unwrap();
482
483        assert_eq!(
484            manager.current_version(),
485            &Version::new(2025, 6, 18).unwrap()
486        );
487        assert!(manager.is_version_supported(&Version::new(2024, 11, 5).unwrap()));
488        assert!(!manager.is_version_supported(&Version::new(2023, 1, 1).unwrap()));
489    }
490
491    #[test]
492    fn test_version_negotiation() {
493        let manager = VersionManager::default();
494
495        let client_versions = vec![
496            Version::new(2024, 11, 5).unwrap(),
497            Version::new(2025, 6, 18).unwrap(),
498        ];
499
500        let negotiated = manager.negotiate_version(&client_versions);
501        assert_eq!(negotiated, Some(Version::new(2025, 6, 18).unwrap()));
502    }
503
504    #[test]
505    fn test_version_requirements() {
506        let version = Version::new(2025, 6, 18).unwrap();
507
508        let exact_req = VersionRequirement::exact(version.clone());
509        assert!(exact_req.is_satisfied_by(&version));
510
511        let min_req = VersionRequirement::minimum(Version::new(2024, 1, 1).unwrap());
512        assert!(min_req.is_satisfied_by(&version));
513
514        let max_req = VersionRequirement::maximum(Version::new(2024, 1, 1).unwrap());
515        assert!(!max_req.is_satisfied_by(&version));
516    }
517
518    #[test]
519    fn test_compatibility_checking() {
520        let manager = VersionManager::default();
521
522        let v1 = Version::new(2025, 6, 18).unwrap();
523        let v2 = Version::new(2025, 12, 1).unwrap();
524        let v3 = Version::new(2024, 1, 1).unwrap();
525
526        // Same year - compatible with warnings
527        let compat = manager.check_compatibility(&v1, &v2);
528        assert!(matches!(
529            compat,
530            VersionCompatibility::CompatibleWithWarnings(_)
531        ));
532
533        // Different year - incompatible
534        let compat = manager.check_compatibility(&v1, &v3);
535        assert!(matches!(compat, VersionCompatibility::Incompatible(_)));
536
537        // Exact match - compatible
538        let compat = manager.check_compatibility(&v1, &v1);
539        assert_eq!(compat, VersionCompatibility::Compatible);
540    }
541
542    #[test]
543    fn test_utils() {
544        let versions = utils::parse_versions(&["2025-06-18", "2024-11-05"]).unwrap();
545        assert_eq!(versions.len(), 2);
546
547        let newest = utils::newest_version(&versions);
548        assert_eq!(newest, Some(&Version::new(2025, 6, 18).unwrap()));
549
550        let oldest = utils::oldest_version(&versions);
551        assert_eq!(oldest, Some(&Version::new(2024, 11, 5).unwrap()));
552    }
553
554    // Property-based tests for comprehensive coverage
555    proptest! {
556        #[test]
557        fn test_version_parse_roundtrip(
558            year in 2020u16..2030u16,
559            month in 1u8..=12u8,
560            day in 1u8..=28u8, // Use 28 to avoid month-specific day validation
561        ) {
562            let version = Version::new(year, month, day)?;
563            let string = version.to_date_string();
564            let parsed = Version::from_date_string(&string)?;
565            prop_assert_eq!(version, parsed);
566        }
567
568        #[test]
569        fn test_version_comparison_transitive(
570            y1 in 2020u16..2030u16,
571            m1 in 1u8..=12u8,
572            d1 in 1u8..=28u8,
573            y2 in 2020u16..2030u16,
574            m2 in 1u8..=12u8,
575            d2 in 1u8..=28u8,
576            y3 in 2020u16..2030u16,
577            m3 in 1u8..=12u8,
578            d3 in 1u8..=28u8,
579        ) {
580            let v1 = Version::new(y1, m1, d1)?;
581            let v2 = Version::new(y2, m2, d2)?;
582            let v3 = Version::new(y3, m3, d3)?;
583
584            // Transitivity: if v1 < v2 and v2 < v3, then v1 < v3
585            if v1 < v2 && v2 < v3 {
586                prop_assert!(v1 < v3);
587            }
588        }
589
590        #[test]
591        fn test_version_compatibility_symmetric(
592            year in 2020u16..2030u16,
593            m1 in 1u8..=12u8,
594            d1 in 1u8..=28u8,
595            m2 in 1u8..=12u8,
596            d2 in 1u8..=28u8,
597        ) {
598            let v1 = Version::new(year, m1, d1)?;
599            let v2 = Version::new(year, m2, d2)?;
600
601            // Same-year versions should be compatible in both directions
602            prop_assert_eq!(v1.is_compatible_with(&v2), v2.is_compatible_with(&v1));
603        }
604
605        #[test]
606        fn test_invalid_month_rejected(
607            year in 2020u16..2030u16,
608            month in 13u8..=255u8,
609            day in 1u8..=28u8,
610        ) {
611            prop_assert!(Version::new(year, month, day).is_err());
612        }
613
614        #[test]
615        fn test_invalid_day_rejected(
616            year in 2020u16..2030u16,
617            month in 1u8..=12u8,
618            day in 32u8..=255u8,
619        ) {
620            prop_assert!(Version::new(year, month, day).is_err());
621        }
622    }
623}