Skip to main content

mcp_trace_validator/checks/draft/meta/
trace_context.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. (https://github.com/tomtom215)
3
4//! `BASE-040`: the OpenTelemetry trace-context keys, and their formats.
5//!
6//! Split from [`super`] because it is the one `_meta` clause that is not about
7//! the protocol's own fields. `traceparent`, `tracestate` and `baggage` are the
8//! specification's single exception to the `_meta` prefix rule — reserved
9//! outright "to maintain compatibility with existing implementations and
10//! OpenTelemetry semantic conventions for MCP" — so the grammar they must
11//! follow is W3C's, not MCP's, and it is dense enough to read on its own.
12
13use serde_json::Value;
14
15use super::super::super::FindingSink;
16use crate::context::TraceContext;
17
18#[cfg(test)]
19mod tests;
20
21/// `BASE-040`: `traceparent` and `tracestate`/`baggage` follow their W3C formats.
22///
23/// Only the `traceparent` grammar is fixed enough to judge from a trace: version
24/// `00`, a 32-hex trace id that is not all zeroes, a 16-hex parent id that is not
25/// all zeroes, and 2 hex flags. `tracestate` and `baggage` are list formats whose
26/// members are vendor-defined, so only their gross shape is checked.
27pub(in crate::checks) fn trace_context_format(context: &TraceContext<'_>, sink: &mut FindingSink) {
28    for (event, _, _) in context.messages() {
29        let Some(payload) = event.message_payload() else {
30            continue;
31        };
32        for envelope in ["params", "result"] {
33            let meta = payload
34                .get(envelope)
35                .and_then(|member| member.get("_meta"))
36                .and_then(Value::as_object);
37            let Some(meta) = meta else { continue };
38            // The subject is a trace-context field that is actually present:
39            // the clause binds their format, not their use.
40            if let Some(value) = meta.get("traceparent") {
41                sink.examined();
42                if let Err(reason) = validate_traceparent(value) {
43                    sink.push(
44                        Some(event.seq),
45                        format!("{envelope}._meta.traceparent {reason}"),
46                    );
47                }
48            }
49            for key in ["tracestate", "baggage"] {
50                if let Some(value) = meta.get(key) {
51                    sink.examined();
52                    if !value.is_string() {
53                        sink.push(
54                            Some(event.seq),
55                            format!("{envelope}._meta.{key} is not a string"),
56                        );
57                    }
58                }
59            }
60        }
61    }
62}
63
64/// The W3C Trace Context `traceparent` grammar, version `00`.
65fn validate_traceparent(value: &Value) -> Result<(), String> {
66    let Some(text) = value.as_str() else {
67        return Err("is not a string".to_owned());
68    };
69    let parts: Vec<&str> = text.split('-').collect();
70    let [version, trace_id, parent_id, flags] = parts.as_slice() else {
71        return Err(format!(
72            "is {text:?}; W3C Trace Context requires four `-`-separated fields"
73        ));
74    };
75    let hex = |s: &str| {
76        s.chars()
77            .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
78    };
79    if version.len() != 2 || !hex(version) {
80        return Err(format!(
81            "has version {version:?}; expected two lowercase hex digits"
82        ));
83    }
84    if trace_id.len() != 32 || !hex(trace_id) {
85        return Err(format!(
86            "has trace-id {trace_id:?}; expected 32 lowercase hex digits"
87        ));
88    }
89    if trace_id.bytes().all(|b| b == b'0') {
90        return Err("has an all-zero trace-id, which W3C Trace Context forbids".to_owned());
91    }
92    if parent_id.len() != 16 || !hex(parent_id) {
93        return Err(format!(
94            "has parent-id {parent_id:?}; expected 16 lowercase hex digits"
95        ));
96    }
97    if parent_id.bytes().all(|b| b == b'0') {
98        return Err("has an all-zero parent-id, which W3C Trace Context forbids".to_owned());
99    }
100    if flags.len() != 2 || !hex(flags) {
101        return Err(format!(
102            "has flags {flags:?}; expected two lowercase hex digits"
103        ));
104    }
105    Ok(())
106}