Skip to main content

tower_mcp/
trace_context.rs

1//! W3C Trace Context carried in request `_meta` (SEP-414).
2//!
3//! SEP-414 reserves the `traceparent` and `tracestate` keys in a request's
4//! `_meta` object for [W3C Trace Context]. A client that is already inside a
5//! trace can send them so the server's work joins that trace.
6//!
7//! [`JsonRpcService`](crate::jsonrpc::JsonRpcService) reads both keys from
8//! `params._meta` on every request, on the 2025-11-25 lifecycle and on
9//! 2026-07-28, and attaches a [`TraceContext`] to the request extensions.
10//! Handlers read it with
11//! [`RequestContext::trace_context`](crate::context::RequestContext::trace_context),
12//! and [`McpTracingLayer`](crate::middleware::McpTracingLayer) records its
13//! identifiers on the `mcp_request` span.
14//!
15//! ```rust
16//! use tower_mcp::TraceContext;
17//!
18//! let ctx = TraceContext::parse(
19//!     "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
20//!     Some("vendor=value"),
21//! )
22//! .expect("valid traceparent");
23//! assert_eq!(ctx.trace_id(), "4bf92f3577b34da6a3ce929d0e0e4736");
24//! assert_eq!(ctx.parent_id(), "00f067aa0ba902b7");
25//! assert!(ctx.sampled());
26//! assert_eq!(ctx.tracestate(), Some("vendor=value"));
27//! ```
28//!
29//! # Sending a context
30//!
31//! A client sends a context by registering a provider on
32//! [`McpClientBuilder::trace_context_provider`](crate::client::McpClientBuilder::trace_context_provider).
33//! [`TraceContext::parse`] is the only constructor, so a provider can only
34//! return a well-formed context: a value it cannot parse becomes `None`, and
35//! the request goes out without trace keys.
36//! [`TraceContext::entries`] yields the `_meta` entries for a context, and the
37//! same pairs work as an OpenTelemetry propagator carrier.
38//!
39//! This crate reads, validates, and sends the context. It does not link the
40//! `tracing` span to an OpenTelemetry parent; the tracing guide
41//! ([`guides::tracing`](crate::guides::tracing)) shows how to do that with
42//! `tracing-opentelemetry`.
43//!
44//! # Parsing rules
45//!
46//! - `traceparent` is `version-trace-id-parent-id-trace-flags`, all lowercase
47//!   hex: a 2-digit version, a 32-digit trace id, a 16-digit parent id, and a
48//!   2-digit flags field. The trace id and parent id must not be all zeros.
49//! - Version `00` must be exactly 55 characters, with no trailing fields.
50//! - Version `ff` is invalid.
51//! - Any other version is read with the W3C forward-compatibility rules: the
52//!   first four fields must satisfy the same rules as version `00`, and the
53//!   value must either end there or continue with `-` and further fields. The
54//!   further fields are ignored.
55//! - `tracestate` is kept as the raw string. It is dropped, and the
56//!   `traceparent` kept, when it is longer than 512 characters or has more
57//!   than 32 list members. Its member syntax is not validated. An empty
58//!   `tracestate` reads as absent.
59//! - A malformed `traceparent` discards both keys and emits a debug-level
60//!   `tracing` event. The request itself is unaffected.
61//!
62//! [W3C Trace Context]: https://www.w3.org/TR/trace-context/
63
64use serde_json::Value;
65
66/// Length of a version `00` `traceparent`: `2 + 1 + 32 + 1 + 16 + 1 + 2`.
67const TRACEPARENT_LEN: usize = 55;
68/// `_meta` key carrying the `traceparent`.
69const TRACEPARENT_KEY: &str = "traceparent";
70/// `_meta` key carrying the `tracestate`.
71const TRACESTATE_KEY: &str = "tracestate";
72/// Maximum accepted `tracestate` length, in characters.
73const MAX_TRACESTATE_LEN: usize = 512;
74/// Maximum accepted number of `tracestate` list members.
75const MAX_TRACESTATE_MEMBERS: usize = 32;
76
77/// The W3C Trace Context a client sent in a request's `_meta`.
78///
79/// Obtain one from
80/// [`RequestContext::trace_context`](crate::context::RequestContext::trace_context),
81/// or parse a value directly with [`TraceContext::parse`]. See the
82/// [module documentation](self) for the parsing rules.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct TraceContext {
85    version: u8,
86    trace_id: String,
87    parent_id: String,
88    trace_flags: u8,
89    tracestate: Option<String>,
90}
91
92impl TraceContext {
93    /// Parse a `traceparent` value and an optional `tracestate` value.
94    ///
95    /// Returns `None` when `traceparent` is malformed. An invalid
96    /// `tracestate` is dropped and the `traceparent` is kept.
97    pub fn parse(traceparent: &str, tracestate: Option<&str>) -> Option<Self> {
98        let (version, trace_id, parent_id, trace_flags) = parse_traceparent(traceparent)?;
99        Some(Self {
100            version,
101            trace_id: trace_id.to_string(),
102            parent_id: parent_id.to_string(),
103            trace_flags,
104            tracestate: tracestate.and_then(valid_tracestate).map(str::to_string),
105        })
106    }
107
108    /// Read the trace context from a request's `_meta` object.
109    ///
110    /// Returns `None` when `meta` is not an object or has no `traceparent`.
111    /// A `traceparent` that is present but not a well-formed string discards
112    /// both keys and emits a debug-level event. A `tracestate` that is not a
113    /// string reads as absent.
114    pub fn from_meta(meta: &Value) -> Option<Self> {
115        let meta = meta.as_object()?;
116        let traceparent = meta.get(TRACEPARENT_KEY)?;
117        let Some(traceparent) = traceparent.as_str() else {
118            tracing::debug!("ignoring trace context: traceparent is not a string");
119            return None;
120        };
121        let tracestate = meta.get(TRACESTATE_KEY).and_then(Value::as_str);
122        let parsed = Self::parse(traceparent, tracestate);
123        if parsed.is_none() {
124            tracing::debug!(
125                traceparent_len = traceparent.len(),
126                "ignoring trace context: malformed traceparent"
127            );
128        }
129        parsed
130    }
131
132    /// The `traceparent` version, `00` for every value defined today.
133    pub fn version(&self) -> u8 {
134        self.version
135    }
136
137    /// The trace id: 32 lowercase hex digits, never all zeros.
138    pub fn trace_id(&self) -> &str {
139        &self.trace_id
140    }
141
142    /// The caller's span id (`parent-id`): 16 lowercase hex digits, never all
143    /// zeros. The server's own span is a child of this one.
144    pub fn parent_id(&self) -> &str {
145        &self.parent_id
146    }
147
148    /// The raw `trace-flags` byte.
149    pub fn trace_flags(&self) -> u8 {
150        self.trace_flags
151    }
152
153    /// Whether the caller set the `sampled` flag (bit 0 of the flags).
154    pub fn sampled(&self) -> bool {
155        self.trace_flags & 0x01 != 0
156    }
157
158    /// The raw `tracestate` string, if one was sent and it passed the length
159    /// and member-count limits.
160    pub fn tracestate(&self) -> Option<&str> {
161        self.tracestate.as_deref()
162    }
163
164    /// The `traceparent` value for this context, formatted as
165    /// `version-trace-id-parent-id-trace-flags`.
166    ///
167    /// A context parsed from a later `traceparent` version formats with that
168    /// version and its first four fields only; trailing fields are not kept.
169    pub fn traceparent(&self) -> String {
170        format!(
171            "{:02x}-{}-{}-{:02x}",
172            self.version, self.trace_id, self.parent_id, self.trace_flags
173        )
174    }
175
176    /// The `_meta` entries that carry this context: `traceparent`, then
177    /// `tracestate` when there is one.
178    ///
179    /// These are the exact key names the server reads, and the same pairs are
180    /// what an OpenTelemetry `TextMapPropagator` reads from or writes to a
181    /// carrier, so they can be collected into a `HashMap<String, String>` for
182    /// `extract`.
183    ///
184    /// ```rust
185    /// use tower_mcp::TraceContext;
186    ///
187    /// let ctx = TraceContext::parse(
188    ///     "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
189    ///     Some("vendor=value"),
190    /// )
191    /// .unwrap();
192    /// let entries: Vec<_> = ctx.entries().collect();
193    /// assert_eq!(entries[0].0, "traceparent");
194    /// assert_eq!(entries[1], ("tracestate", "vendor=value".to_string()));
195    /// ```
196    pub fn entries(&self) -> impl Iterator<Item = (&'static str, String)> + '_ {
197        std::iter::once((TRACEPARENT_KEY, self.traceparent())).chain(
198            self.tracestate()
199                .map(|state| (TRACESTATE_KEY, state.to_string())),
200        )
201    }
202}
203
204/// Split and validate a `traceparent`, returning the version, trace id, parent
205/// id, and flags. Returns `None` for anything the module rules reject.
206fn parse_traceparent(value: &str) -> Option<(u8, &str, &str, u8)> {
207    // Every byte of a valid value is ASCII, so byte indexing below cannot
208    // split a character once this holds.
209    if !value.is_ascii() || value.len() < TRACEPARENT_LEN {
210        return None;
211    }
212    let bytes = value.as_bytes();
213    if bytes[2] != b'-' || bytes[35] != b'-' || bytes[52] != b'-' {
214        return None;
215    }
216
217    let version = value.get(0..2)?;
218    let trace_id = value.get(3..35)?;
219    let parent_id = value.get(36..52)?;
220    let flags = value.get(53..55)?;
221    if !is_lower_hex(version)
222        || !is_lower_hex(trace_id)
223        || !is_lower_hex(parent_id)
224        || !is_lower_hex(flags)
225    {
226        return None;
227    }
228    if is_all_zero(trace_id) || is_all_zero(parent_id) {
229        return None;
230    }
231
232    let version = u8::from_str_radix(version, 16).ok()?;
233    match version {
234        0xff => return None,
235        // Version 00 has exactly four fields.
236        0x00 if value.len() != TRACEPARENT_LEN => return None,
237        // A later version may append fields, each introduced by a dash.
238        _ if value.len() > TRACEPARENT_LEN && bytes[TRACEPARENT_LEN] != b'-' => return None,
239        _ => {}
240    }
241
242    let flags = u8::from_str_radix(flags, 16).ok()?;
243    Some((version, trace_id, parent_id, flags))
244}
245
246/// Return `value` when it is an acceptable `tracestate`.
247fn valid_tracestate(value: &str) -> Option<&str> {
248    if value.chars().count() > MAX_TRACESTATE_LEN {
249        return None;
250    }
251    let members = value
252        .split(',')
253        .filter(|member| !member.trim().is_empty())
254        .count();
255    if members == 0 || members > MAX_TRACESTATE_MEMBERS {
256        return None;
257    }
258    Some(value)
259}
260
261fn is_lower_hex(value: &str) -> bool {
262    value
263        .bytes()
264        .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
265}
266
267fn is_all_zero(value: &str) -> bool {
268    value.bytes().all(|byte| byte == b'0')
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use serde_json::json;
275
276    const TRACE_ID: &str = "4bf92f3577b34da6a3ce929d0e0e4736";
277    const PARENT_ID: &str = "00f067aa0ba902b7";
278    const VALID: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
279
280    #[test]
281    fn parses_a_valid_traceparent_without_tracestate() {
282        let ctx = TraceContext::parse(VALID, None).unwrap();
283        assert_eq!(ctx.version(), 0);
284        assert_eq!(ctx.trace_id(), TRACE_ID);
285        assert_eq!(ctx.parent_id(), PARENT_ID);
286        assert_eq!(ctx.trace_flags(), 1);
287        assert!(ctx.sampled());
288        assert_eq!(ctx.tracestate(), None);
289    }
290
291    #[test]
292    fn parses_a_valid_traceparent_with_tracestate() {
293        let ctx =
294            TraceContext::parse(VALID, Some("congo=t61rcWkgMzE,rojo=00f067aa0ba902b7")).unwrap();
295        assert_eq!(
296            ctx.tracestate(),
297            Some("congo=t61rcWkgMzE,rojo=00f067aa0ba902b7")
298        );
299    }
300
301    #[test]
302    fn unsampled_flag_is_reported() {
303        let ctx = TraceContext::parse(
304            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00",
305            None,
306        )
307        .unwrap();
308        assert!(!ctx.sampled());
309        assert_eq!(ctx.trace_flags(), 0);
310    }
311
312    #[test]
313    fn sampled_is_bit_zero_only() {
314        let ctx = TraceContext::parse(
315            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-02",
316            None,
317        )
318        .unwrap();
319        assert!(!ctx.sampled());
320        assert_eq!(ctx.trace_flags(), 2);
321    }
322
323    #[test]
324    fn rejects_wrong_length() {
325        for value in [
326            "",
327            "00",
328            &VALID[..54],
329            &format!("{VALID}0"),
330            // Version 00 allows no trailing fields, even dash-separated ones.
331            &format!("{VALID}-extra"),
332            "00-4bf92f3577b34da6a3ce929d0e0e473-00f067aa0ba902b7-01",
333            "00-4bf92f3577b34da6a3ce929d0e0e47366-00f067aa0ba902b7-01",
334            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b-01",
335            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1",
336        ] {
337            assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
338        }
339    }
340
341    #[test]
342    fn rejects_uppercase_hex() {
343        for value in [
344            "00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01",
345            "00-4bf92f3577b34da6a3ce929d0e0e4736-00F067AA0BA902B7-01",
346            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0A",
347            "0A-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
348        ] {
349            assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
350        }
351    }
352
353    #[test]
354    fn rejects_non_hex_and_bad_separators() {
355        for value in [
356            "00-4bf92f3577b34da6a3ce929d0e0e473g-00f067aa0ba902b7-01",
357            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902bz-01",
358            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0x",
359            "zz-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
360            "00_4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
361            "00-4bf92f3577b34da6a3ce929d0e0e4736_00f067aa0ba902b7-01",
362            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7_01",
363            // Multibyte input must be rejected without panicking.
364            "00-4bf92f3577b34da6a3ce929d0e0e473\u{e9}-00f067aa0ba902b7-01",
365        ] {
366            assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
367        }
368    }
369
370    #[test]
371    fn rejects_all_zero_trace_id() {
372        let value = "00-00000000000000000000000000000000-00f067aa0ba902b7-01";
373        assert!(TraceContext::parse(value, None).is_none());
374    }
375
376    #[test]
377    fn rejects_all_zero_parent_id() {
378        let value = "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01";
379        assert!(TraceContext::parse(value, None).is_none());
380    }
381
382    #[test]
383    fn rejects_version_ff() {
384        let value = "ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
385        assert!(TraceContext::parse(value, None).is_none());
386    }
387
388    #[test]
389    fn future_version_reads_the_first_four_fields() {
390        // Exactly four fields.
391        let ctx = TraceContext::parse(
392            "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
393            None,
394        )
395        .unwrap();
396        assert_eq!(ctx.version(), 1);
397        assert_eq!(ctx.trace_id(), TRACE_ID);
398        assert_eq!(ctx.parent_id(), PARENT_ID);
399        assert!(ctx.sampled());
400
401        // Unknown trailing fields are ignored.
402        let ctx = TraceContext::parse(
403            "cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-what-the-future-will-be-like",
404            None,
405        )
406        .unwrap();
407        assert_eq!(ctx.version(), 0xcc);
408        assert_eq!(ctx.trace_id(), TRACE_ID);
409    }
410
411    #[test]
412    fn future_version_still_enforces_the_field_rules() {
413        for value in [
414            // Trailing data must start with a dash.
415            "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01x",
416            "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-010",
417            // Too short to hold four fields.
418            "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7",
419            "01-00000000000000000000000000000000-00f067aa0ba902b7-01-x",
420            "01-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01-x",
421            "01-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01-x",
422        ] {
423            assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
424        }
425    }
426
427    #[test]
428    fn oversized_tracestate_is_dropped_but_traceparent_kept() {
429        let too_long = format!("k={}", "v".repeat(MAX_TRACESTATE_LEN));
430        let ctx = TraceContext::parse(VALID, Some(&too_long)).unwrap();
431        assert_eq!(ctx.trace_id(), TRACE_ID);
432        assert_eq!(ctx.tracestate(), None);
433
434        let at_limit = format!("k={}", "v".repeat(MAX_TRACESTATE_LEN - 2));
435        assert_eq!(at_limit.len(), MAX_TRACESTATE_LEN);
436        let ctx = TraceContext::parse(VALID, Some(&at_limit)).unwrap();
437        assert_eq!(ctx.tracestate(), Some(at_limit.as_str()));
438    }
439
440    #[test]
441    fn tracestate_over_the_member_limit_is_dropped() {
442        let members = |n: usize| {
443            (0..n)
444                .map(|i| format!("k{i}=v"))
445                .collect::<Vec<_>>()
446                .join(",")
447        };
448        let ok = members(MAX_TRACESTATE_MEMBERS);
449        let ctx = TraceContext::parse(VALID, Some(&ok)).unwrap();
450        assert_eq!(ctx.tracestate(), Some(ok.as_str()));
451
452        let too_many = members(MAX_TRACESTATE_MEMBERS + 1);
453        let ctx = TraceContext::parse(VALID, Some(&too_many)).unwrap();
454        assert_eq!(ctx.trace_id(), TRACE_ID);
455        assert_eq!(ctx.tracestate(), None);
456    }
457
458    #[test]
459    fn empty_tracestate_reads_as_absent() {
460        for value in ["", " ", ",", " , "] {
461            let ctx = TraceContext::parse(VALID, Some(value)).unwrap();
462            assert_eq!(ctx.tracestate(), None, "{value:?}");
463        }
464    }
465
466    #[test]
467    fn traceparent_round_trips() {
468        let ctx = TraceContext::parse(VALID, None).unwrap();
469        assert_eq!(ctx.traceparent(), VALID);
470
471        let unsampled = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00";
472        let ctx = TraceContext::parse(unsampled, None).unwrap();
473        assert_eq!(ctx.traceparent(), unsampled);
474
475        // A later version keeps its version and first four fields only.
476        let ctx = TraceContext::parse(
477            "cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0a-future",
478            None,
479        )
480        .unwrap();
481        assert_eq!(
482            ctx.traceparent(),
483            "cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0a"
484        );
485        assert!(TraceContext::parse(&ctx.traceparent(), None).is_some());
486    }
487
488    #[test]
489    fn entries_are_the_meta_keys() {
490        let ctx = TraceContext::parse(VALID, Some("vendor=value")).unwrap();
491        let entries: Vec<_> = ctx.entries().collect();
492        assert_eq!(
493            entries,
494            [
495                ("traceparent", VALID.to_string()),
496                ("tracestate", "vendor=value".to_string()),
497            ]
498        );
499
500        let ctx = TraceContext::parse(VALID, None).unwrap();
501        let entries: Vec<_> = ctx.entries().collect();
502        assert_eq!(entries, [("traceparent", VALID.to_string())]);
503    }
504
505    #[test]
506    fn entries_read_back_through_from_meta() {
507        let ctx = TraceContext::parse(VALID, Some("vendor=value")).unwrap();
508        let meta: serde_json::Map<String, Value> = ctx
509            .entries()
510            .map(|(key, value)| (key.to_string(), Value::String(value)))
511            .collect();
512        assert_eq!(TraceContext::from_meta(&Value::Object(meta)), Some(ctx));
513    }
514
515    #[test]
516    fn from_meta_reads_both_keys() {
517        let meta = json!({
518            "traceparent": VALID,
519            "tracestate": "vendor=value",
520            "progressToken": "p",
521        });
522        let ctx = TraceContext::from_meta(&meta).unwrap();
523        assert_eq!(ctx.trace_id(), TRACE_ID);
524        assert_eq!(ctx.tracestate(), Some("vendor=value"));
525    }
526
527    #[test]
528    fn from_meta_ignores_absent_and_malformed_values() {
529        for meta in [
530            json!(null),
531            json!("traceparent"),
532            json!({}),
533            json!({"tracestate": "vendor=value"}),
534            json!({"traceparent": 7}),
535            json!({"traceparent": null}),
536            json!({"traceparent": "not-a-traceparent", "tracestate": "vendor=value"}),
537        ] {
538            assert!(TraceContext::from_meta(&meta).is_none(), "{meta}");
539        }
540    }
541
542    #[test]
543    fn from_meta_treats_a_non_string_tracestate_as_absent() {
544        let meta = json!({"traceparent": VALID, "tracestate": 5});
545        let ctx = TraceContext::from_meta(&meta).unwrap();
546        assert_eq!(ctx.trace_id(), TRACE_ID);
547        assert_eq!(ctx.tracestate(), None);
548    }
549}