mcp_usage_core/
version.rs1use std::fmt;
22
23pub const HEADER_VALIDATION_SINCE: &str = "2026-07-28";
26
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct ProtocolVersion(String);
35
36impl ProtocolVersion {
37 #[must_use]
42 pub fn parse(raw: &str) -> Option<Self> {
43 valid_date(raw).then(|| Self(raw.to_owned()))
44 }
45
46 #[must_use]
48 pub fn as_str(&self) -> &str {
49 &self.0
50 }
51
52 #[must_use]
55 pub fn requires_header_validation(&self) -> bool {
56 self.0.as_str() >= HEADER_VALIDATION_SINCE
57 }
58}
59
60impl fmt::Display for ProtocolVersion {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.write_str(&self.0)
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum HeaderTrust {
69 Trusted(ProtocolVersion),
71 Reject(TrustFailure),
73}
74
75impl HeaderTrust {
76 #[must_use]
78 pub const fn is_trusted(&self) -> bool {
79 matches!(self, Self::Trusted(_))
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum TrustFailure {
86 MissingVersionHeader,
89 UnvalidatedRevision(ProtocolVersion),
91 MalformedVersion(String),
94}
95
96impl fmt::Display for TrustFailure {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 match self {
99 Self::MissingVersionHeader => {
100 f.write_str("missing MCP-Protocol-Version header; mirrored headers are unvalidated")
101 }
102 Self::UnvalidatedRevision(v) => write!(
103 f,
104 "protocol version {v} predates {HEADER_VALIDATION_SINCE}; \
105 mirrored headers are not validated against the body"
106 ),
107 Self::MalformedVersion(v) => write!(f, "malformed MCP-Protocol-Version value {v:?}"),
108 }
109 }
110}
111
112#[must_use]
116pub fn assess(version_header: Option<&str>) -> HeaderTrust {
117 match validate_header(version_header) {
118 Ok(()) => HeaderTrust::Trusted(ProtocolVersion(
119 version_header.unwrap_or_default().to_owned(),
120 )),
121 Err(failure) => HeaderTrust::Reject(failure),
122 }
123}
124
125pub fn validate_header(version_header: Option<&str>) -> Result<(), TrustFailure> {
136 let Some(raw) = version_header else {
137 return Err(TrustFailure::MissingVersionHeader);
138 };
139 if !valid_date(raw) {
140 return Err(TrustFailure::MalformedVersion(raw.to_owned()));
141 }
142 if raw < HEADER_VALIDATION_SINCE {
143 return Err(TrustFailure::UnvalidatedRevision(ProtocolVersion(
144 raw.to_owned(),
145 )));
146 }
147 Ok(())
148}
149
150fn valid_date(raw: &str) -> bool {
151 let bytes = raw.as_bytes();
152 if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
153 return false;
154 }
155 if ![0, 1, 2, 3, 5, 6, 8, 9]
156 .iter()
157 .all(|&i| bytes[i].is_ascii_digit())
158 {
159 return false;
160 }
161 let year = parse_digits(&bytes[0..4]);
162 let month = parse_digits(&bytes[5..7]);
163 let day = parse_digits(&bytes[8..10]);
164 let max_day = match month {
165 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
166 4 | 6 | 9 | 11 => 30,
167 2 if is_leap_year(year) => 29,
168 2 => 28,
169 _ => return false,
170 };
171 day != 0 && day <= max_day
172}
173
174fn parse_digits(bytes: &[u8]) -> u32 {
175 bytes
176 .iter()
177 .fold(0, |value, byte| value * 10 + u32::from(byte - b'0'))
178}
179
180const fn is_leap_year(year: u32) -> bool {
181 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn known_revisions_parse_and_order_chronologically() {
190 let revisions = [
191 "2024-11-05",
192 "2025-03-26",
193 "2025-06-18",
194 "2025-11-25",
195 "2026-07-28",
196 ];
197 let parsed: Vec<ProtocolVersion> = revisions
198 .iter()
199 .map(|r| ProtocolVersion::parse(r).expect("known revision parses"))
200 .collect();
201
202 let mut sorted = parsed.clone();
203 sorted.sort();
204 assert_eq!(parsed, sorted, "byte order must equal chronological order");
205 }
206
207 #[test]
208 fn only_the_current_revision_mandates_validation() {
209 for old in ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"] {
210 let v = ProtocolVersion::parse(old).unwrap();
211 assert!(
212 !v.requires_header_validation(),
213 "{old} must not be treated as validating"
214 );
215 }
216 assert!(
217 ProtocolVersion::parse("2026-07-28")
218 .unwrap()
219 .requires_header_validation()
220 );
221 }
222
223 #[test]
224 fn future_revisions_are_trusted() {
225 for future in ["2026-08-01", "2027-01-01", "2099-12-31"] {
229 assert!(
230 ProtocolVersion::parse(future)
231 .unwrap()
232 .requires_header_validation(),
233 "{future} should be trusted"
234 );
235 }
236 }
237
238 #[test]
239 fn malformed_versions_are_rejected_not_parsed() {
240 for bad in [
241 "draft",
242 "latest",
243 "2026-7-28",
244 "2026/07/28",
245 "20260728",
246 "2026-07-28 ",
247 "",
248 "202X-07-28",
249 "2026-00-10",
250 "2026-13-01",
251 "2026-04-31",
252 "2026-02-29",
253 "2024-02-30",
254 ] {
255 assert!(
256 ProtocolVersion::parse(bad).is_none(),
257 "{bad:?} should not parse"
258 );
259 }
260 }
261
262 #[test]
263 fn leap_days_are_calendar_validated() {
264 assert!(ProtocolVersion::parse("2024-02-29").is_some());
265 assert!(ProtocolVersion::parse("2000-02-29").is_some());
266 assert!(ProtocolVersion::parse("2100-02-29").is_none());
267 }
268
269 #[test]
270 fn assess_rejects_absent_header() {
271 assert_eq!(
272 assess(None),
273 HeaderTrust::Reject(TrustFailure::MissingVersionHeader)
274 );
275 }
276
277 #[test]
278 fn assess_rejects_legacy_rather_than_falling_back() {
279 let verdict = assess(Some("2025-11-25"));
282 assert!(!verdict.is_trusted());
283 assert!(matches!(
284 verdict,
285 HeaderTrust::Reject(TrustFailure::UnvalidatedRevision(_))
286 ));
287 }
288
289 #[test]
290 fn assess_rejects_garbage_without_assuming_in_the_clients_favour() {
291 let verdict = assess(Some("not-a-version"));
292 assert!(!verdict.is_trusted());
293 assert!(matches!(
294 verdict,
295 HeaderTrust::Reject(TrustFailure::MalformedVersion(_))
296 ));
297 }
298
299 #[test]
300 fn assess_trusts_the_current_revision() {
301 let verdict = assess(Some("2026-07-28"));
302 assert!(verdict.is_trusted());
303 assert_eq!(
304 verdict,
305 HeaderTrust::Trusted(ProtocolVersion::parse("2026-07-28").unwrap())
306 );
307 }
308}