strands-agents 0.1.0

A Rust implementation of the Strands AI Agents SDK
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
//! OpenTelemetry instrumentation for Model Context Protocol (MCP) tracing.
//!
//! Enables distributed tracing across MCP client-server boundaries by injecting
//! OpenTelemetry context into MCP request metadata (_meta field) and extracting
//! it on the server side, creating unified traces that span from agent calls
//! through MCP tool executions.
//!
//! Based on: https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-mcp
//! Related issue: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/246

use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};

use opentelemetry::Context;
use opentelemetry::propagation::{TextMapPropagator, Extractor, Injector};
use opentelemetry_sdk::propagation::TraceContextPropagator;
use serde_json::{json, Value};
use tracing::{debug, instrument, Span};

/// Module-level flag to ensure instrumentation is applied only once.
static INSTRUMENTATION_APPLIED: AtomicBool = AtomicBool::new(false);

/// Wrapper for items that need to carry tracing context.
///
/// Used to preserve tracing context across async boundaries in MCP sessions,
/// ensuring that distributed traces remain connected even when messages are
/// processed asynchronously.
#[derive(Debug, Clone)]
pub struct ItemWithContext<T> {
    /// The original item being wrapped.
    pub item: T,
    /// The context data associated with the item.
    pub context: HashMap<String, String>,
}

impl<T> ItemWithContext<T> {
    /// Creates a new item with the current tracing context.
    pub fn new(item: T) -> Self {
        Self {
            item,
            context: get_current_trace_context(),
        }
    }

    /// Creates a new item with explicit context.
    pub fn with_context(item: T, context: HashMap<String, String>) -> Self {
        Self { item, context }
    }

    /// Unwraps the item, discarding the context.
    pub fn into_inner(self) -> T {
        self.item
    }
}

/// Initializes MCP OpenTelemetry instrumentation.
///
/// This function sets up instrumentation for MCP client-server communication:
/// 1. Client-side: Injects tracing context into tool call requests
/// 2. Transport-level: Extracts context from incoming messages
/// 3. Session-level: Manages bidirectional context flow
///
/// The patches enable distributed tracing by:
/// - Adding OpenTelemetry context to the _meta field of MCP requests
/// - Extracting and activating context on the server side
/// - Preserving context across async message processing boundaries
///
/// This function is idempotent - multiple calls will not accumulate wrappers.
pub fn init_mcp_instrumentation() {
    if INSTRUMENTATION_APPLIED.swap(true, Ordering::SeqCst) {
        debug!("MCP instrumentation already applied");
        return;
    }

    debug!("Initializing MCP OpenTelemetry instrumentation");



}

/// Checks if MCP instrumentation has been initialized.
pub fn is_instrumentation_applied() -> bool {
    INSTRUMENTATION_APPLIED.load(Ordering::SeqCst)
}

/// Resets the instrumentation flag (mainly for testing).
#[cfg(test)]
pub fn reset_instrumentation() {
    INSTRUMENTATION_APPLIED.store(false, Ordering::SeqCst);
}

/// Global propagator instance for W3C TraceContext propagation.
static PROPAGATOR: std::sync::OnceLock<TraceContextPropagator> = std::sync::OnceLock::new();

fn get_propagator() -> &'static TraceContextPropagator {
    PROPAGATOR.get_or_init(|| TraceContextPropagator::new())
}

/// Gets the current trace context as a map of headers.
///
/// This extracts the current OpenTelemetry context's trace ID, span ID, and trace flags
/// into a format suitable for propagation via the _meta field using W3C TraceContext format.
pub fn get_current_trace_context() -> HashMap<String, String> {
    let mut context = HashMap::new();
    

    let otel_context = Context::current();
    

    let mut injector = HashMapInjector(&mut context);
    get_propagator().inject_context(&otel_context, &mut injector);
    
    context
}

/// Helper struct to inject context into a HashMap.
struct HashMapInjector<'a>(&'a mut HashMap<String, String>);

impl<'a> Injector for HashMapInjector<'a> {
    fn set(&mut self, key: &str, value: String) {
        self.0.insert(key.to_string(), value);
    }
}

/// Helper struct to extract context from a HashMap.
struct HashMapExtractor<'a>(&'a HashMap<String, String>);

