Skip to main content

mcp_trace_validator/checks/
support.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. (https://github.com/tomtom215)
3
4//! Shared helpers for feature-area checks: declared-capability lookups and the
5//! zero-dependency encoding validators (base64, RFC 3986 scheme syntax) that several
6//! areas judge against.
7
8use serde_json::Value;
9
10use crate::context::TraceContext;
11
12/// Whether the server declared the capability at `path` (e.g. `["tools"]` or
13/// `["resources", "subscribe"]`): every segment resolves and the final value is
14/// neither `false` nor `null` — the ADR-0006 reading. Returns `None` when the trace
15/// has no `initialize` result to read declarations from (judgment must abstain), and
16/// `Some(declared)` otherwise.
17pub(super) fn server_capability(context: &TraceContext<'_>, path: &[&str]) -> Option<bool> {
18    capability_in(context.server_capabilities(), path, context)
19}
20
21/// The client-side counterpart of [`server_capability`], read from the `initialize`
22/// request params.
23pub(super) fn client_capability(context: &TraceContext<'_>, path: &[&str]) -> Option<bool> {
24    capability_in(context.client_capabilities(), path, context)
25}
26
27fn capability_in(
28    capabilities: Option<&Value>,
29    path: &[&str],
30    context: &TraceContext<'_>,
31) -> Option<bool> {
32    // No initialize result at all: there is no declaration surface, so the session's
33    // capability state is unknowable rather than empty.
34    context.initialize().result?;
35    let Some(mut current) = capabilities else {
36        return Some(false);
37    };
38    for segment in path {
39        match current.get(segment) {
40            Some(next) => current = next,
41            None => return Some(false),
42        }
43    }
44    Some(!(current.is_null() || matches!(current, Value::Bool(false))))
45}
46
47/// `true` when `text` is standard base64 (RFC 4648 §4 alphabet, `=` padding to a
48/// multiple of four, padding only at the end). Validation only — nothing is decoded.
49pub(super) fn is_base64(text: &str) -> bool {
50    let bytes = text.as_bytes();
51    if bytes.len() % 4 != 0 {
52        return false;
53    }
54    let padding = bytes.iter().rev().take_while(|&&b| b == b'=').count();
55    if padding > 2 {
56        return false;
57    }
58    let content = &bytes[..bytes.len() - padding];
59    content
60        .iter()
61        .all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/')
62}
63
64/// `true` when `uri` begins with an RFC 3986 §3.1 scheme followed by `:`:
65/// `ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`. Judges scheme syntax only — the
66/// registry documents that deeper RFC 3986 validation is out of trace scope.
67pub(super) fn has_rfc3986_scheme(uri: &str) -> bool {
68    let Some((scheme, _)) = uri.split_once(':') else {
69        return false;
70    };
71    let mut chars = scheme.chars();
72    let Some(first) = chars.next() else {
73        return false;
74    };
75    first.is_ascii_alphabetic()
76        && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn base64_validation_is_exact() {
85        for valid in ["", "aGk=", "aGV5", "aGV5bw==", "AB+/", "QUJDRA=="] {
86            assert!(is_base64(valid), "{valid:?} should validate");
87        }
88        for invalid in [
89            "aGk",     // length not a multiple of four
90            "aGk =",   // space in alphabet
91            "aGk!",    // symbol outside alphabet
92            "====",    // padding longer than two
93            "aG=k",    // padding before the end
94            "aGV5bw=", // wrong padding length for content
95        ] {
96            assert!(!is_base64(invalid), "{invalid:?} should not validate");
97        }
98    }
99
100    #[test]
101    fn rfc3986_scheme_validation_is_exact() {
102        for valid in ["https://x", "file:///a", "git://r", "a:", "z+ssh.2-x:rest"] {
103            assert!(has_rfc3986_scheme(valid), "{valid:?} should validate");
104        }
105        for invalid in [
106            "",           // no scheme at all
107            "no-colon",   // not a URI
108            ":rest",      // empty scheme
109            "1https://x", // scheme must start with ALPHA
110            "ht tp://x",  // space inside scheme
111            "ht_tp://x",  // underscore is not scheme syntax
112        ] {
113            assert!(
114                !has_rfc3986_scheme(invalid),
115                "{invalid:?} should not validate"
116            );
117        }
118    }
119}