Skip to content

Commit 197248f

Browse files
committed
feat: add SEP-414 trace context meta accessors
1 parent 64d22de commit 197248f

4 files changed

Lines changed: 236 additions & 0 deletions

File tree

crates/rmcp/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,11 @@ name = "test_custom_request"
304304
required-features = ["server", "client"]
305305
path = "tests/test_custom_request.rs"
306306

307+
[[test]]
308+
name = "test_trace_context"
309+
required-features = ["server", "client"]
310+
path = "tests/test_trace_context.rs"
311+
307312
[[test]]
308313
name = "test_prompt_macros"
309314
required-features = ["server", "client"]

crates/rmcp/src/model/meta.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,34 @@ pub trait RequestParamsMeta {
4747
}
4848
}
4949
}
50+
/// Get the W3C `traceparent` value from meta, if present (SEP-414)
51+
fn traceparent(&self) -> Option<&str> {
52+
self.meta().and_then(|m| m.get_traceparent())
53+
}
54+
/// Set the W3C `traceparent` value in meta (SEP-414)
55+
fn set_traceparent(&mut self, value: &str) {
56+
self.meta_or_default().set_traceparent(value);
57+
}
58+
/// Get the W3C `tracestate` value from meta, if present (SEP-414)
59+
fn tracestate(&self) -> Option<&str> {
60+
self.meta().and_then(|m| m.get_tracestate())
61+
}
62+
/// Set the W3C `tracestate` value in meta (SEP-414)
63+
fn set_tracestate(&mut self, value: &str) {
64+
self.meta_or_default().set_tracestate(value);
65+
}
66+
/// Get the W3C `baggage` value from meta, if present (SEP-414)
67+
fn baggage(&self) -> Option<&str> {
68+
self.meta().and_then(|m| m.get_baggage())
69+
}
70+
/// Set the W3C `baggage` value in meta (SEP-414)
71+
fn set_baggage(&mut self, value: &str) {
72+
self.meta_or_default().set_baggage(value);
73+
}
74+
/// Get a mutable reference to meta, inserting an empty one if absent.
75+
fn meta_or_default(&mut self) -> &mut Meta {
76+
self.meta_mut().get_or_insert_with(Meta::new)
77+
}
5078
}
5179