impl<'a> Extractor for HashMapExtractor<'a> {
    fn get(&self, key: &str) -> Option<&str> {
        self.0.get(key).map(|s| s.as_str())
    }

    fn keys(&self) -> Vec<&str> {
        self.0.keys().map(|s| s.as_str()).collect()
    }
}

/// Injects trace context into an MCP request's _meta field.
///
/// This should be called before sending MCP tool call requests to
/// propagate the current trace context to the server.
///
/// # Arguments
///
/// * `request` - The MCP request parameters as a mutable JSON value
///
/// # Example
///
/// ```ignore
/// let mut params = json!({
///     "name": "my_tool",
///     "arguments": { "input": "value" }
/// });
/// inject_trace_context(&mut params);
/// // params now contains _meta with tracing headers
/// ```
#[instrument(skip(request), level = "trace")]
pub fn inject_trace_context(request: &mut Value) {
    let context = get_current_trace_context();

    if context.is_empty() {
        return;
    }


    let obj = match request.as_object_mut() {
        Some(o) => o,
        None => return,
    };


    let meta = obj
        .entry("_meta")
        .or_insert_with(|| json!({}));


    if let Some(meta_obj) = meta.as_object_mut() {
        for (key, value) in context {
            meta_obj.insert(key, Value::String(value));
        }
    }

    debug!("Injected trace context into MCP request");
}

/// Extracts trace context from an MCP request's _meta field.
///
/// This should be called on the server side when receiving MCP tool
/// call requests to restore the trace context from the client.
///
/// # Arguments
///
/// * `request` - The MCP request parameters as a JSON value
///
/// # Returns
///
/// A map of trace context headers extracted from the request.
#[instrument(skip(request), level = "trace")]
pub fn extract_trace_context(request: &Value) -> HashMap<String, String> {
    let mut context = HashMap::new();

    let meta = match request.get("_meta").and_then(|m| m.as_object()) {
        Some(m) => m,
        None => return context,
    };


    for key in &["traceparent", "tracestate", "baggage"] {
        if let Some(value) = meta.get(*key).and_then(|v| v.as_str()) {
            context.insert((*key).to_string(), value.to_string());
        }
    }

    if !context.is_empty() {
        debug!(keys = ?context.keys().collect::<Vec<_>>(), "Extracted trace context from MCP request");
    }

    context
}

/// Extracts and activates OpenTelemetry context from an MCP request.
///
/// This should be called on the server side to restore the trace context
/// and make it the current context for subsequent operations.
///
/// # Arguments
///
/// * `request` - The MCP request parameters as a JSON value
///
/// # Returns
///
/// A guard that will restore the previous context when dropped.
pub fn extract_and_activate_context(request: &Value) -> ContextGuard {
    let context_map = extract_trace_context(request);
    
    if context_map.is_empty() {
        return ContextGuard::new(Context::current());
    }
    
    let extractor = HashMapExtractor(&context_map);
    let extracted_context = get_propagator().extract(&extractor);
    

    let previous_context = Context::current();
    

    let _guard = extracted_context.clone().attach();
    
    ContextGuard::new(previous_context)
}

/// Guard that restores the previous OpenTelemetry context when dropped.
pub struct ContextGuard {
    _active_guard: opentelemetry::ContextGuard,
}

impl ContextGuard {
    fn new(context: Context) -> Self {

        let _active_guard = context.attach();
        Self { _active_guard }
    }
}

/// Creates a tracing span for an MCP tool call with extracted context.
///
/// This helper creates a properly configured span for server-side
/// tool execution, linking it to the client's trace if context is available.
///
/// # Arguments
///
/// * `tool_name` - The name of the tool being called
/// * `request` - The MCP request parameters (for context extraction)
///
/// # Returns
///
/// A configured tracing Span and a context guard that should be kept alive
/// for the duration of the span.
#[must_use]
pub fn create_mcp_tool_span(tool_name: &str, request: &Value) -> (Span, ContextGuard) {

    let context_guard = extract_and_activate_context(request);


    let span = tracing::info_span!(
        "mcp.tool.call",
        tool.name = %tool_name,
        otel.kind = "server",
        mcp.instrumented = true
    );

    (span, context_guard)
}

/// Trait for MCP message context injection.
///
/// Implement this trait for your MCP request types to enable
/// automatic trace context injection.
pub trait InjectableContext {
    /// Injects trace context into this message.
    fn inject_context(&mut self);

