Skip to main content

mcp_usage_core/
version.rs

1//! Protocol version, and the guard that decides whether the mirrored headers may
2//! be trusted at all.
3//!
4//! The mirrored headers (`Mcp-Method`, `Mcp-Name`, `Mcp-Param-*`) are only
5//! usable because 2026-07-28 obliges the origin server to reject any request
6//! whose headers disagree with its body, with `-32020 HeaderMismatch`. On an
7//! earlier revision no such obligation exists, so the client controls those
8//! values. The specification gives intermediaries this requirement:
9//!
10//! > Intermediaries that enforce policy based on mirrored headers (e.g., routing
11//! > or rate-limiting by tenant) SHOULD verify that the `MCP-Protocol-Version`
12//! > header indicates a version that requires header-body validation. If the
13//! > version is older or the header is absent, the intermediary SHOULD reject the
14//! > request rather than trusting unvalidated header values.
15//!
16//! Note "reject", not "fall back to parsing the body". A meter that quietly
17//! degrades to body parsing on a legacy request is metering a request the origin
18//! will never validate, and a client that wants to be billed for a cheap tool
19//! while calling an expensive one only has to claim an old protocol version.
20
21use std::fmt;
22
23/// The revision from which header/body validation is mandatory, and therefore
24/// from which mirrored headers can be trusted.
25pub const HEADER_VALIDATION_SINCE: &str = "2026-07-28";
26
27/// A dated MCP protocol version, e.g. `2026-07-28`.
28///
29/// Dated versions are `YYYY-MM-DD`, so byte ordering is chronological ordering
30/// and comparison needs no date parsing. That only holds for well-formed values,
31/// which is why [`ProtocolVersion::parse`] validates the shape rather than
32/// accepting any string.
33#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct ProtocolVersion(String);
35
36impl ProtocolVersion {
37    /// Parse a dated version string, returning `None` if it is not `YYYY-MM-DD`.
38    ///
39    /// Non-dated labels such as `draft` are rejected. They carry no
40    /// ordering, so admitting them would make the comparison below meaningless.
41    #[must_use]
42    pub fn parse(raw: &str) -> Option<Self> {
43        valid_date(raw).then(|| Self(raw.to_owned()))
44    }
45
46    /// The wire string.
47    #[must_use]
48    pub fn as_str(&self) -> &str {
49        &self.0
50    }
51
52    /// Whether this revision obliges the origin to validate headers against the
53    /// body, and therefore whether an intermediary may price on those headers.
54    #[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/// The verdict on whether mirrored headers may drive billing for this request.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum HeaderTrust {
69    /// The request declares a revision that mandates header/body validation.
70    Trusted(ProtocolVersion),
71    /// The request must be rejected rather than metered on its headers.
72    Reject(TrustFailure),
73}
74
75impl HeaderTrust {
76    /// Whether the headers may be used to price this request.
77    #[must_use]
78    pub const fn is_trusted(&self) -> bool {
79        matches!(self, Self::Trusted(_))
80    }
81}
82
83/// Why mirrored headers could not be trusted.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum TrustFailure {
86    /// No `MCP-Protocol-Version` header. Pre-2025-06-18 clients omitted it, so
87    /// its absence means an era with no validation obligation.
88    MissingVersionHeader,
89    /// A well-formed revision that predates the validation requirement.
90    UnvalidatedRevision(ProtocolVersion),
91    /// Not a dated version at all. Unorderable, so it cannot be shown to be new
92    /// enough, and an intermediary must not assume in the client's favour.
93    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/// Decide whether this request's mirrored headers may drive billing.
113///
114/// Pass the raw `MCP-Protocol-Version` header value, or `None` when it is absent.
115#[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
125/// Validate a protocol-version header without allocating on the trusted path.
126///
127/// This is the entry point for latency-sensitive intermediaries that need only
128/// a yes/no trust decision. Owned strings are created solely for rejection
129/// diagnostics.
130///
131/// # Errors
132///
133/// Returns the precise [`TrustFailure`] for missing, malformed, or legacy
134/// revisions.
135pub 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        // A later dated revision cannot remove the validation requirement without
226        // a new spec, and refusing to meter traffic from a newer client would take
227        // the gateway down on release day.
228        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        // The failure mode this guards: a client claiming an old revision so its
280        // unvalidated Mcp-Name is believed by the meter.
281        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}