5280
/// Trait for task-augmented request params that contain both `_meta` and `task` fields.
@@ -207,6 +235,12 @@ impl Meta {
207235
const META_KEY_CLIENT_INFO: &str = "io.modelcontextprotocol/clientInfo";
208236
const META_KEY_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities";
209237
const META_KEY_LOG_LEVEL: &str = "io.modelcontextprotocol/logLevel";
238+
/// Reserved `_meta` key for the W3C Trace Context `traceparent` value (SEP-414).
239+
const TRACEPARENT_FIELD: &str = "traceparent";
240+
/// Reserved `_meta` key for the W3C Trace Context `tracestate` value (SEP-414).
241+
const TRACESTATE_FIELD: &str = "tracestate";
242+
/// Reserved `_meta` key for the W3C Baggage value (SEP-414).
243+
const BAGGAGE_FIELD: &str = "baggage";
210244

211245
pub fn new() -> Self {
212246
Self(JsonObject::new())
@@ -304,6 +338,58 @@ impl Meta {
304338
self.insert_serialized(Self::META_KEY_LOG_LEVEL, log_level);
305339
}
306340

341+
/// Read a string-valued `_meta` field, or `None` if absent or not a string.
342+
fn get_str(&self, field: &str) -> Option<&str> {
343+
self.0.get(field).and_then(Value::as_str)
344+
}
345+
346+
/// Write a string-valued `_meta` field.
347+
fn set_str(&mut self, field: &str, value: impl Into<String>) {
348+
self.0
349+
.insert(field.to_string(), Value::String(value.into()));
350+
}
351+
352+
/// Get the W3C `traceparent` value (SEP-414), if present.
353+
pub fn get_traceparent(&self) -> Option<&str> {
354+
self.get_str(Self::TRACEPARENT_FIELD)
355+
}
356+
357+
/// Set the W3C `traceparent` value (SEP-414).
358+
///
359+
/// ```
360+
/// use rmcp::model::Meta;
361+
///
362+
/// let mut meta = Meta::new();
363+
/// meta.set_traceparent("00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01");
364+
/// assert_eq!(
365+
/// meta.get_traceparent(),
366+
/// Some("00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"),
367+
/// );
368+
/// ```
369+
pub fn set_traceparent(&mut self, value: impl Into<String>) {
370+
self.set_str(Self::TRACEPARENT_FIELD, value);
371+
}
372+
373+
/// Get the W3C `tracestate` value (SEP-414), if present.
374+
pub fn get_tracestate(&self) -> Option<&str> {
375+
self.get_str(Self::TRACESTATE_FIELD)
376+
}
377+
378+
/// Set the W3C `tracestate` value (SEP-414).
379+
pub fn set_tracestate(&mut self, value: impl Into<String>) {
380+
self.set_str(Self::TRACESTATE_FIELD, value);
381+
}
382+
383+
/// Get the W3C `baggage` value (SEP-414), if present.
384+
pub fn get_baggage(&self) -> Option<&str> {
385+
self.get_str(Self::BAGGAGE_FIELD)
386+
}
387+
388+
/// Set the W3C `baggage` value (SEP-414).
389+
pub fn set_baggage(&mut self, value: impl Into<String>) {
390+
self.set_str(Self::BAGGAGE_FIELD, value);
391+
}
392+
307393
pub fn extend(&mut self, other: Meta) {
308394
for (k, v) in other.0.into_iter() {
309395
self.0.insert(k, v);
@@ -361,3 +447,59 @@ where
361447
}
362448
}
363449
}
450+
451+
#[cfg(test)]
452+
mod tests {
453+
use super::*;
454+
455+
#[derive(Default)]
456+
struct Params {
457+
meta: Option<Meta>,
458+
}
459+
460+
impl RequestParamsMeta for Params {
461+
fn meta(&self) -> Option<&Meta> {
462+
self.meta.as_ref()
463+
}
464+
fn meta_mut(&mut self) -> &mut Option<Meta> {
465+
&mut self.meta
466+
}
467+
}
468+
469+
const TRACEPARENT: &str = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01";
470+
471+
#[test]
472+
fn trace_context_round_trip() {
473+
let mut meta = Meta::new();
474+
meta.set_traceparent(TRACEPARENT);
475+
meta.set_tracestate("vendor1=value1,vendor2=value2");
476+
meta.set_baggage("userId=alice,region=us-east-1");
477+
assert_eq!(meta.get_traceparent(), Some(TRACEPARENT));
478+
assert_eq!(meta.get_tracestate(), Some("vendor1=value1,vendor2=value2"));
479+
assert_eq!(meta.get_baggage(), Some("userId=alice,region=us-east-1"));
480+
}
481+
482+
#[test]
483+
fn absent_field_is_none() {
484+
let meta = Meta::new();
485+
assert_eq!(meta.get_traceparent(), None);
486+
assert_eq!(meta.get_tracestate(), None);
487+
assert_eq!(meta.get_baggage(), None);
488+
}
489+
490+
#[test]
491+
fn non_string_value_is_none() {
492+
let mut meta = Meta::new();
493+
meta.0
494+
.insert(Meta::TRACEPARENT_FIELD.to_string(), Value::from(42));
495+
assert_eq!(meta.get_traceparent(), None);
496+
}
497+
498+
#[test]
499+
fn trait_setter_inserts_meta_when_absent() {
500+
let mut params = Params::default();
501+
assert_eq!(params.traceparent(), None);
502+
params.set_traceparent(TRACEPARENT);
503+
assert_eq!(params.traceparent(), Some(TRACEPARENT));
504+
}
505+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#![cfg(not(feature = "local"))]
2+
//! SEP-414: the reserved trace-context `_meta` keys survive a client→server round trip unchanged.
3+
use std::sync::Arc;
4+
5+
use rmcp::{
6+
RoleServer, ServerHandler, ServiceExt,
7+
model::{ClientRequest, CustomRequest, CustomResult, Meta},
8+
service::{PeerRequestOptions, RequestContext},
9+
};
10+
use serde_json::json;
11+
use tokio::sync::{Mutex, Notify};
12+
13+
const TRACEPARENT: &str = "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01";
14+
const TRACESTATE: &str = "vendor1=value1,vendor2=value2";
15+
const BAGGAGE: &str = "userId=alice,region=us-east-1";
16+
17+
/// Records the `_meta` it receives on the incoming request so the test can assert passthrough.
18+
struct TraceCapturingServer {
19+
receive_signal: Arc<Notify>,
20+
seen: Arc<Mutex<Option<Meta>>>,
21+
}
22+
23+
impl ServerHandler for TraceCapturingServer {
24+
async fn on_custom_request(
25+
&self,
26+
_request: CustomRequest,
27+
context: RequestContext<RoleServer>,
28+
) -> Result<CustomResult, rmcp::ErrorData> {
29+
*self.seen.lock().await = Some(context.meta);
30+
self.receive_signal.notify_one();
31+
Ok(CustomResult::new(json!({ "status": "ok" })))
32+
}
33+
}
34+
35+
#[tokio::test]
36+
async fn trace_context_meta_survives_round_trip() -> anyhow::Result<()> {
37+
let (server_transport, client_transport) = tokio::io::duplex(4096);
38+
let receive_signal = Arc::new(Notify::new());
39+
let seen = Arc::new(Mutex::new(None));
40+
41+
{
42+
let receive_signal = receive_signal.clone();
43+
let seen = seen.clone();
44+
tokio::spawn(async move {
45+
let server = TraceCapturingServer {
46+
receive_signal,
47+
seen,
48+
}
49+
.serve(server_transport)
50+
.await?;
51+
server.waiting().await?;
52+
anyhow::Ok(())
53+
});
54+
}
55+
56+
let client = ().serve(client_transport).await?;
57+
58+
// Client attaches trace context to the outgoing request's `_meta`.
59+
let mut meta = Meta::new();
60+
meta.set_traceparent(TRACEPARENT);
61+
meta.set_tracestate(TRACESTATE);
62+
meta.set_baggage(BAGGAGE);
63+
64+
let mut options = PeerRequestOptions::no_options();
65+
options.meta = Some(meta);
66+
client
67+
.send_cancellable_request(
68+
ClientRequest::CustomRequest(CustomRequest::new("requests/trace-test", None)),
69+
options,
70+
)
71+
.await?
72+
.await_response()
73+
.await?;
74+
75+
tokio::time::timeout(std::time::Duration::from_secs(5), receive_signal.notified()).await?;
76+
77+
// Server saw the reserved keys unchanged (alongside the injected progressToken).
78+
let seen = seen.lock().await.take().expect("server observed meta");
79+
assert_eq!(seen.get_traceparent(), Some(TRACEPARENT));
80+
assert_eq!(seen.get_tracestate(), Some(TRACESTATE));
81+
assert_eq!(seen.get_baggage(), Some(BAGGAGE));
82+
83+
client.cancel().await?;
84+
Ok(())
85+
}

typos.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# W3C `traceparent` example values (SEP-414) embed hex spans like `0ba9` that the
2+
# spell checker misreads as typos; ignore the canonical traceparent format.
3+
[default]
4+
extend-ignore-re = ["00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}"]

0 commit comments

Comments
 (0)