Skip to main content

turbomcp_core/
context.rs

1//! Minimal request context for cross-platform MCP handlers.
2//!
3//! This module provides a `RequestContext` type that works on all platforms,
4//! including `no_std` environments. Platform-specific extensions (cancellation
5//! tokens, UUIDs, etc.) are provided by runtime crates (`turbomcp-server`, `turbomcp-wasm`).
6//!
7//! # Design Philosophy
8//!
9//! The context is intentionally minimal:
10//! - Uses `BTreeMap` instead of `HashMap` for `no_std` compatibility
11//! - No tokio-specific types (CancellationToken, etc.)
12//! - Serializable for transport across boundaries
13//! - Cloneable for async handler patterns
14//!
15//! # Example
16//!
17//! ```rust
18//! use turbomcp_core::context::{RequestContext, TransportType};
19//!
20//! let ctx = RequestContext::new("request-1", TransportType::Http)
21//!     .with_metadata("user-agent", "Mozilla/5.0")
22//!     .with_metadata("x-request-id", "abc123");
23//!
24//! assert_eq!(ctx.transport, TransportType::Http);
25//! assert_eq!(ctx.get_metadata("user-agent"), Some("Mozilla/5.0"));
26//! ```
27
28use crate::auth::Principal;
29use alloc::collections::BTreeMap;
30use alloc::string::String;
31use serde::{Deserialize, Serialize};
32
33/// Transport type identifier.
34///
35/// Indicates which transport received the request. This is useful for:
36/// - Logging and metrics
37/// - Transport-specific behavior (e.g., different timeouts)
38/// - Debugging and tracing
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
40#[serde(rename_all = "lowercase")]
41#[non_exhaustive]
42pub enum TransportType {
43    /// Standard I/O transport (default for CLI tools)
44    #[default]
45    Stdio,
46    /// HTTP transport (REST or SSE)
47    Http,
48    /// WebSocket transport
49    WebSocket,
50    /// Raw TCP transport
51    Tcp,
52    /// Unix domain socket transport
53    Unix,
54    /// WebAssembly/Worker transport (Cloudflare Workers, etc.)
55    Wasm,
56    /// In-process channel transport (zero-copy, no serialization overhead)
57    Channel,
58    /// Unknown or custom transport
59    Unknown,
60}
61
62impl TransportType {
63    /// Returns true if this is a network-based transport.
64    #[inline]
65    pub fn is_network(&self) -> bool {
66        matches!(self, Self::Http | Self::WebSocket | Self::Tcp)
67    }
68
69    /// Returns true if this is a local transport.
70    #[inline]
71    pub fn is_local(&self) -> bool {
72        matches!(self, Self::Stdio | Self::Unix | Self::Channel)
73    }
74
75    /// Returns the transport name as a string.
76    pub fn as_str(&self) -> &'static str {
77        match self {
78            Self::Stdio => "stdio",
79            Self::Http => "http",
80            Self::WebSocket => "websocket",
81            Self::Tcp => "tcp",
82            Self::Unix => "unix",
83            Self::Wasm => "wasm",
84            Self::Channel => "channel",
85            Self::Unknown => "unknown",
86        }
87    }
88}
89
90impl core::fmt::Display for TransportType {
91    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
92        write!(f, "{}", self.as_str())
93    }
94}
95
96/// Minimal request context that works on all platforms.
97///
98/// This struct contains only the essential information needed to process
99/// a request. Platform-specific extensions (cancellation tokens, UUIDs, etc.)
100/// are provided by the runtime layer.
101///
102/// # Thread Safety
103///
104/// `RequestContext` is `Send + Sync` on native targets, enabling safe use
105/// across async task boundaries. On WASM targets, thread safety is not required.
106///
107/// # Serialization
108///
109/// The context is designed to be serializable, enabling transport across
110/// process boundaries (e.g., for distributed tracing).
111#[derive(Debug, Clone, Default)]
112pub struct RequestContext {
113    /// Unique request identifier (JSON-RPC id as string, or generated UUID)
114    pub request_id: String,
115    /// Transport type that received this request
116    pub transport: TransportType,
117    /// Optional metadata (headers, user info, etc.)
118    ///
119    /// Uses `BTreeMap` for `no_std` compatibility and deterministic iteration.
120    pub metadata: BTreeMap<String, String>,
121    /// Authenticated principal (set after successful authentication)
122    ///
123    /// This field is `None` for unauthenticated requests or when
124    /// authentication is not configured.
125    pub principal: Option<Principal>,
126}
127
128impl RequestContext {
129    /// Create a new request context with the given ID and transport.
130    ///
131    /// # Example
132    ///
133    /// ```rust
134    /// use turbomcp_core::context::{RequestContext, TransportType};
135    ///
136    /// let ctx = RequestContext::new("req-123", TransportType::Http);
137    /// assert_eq!(ctx.request_id, "req-123");
138    /// ```
139    pub fn new(request_id: impl Into<String>, transport: TransportType) -> Self {
140        Self {
141            request_id: request_id.into(),
142            transport,
143            metadata: BTreeMap::new(),
144            principal: None,
145        }
146    }
147
148    /// Create a context for STDIO transport.
149    #[inline]
150    pub fn stdio() -> Self {
151        Self::new("", TransportType::Stdio)
152    }
153
154    /// Create a context for HTTP transport.
155    #[inline]
156    pub fn http() -> Self {
157        Self::new("", TransportType::Http)
158    }
159
160    /// Create a context for WebSocket transport.
161    #[inline]
162    pub fn websocket() -> Self {
163        Self::new("", TransportType::WebSocket)
164    }
165
166    /// Create a context for TCP transport.
167    #[inline]
168    pub fn tcp() -> Self {
169        Self::new("", TransportType::Tcp)
170    }
171
172    /// Create a context for WASM transport.
173    #[inline]
174    pub fn wasm() -> Self {
175        Self::new("", TransportType::Wasm)
176    }
177
178    /// Add metadata to the context.
179    ///
180    /// # Example
181    ///
182    /// ```rust
183    /// use turbomcp_core::context::{RequestContext, TransportType};
184    ///
185    /// let ctx = RequestContext::new("1", TransportType::Http)
186    ///     .with_metadata("user-agent", "MyClient/1.0")
187    ///     .with_metadata("x-trace-id", "abc123");
188    ///
189    /// assert_eq!(ctx.get_metadata("user-agent"), Some("MyClient/1.0"));
190    /// ```
191    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
192        self.metadata.insert(key.into(), value.into());
193        self
194    }
195
196    /// Add metadata to the context (mutable version).
197    pub fn insert_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
198        self.metadata.insert(key.into(), value.into());
199    }
200
201    /// Get metadata value by key.
202    ///
203    /// # Example
204    ///
205    /// ```rust
206    /// use turbomcp_core::context::{RequestContext, TransportType};
207    ///
208    /// let ctx = RequestContext::new("1", TransportType::Http)
209    ///     .with_metadata("key", "value");
210    ///
211    /// assert_eq!(ctx.get_metadata("key"), Some("value"));
212    /// assert_eq!(ctx.get_metadata("missing"), None);
213    /// ```
214    pub fn get_metadata(&self, key: &str) -> Option<&str> {
215        self.metadata.get(key).map(|s| s.as_str())
216    }
217
218    /// Check if metadata contains a key.
219    pub fn has_metadata(&self, key: &str) -> bool {
220        self.metadata.contains_key(key)
221    }
222
223    /// Set the request ID.
224    pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
225        self.request_id = id.into();
226        self
227    }
228
229    /// Returns true if this context has a valid (non-empty) request ID.
230    pub fn has_request_id(&self) -> bool {
231        !self.request_id.is_empty()
232    }
233
234    /// Set the authenticated principal.
235    ///
236    /// # Example
237    ///
238    /// ```rust
239    /// use turbomcp_core::context::{RequestContext, TransportType};
240    /// use turbomcp_core::auth::Principal;
241    ///
242    /// let ctx = RequestContext::new("1", TransportType::Http)
243    ///     .with_principal(Principal::new("user-123"));
244    ///
245    /// assert!(ctx.principal().is_some());
246    /// assert_eq!(ctx.principal().unwrap().subject, "user-123");
247    /// ```
248    pub fn with_principal(mut self, principal: Principal) -> Self {
249        self.principal = Some(principal);
250        self
251    }
252
253    /// Set the authenticated principal (mutable version).
254    pub fn set_principal(&mut self, principal: Principal) {
255        self.principal = Some(principal);
256    }
257
258    /// Get the authenticated principal, if any.
259    ///
260    /// Returns `None` if the request was not authenticated or if
261    /// authentication is not configured.
262    pub fn principal(&self) -> Option<&Principal> {
263        self.principal.as_ref()
264    }
265
266    /// Returns true if this context has an authenticated principal.
267    pub fn is_authenticated(&self) -> bool {
268        self.principal.is_some()
269    }
270
271    /// Get the subject of the authenticated principal.
272    ///
273    /// Convenience method that returns `None` if not authenticated.
274    pub fn subject(&self) -> Option<&str> {
275        self.principal.as_ref().map(|p| p.subject.as_str())
276    }
277
278    /// Clear the authenticated principal.
279    pub fn clear_principal(&mut self) {
280        self.principal = None;
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_transport_type_display() {
290        assert_eq!(TransportType::Stdio.to_string(), "stdio");
291        assert_eq!(TransportType::Http.to_string(), "http");
292        assert_eq!(TransportType::WebSocket.to_string(), "websocket");
293        assert_eq!(TransportType::Tcp.to_string(), "tcp");
294        assert_eq!(TransportType::Unix.to_string(), "unix");
295        assert_eq!(TransportType::Wasm.to_string(), "wasm");
296        assert_eq!(TransportType::Channel.to_string(), "channel");
297        assert_eq!(TransportType::Unknown.to_string(), "unknown");
298    }
299
300    #[test]
301    fn test_transport_type_classification() {
302        assert!(TransportType::Http.is_network());
303        assert!(TransportType::WebSocket.is_network());
304        assert!(TransportType::Tcp.is_network());
305        assert!(!TransportType::Stdio.is_network());
306
307        assert!(TransportType::Stdio.is_local());
308        assert!(TransportType::Unix.is_local());
309        assert!(TransportType::Channel.is_local());
310        assert!(!TransportType::Http.is_local());
311    }
312
313    #[test]
314    fn test_request_context_new() {
315        let ctx = RequestContext::new("test-123", TransportType::Http);
316        assert_eq!(ctx.request_id, "test-123");
317        assert_eq!(ctx.transport, TransportType::Http);
318        assert!(ctx.metadata.is_empty());
319    }
320
321    #[test]
322    fn test_request_context_factory_methods() {
323        assert_eq!(RequestContext::stdio().transport, TransportType::Stdio);
324        assert_eq!(RequestContext::http().transport, TransportType::Http);
325        assert_eq!(
326            RequestContext::websocket().transport,
327            TransportType::WebSocket
328        );
329        assert_eq!(RequestContext::tcp().transport, TransportType::Tcp);
330        assert_eq!(RequestContext::wasm().transport, TransportType::Wasm);
331    }
332
333    #[test]
334    fn test_request_context_metadata() {
335        let ctx = RequestContext::new("1", TransportType::Http)
336            .with_metadata("key1", "value1")
337            .with_metadata("key2", "value2");
338
339        assert_eq!(ctx.get_metadata("key1"), Some("value1"));
340        assert_eq!(ctx.get_metadata("key2"), Some("value2"));
341        assert_eq!(ctx.get_metadata("key3"), None);
342
343        assert!(ctx.has_metadata("key1"));
344        assert!(!ctx.has_metadata("key3"));
345    }
346
347    #[test]
348    fn test_request_context_mutable_metadata() {
349        let mut ctx = RequestContext::new("1", TransportType::Http);
350        ctx.insert_metadata("key", "value");
351        assert_eq!(ctx.get_metadata("key"), Some("value"));
352    }
353
354    #[test]
355    fn test_request_context_request_id() {
356        let ctx = RequestContext::new("", TransportType::Http);
357        assert!(!ctx.has_request_id());
358
359        let ctx = ctx.with_request_id("request-456");
360        assert!(ctx.has_request_id());
361        assert_eq!(ctx.request_id, "request-456");
362    }
363
364    #[test]
365    fn test_request_context_default() {
366        let ctx = RequestContext::default();
367        assert_eq!(ctx.request_id, "");
368        assert_eq!(ctx.transport, TransportType::Stdio);
369        assert!(ctx.metadata.is_empty());
370    }
371
372    #[test]
373    fn test_request_context_clone() {
374        let ctx1 = RequestContext::new("1", TransportType::Http).with_metadata("key", "value");
375        let ctx2 = ctx1.clone();
376
377        assert_eq!(ctx1.request_id, ctx2.request_id);
378        assert_eq!(ctx1.transport, ctx2.transport);
379        assert_eq!(ctx1.get_metadata("key"), ctx2.get_metadata("key"));
380    }
381
382    #[test]
383    fn test_request_context_principal() {
384        let ctx = RequestContext::new("1", TransportType::Http);
385        assert!(!ctx.is_authenticated());
386        assert!(ctx.principal().is_none());
387        assert!(ctx.subject().is_none());
388
389        let principal = Principal::new("user-123")
390            .with_email("user@example.com")
391            .with_role("admin");
392
393        let ctx = ctx.with_principal(principal);
394        assert!(ctx.is_authenticated());
395        assert!(ctx.principal().is_some());
396        assert_eq!(ctx.subject(), Some("user-123"));
397        assert_eq!(
398            ctx.principal().unwrap().email,
399            Some("user@example.com".to_string())
400        );
401        assert!(ctx.principal().unwrap().has_role("admin"));
402    }
403
404    #[test]
405    fn test_request_context_principal_mutable() {
406        let mut ctx = RequestContext::new("1", TransportType::Http);
407        assert!(!ctx.is_authenticated());
408
409        ctx.set_principal(Principal::new("user-456"));
410        assert!(ctx.is_authenticated());
411        assert_eq!(ctx.subject(), Some("user-456"));
412
413        ctx.clear_principal();
414        assert!(!ctx.is_authenticated());
415    }
416}