    /// Checks if context has already been injected.
    fn has_context(&self) -> bool;
}

impl InjectableContext for Value {
    fn inject_context(&mut self) {
        inject_trace_context(self);
    }

    fn has_context(&self) -> bool {
        self.get("_meta")
            .and_then(|m| m.get("traceparent"))
            .is_some()
    }
}

/// Trait for MCP message context extraction.
///
/// Implement this trait for your MCP request types to enable
/// automatic trace context extraction.
pub trait ExtractableContext {
    /// Extracts trace context from this message.
    fn extract_context(&self) -> HashMap<String, String>;
}

impl ExtractableContext for Value {
    fn extract_context(&self) -> HashMap<String, String> {
        extract_trace_context(self)
    }
}

/// Configuration for MCP instrumentation.
#[derive(Debug, Clone, Default)]
pub struct MCPInstrumentationConfig {
    /// Whether to enable client-side context injection.
    pub inject_client_context: bool,
    /// Whether to enable server-side context extraction.
    pub extract_server_context: bool,
    /// Additional headers to propagate.
    pub additional_headers: Vec<String>,
}

impl MCPInstrumentationConfig {
    /// Creates a new configuration with all features enabled.
    pub fn enabled() -> Self {
        Self {
            inject_client_context: true,
            extract_server_context: true,
            additional_headers: Vec::new(),
        }
    }

    /// Creates a client-only configuration.
    pub fn client_only() -> Self {
        Self {
            inject_client_context: true,
            extract_server_context: false,
            additional_headers: Vec::new(),
        }
    }

    /// Creates a server-only configuration.
    pub fn server_only() -> Self {
        Self {
            inject_client_context: false,
            extract_server_context: true,
            additional_headers: Vec::new(),
        }
    }

    /// Adds additional headers to propagate.
    pub fn with_headers(mut self, headers: Vec<String>) -> Self {
        self.additional_headers = headers;
        self
    }
}

/// Guards that apply instrumentation within a scope.
pub struct InstrumentationGuard {
    _config: MCPInstrumentationConfig,
}

impl InstrumentationGuard {
    /// Creates a new instrumentation guard.
    pub fn new(config: MCPInstrumentationConfig) -> Self {
        init_mcp_instrumentation();
        Self { _config: config }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_inject_trace_context() {
        let mut request = json!({
            "name": "test_tool",
            "arguments": {}
        });

        inject_trace_context(&mut request);




    }

    #[test]
    fn test_extract_trace_context_empty() {
        let request = json!({
            "name": "test_tool",
            "arguments": {}
        });

        let context = extract_trace_context(&request);
        assert!(context.is_empty());
    }

    #[test]
    fn test_extract_trace_context_with_meta() {
        let request = json!({
            "name": "test_tool",
            "_meta": {
                "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
            }
        });

        let context = extract_trace_context(&request);
        assert_eq!(
            context.get("traceparent"),
            Some(&"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string())
        );
    }

    #[test]
    fn test_injectable_context_trait() {
        let mut request = json!({
            "name": "test_tool"
        });


        assert!(!request.has_context());
        request.inject_context();

        assert!(!request.has_context());
    }

    #[test]
    fn test_instrumentation_idempotent() {
        reset_instrumentation();
        assert!(!is_instrumentation_applied());

        init_mcp_instrumentation();
        assert!(is_instrumentation_applied());

        init_mcp_instrumentation();
        assert!(is_instrumentation_applied());
    }

    #[test]
    fn test_item_with_context() {
        let item = "test_message";
        let wrapped = ItemWithContext::new(item);

        assert_eq!(wrapped.item, "test_message");
        assert_eq!(wrapped.into_inner(), "test_message");
    }

    #[test]
    fn test_config_builders() {
        let enabled = MCPInstrumentationConfig::enabled();
        assert!(enabled.inject_client_context);
        assert!(enabled.extract_server_context);

        let client_only = MCPInstrumentationConfig::client_only();
        assert!(client_only.inject_client_context);
        assert!(!client_only.extract_server_context);

        let server_only = MCPInstrumentationConfig::server_only();
        assert!(!server_only.inject_client_context);
        assert!(server_only.extract_server_context);
    }
}