mcp_trace_validator/checks/draft/meta/
trace_context.rs1use serde_json::Value;
14
15use super::super::super::FindingSink;
16use crate::context::TraceContext;
17
18#[cfg(test)]
19mod tests;
20
21pub(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 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
64fn 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}