turul_mcp_protocol_2025_06_18/
meta.rs

1//! _meta Field Support for MCP 2025-06-18
2//!
3//! This module provides comprehensive support for the structured _meta fields
4//! introduced in MCP 2025-06-18 specification.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9
10/// Generic annotations structure (matches TypeScript Annotations)
11/// Used across all MCP types that support client annotations
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Annotations {
14    /// Display name override for clients
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub title: Option<String>,
17    // Additional annotation fields can be added here as needed
18}
19
20impl Annotations {
21    pub fn new() -> Self {
22        Self { title: None }
23    }
24
25    pub fn with_title(mut self, title: impl Into<String>) -> Self {
26        self.title = Some(title.into());
27        self
28    }
29}
30
31impl Default for Annotations {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37/// Progress token for tracking long-running operations
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(transparent)]
40pub struct ProgressToken(pub String);
41
42impl ProgressToken {
43    pub fn new(token: impl Into<String>) -> Self {
44        Self(token.into())
45    }
46
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52impl From<String> for ProgressToken {
53    fn from(s: String) -> Self {
54        Self(s)
55    }
56}
57
58impl From<&str> for ProgressToken {
59    fn from(s: &str) -> Self {
60        Self(s.to_string())
61    }
62}
63
64/// Cursor for pagination support
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66#[serde(transparent)]
67pub struct Cursor(pub String);
68
69impl Cursor {
70    pub fn new(cursor: impl Into<String>) -> Self {
71        Self(cursor.into())
72    }
73
74    pub fn as_str(&self) -> &str {
75        &self.0
76    }
77}
78
79impl From<String> for Cursor {
80    fn from(s: String) -> Self {
81        Self(s)
82    }
83}
84
85impl From<&str> for Cursor {
86    fn from(s: &str) -> Self {
87        Self(s.to_string())
88    }
89}
90
91/// Structured _meta field for MCP 2025-06-18
92#[derive(Debug, Clone, Serialize, Deserialize, Default)]
93#[serde(rename_all = "camelCase")]
94pub struct Meta {
95    /// Progress token for tracking long-running operations
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub progress_token: Option<ProgressToken>,
98
99    /// Cursor for pagination
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub cursor: Option<Cursor>,
102
103    /// Total number of items (for pagination context)
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub total: Option<u64>,
106
107    /// Whether there are more items available
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub has_more: Option<bool>,
110
111    /// Estimated remaining time in seconds
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub estimated_remaining_seconds: Option<f64>,
114
115    /// Current progress (0.0 to 1.0)
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub progress: Option<f64>,
118
119    /// Current step in a multi-step process
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub current_step: Option<u64>,
122
123    /// Total steps in a multi-step process
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub total_steps: Option<u64>,
126
127    /// Additional metadata as key-value pairs
128    #[serde(flatten)]
129    pub extra: HashMap<String, Value>,
130}
131
132impl Meta {
133    /// Create a new empty Meta
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    /// Create Meta with progress token
139    pub fn with_progress_token(token: impl Into<ProgressToken>) -> Self {
140        Self {
141            progress_token: Some(token.into()),
142            ..Default::default()
143        }
144    }
145
146    /// Create Meta with cursor for pagination
147    pub fn with_cursor(cursor: impl Into<Cursor>) -> Self {
148        Self {
149            cursor: Some(cursor.into()),
150            ..Default::default()
151        }
152    }
153
154    /// Create Meta with pagination info
155    pub fn with_pagination(cursor: Option<Cursor>, total: Option<u64>, has_more: bool) -> Self {
156        Self {
157            cursor,
158            total,
159            has_more: Some(has_more),
160            ..Default::default()
161        }
162    }
163
164    /// Create Meta with progress information
165    pub fn with_progress(
166        progress: f64,
167        current_step: Option<u64>,
168        total_steps: Option<u64>,
169    ) -> Self {
170        Self {
171            progress: Some(progress.clamp(0.0, 1.0)),
172            current_step,
173            total_steps,
174            ..Default::default()
175        }
176    }
177
178    /// Add progress token
179    pub fn set_progress_token(mut self, token: impl Into<ProgressToken>) -> Self {
180        self.progress_token = Some(token.into());
181        self
182    }
183
184    /// Add cursor
185    pub fn set_cursor(mut self, cursor: impl Into<Cursor>) -> Self {
186        self.cursor = Some(cursor.into());
187        self
188    }
189
190    /// Set pagination info
191    pub fn set_pagination(
192        mut self,
193        cursor: Option<Cursor>,
194        total: Option<u64>,
195        has_more: bool,
196    ) -> Self {
197        self.cursor = cursor;
198        self.total = total;
199        self.has_more = Some(has_more);
200        self
201    }
202
203    /// Set progress info
204    pub fn set_progress(
205        mut self,
206        progress: f64,
207        current_step: Option<u64>,
208        total_steps: Option<u64>,
209    ) -> Self {
210        self.progress = Some(progress.clamp(0.0, 1.0));
211        self.current_step = current_step;
212        self.total_steps = total_steps;
213        self
214    }
215
216    /// Set estimated remaining time
217    pub fn set_estimated_remaining(mut self, seconds: f64) -> Self {
218        self.estimated_remaining_seconds = Some(seconds);
219        self
220    }
221
222    /// Add extra metadata
223    pub fn add_extra(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
224        self.extra.insert(key.into(), value.into());
225        self
226    }
227
228    /// Check if meta has any content
229    pub fn is_empty(&self) -> bool {
230        self.progress_token.is_none()
231            && self.cursor.is_none()
232            && self.total.is_none()
233            && self.has_more.is_none()
234            && self.estimated_remaining_seconds.is_none()
235            && self.progress.is_none()
236            && self.current_step.is_none()
237            && self.total_steps.is_none()
238            && self.extra.is_empty()
239    }
240
241    /// Merge request extras from incoming request _meta into this Meta
242    /// This helper preserves pagination context while adding request metadata
243    pub fn merge_request_extras(mut self, request_meta: Option<&HashMap<String, Value>>) -> Self {
244        if let Some(request_extras) = request_meta {
245            for (key, value) in request_extras {
246                // Don't override structured fields - only merge into extra
247                match key.as_str() {
248                    "progressToken"
249                    | "cursor"
250                    | "total"
251                    | "hasMore"
252                    | "estimatedRemainingSeconds"
253                    | "progress"
254                    | "currentStep"
255                    | "totalSteps" => {
256                        // Skip reserved fields - these should be set explicitly
257                    }
258                    _ => {
259                        self.extra.insert(key.clone(), value.clone());
260                    }
261                }
262            }
263        }
264        self
265    }
266}
267
268/// Trait for types that can include _meta fields
269pub trait WithMeta {
270    /// Get the _meta field
271    fn meta(&self) -> Option<&Meta>;
272
273    /// Set the _meta field
274    fn set_meta(&mut self, meta: Option<Meta>);
275
276    /// Add or update _meta field with builder pattern
277    fn with_meta(mut self, meta: Meta) -> Self
278    where
279        Self: Sized,
280    {
281        self.set_meta(Some(meta));
282        self
283    }
284}
285
286/// Helper for pagination responses
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct PaginatedResponse<T> {
289    /// The actual response data
290    #[serde(flatten)]
291    pub data: T,
292
293    /// Pagination metadata
294    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
295    pub meta: Option<Meta>,
296}
297
298impl<T> PaginatedResponse<T> {
299    pub fn new(data: T) -> Self {
300        Self { data, meta: None }
301    }
302
303    pub fn with_pagination(
304        data: T,
305        cursor: Option<Cursor>,
306        total: Option<u64>,
307        has_more: bool,
308    ) -> Self {
309        Self {
310            data,
311            meta: Some(Meta::with_pagination(cursor, total, has_more)),
312        }
313    }
314}
315
316impl<T> WithMeta for PaginatedResponse<T> {
317    fn meta(&self) -> Option<&Meta> {
318        self.meta.as_ref()
319    }
320
321    fn set_meta(&mut self, meta: Option<Meta>) {
322        self.meta = meta;
323    }
324}
325
326/// Helper for progress responses
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ProgressResponse<T> {
329    /// The actual response data
330    #[serde(flatten)]
331    pub data: T,
332
333    /// Progress metadata
334    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
335    pub meta: Option<Meta>,
336}
337
338impl<T> ProgressResponse<T> {
339    pub fn new(data: T) -> Self {
340        Self { data, meta: None }
341    }
342
343    pub fn with_progress(
344        data: T,
345        progress_token: Option<ProgressToken>,
346        progress: f64,
347        current_step: Option<u64>,
348        total_steps: Option<u64>,
349    ) -> Self {
350        let mut meta = Meta::with_progress(progress, current_step, total_steps);
351        if let Some(token) = progress_token {
352            meta = meta.set_progress_token(token);
353        }
354
355        Self {
356            data,
357            meta: Some(meta),
358        }
359    }
360}
361
362impl<T> WithMeta for ProgressResponse<T> {
363    fn meta(&self) -> Option<&Meta> {
364        self.meta.as_ref()
365    }
366
367    fn set_meta(&mut self, meta: Option<Meta>) {
368        self.meta = meta;
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use serde_json::json;
376
377    #[test]
378    fn test_progress_token() {
379        let token = ProgressToken::new("task-123");
380        assert_eq!(token.as_str(), "task-123");
381
382        let from_string: ProgressToken = "task-456".into();
383        assert_eq!(from_string.as_str(), "task-456");
384    }
385
386    #[test]
387    fn test_cursor() {
388        let cursor = Cursor::new("page-2");
389        assert_eq!(cursor.as_str(), "page-2");
390
391        let from_string: Cursor = "page-3".into();
392        assert_eq!(from_string.as_str(), "page-3");
393    }
394
395    #[test]
396    fn test_meta_creation() {
397        let meta = Meta::new()
398            .set_progress_token("task-123")
399            .set_progress(0.5, Some(5), Some(10))
400            .add_extra("custom_field", "custom_value");
401
402        assert_eq!(meta.progress_token.as_ref().unwrap().as_str(), "task-123");
403        assert_eq!(meta.progress, Some(0.5));
404        assert_eq!(meta.current_step, Some(5));
405        assert_eq!(meta.total_steps, Some(10));
406        assert_eq!(meta.extra.get("custom_field"), Some(&json!("custom_value")));
407    }
408
409    #[test]
410    fn test_meta_serialization() {
411        let meta = Meta::with_progress_token("task-123")
412            .set_cursor("page-1")
413            .set_progress(0.75, Some(3), Some(4));
414
415        let json = serde_json::to_string(&meta).unwrap();
416        let deserialized: Meta = serde_json::from_str(&json).unwrap();
417
418        assert_eq!(meta.progress_token, deserialized.progress_token);
419        assert_eq!(meta.cursor, deserialized.cursor);
420        assert_eq!(meta.progress, deserialized.progress);
421    }
422
423    #[test]
424    fn test_paginated_response() {
425        #[derive(Serialize, Deserialize)]
426        struct TestData {
427            items: Vec<String>,
428        }
429
430        let data = TestData {
431            items: vec!["item1".to_string(), "item2".to_string()],
432        };
433
434        let response =
435            PaginatedResponse::with_pagination(data, Some("next-page".into()), Some(100), true);
436
437        let json = serde_json::to_string(&response).unwrap();
438        let deserialized: PaginatedResponse<TestData> = serde_json::from_str(&json).unwrap();
439
440        assert_eq!(deserialized.data.items.len(), 2);
441        assert!(deserialized.meta.is_some());
442        assert_eq!(
443            deserialized
444                .meta
445                .as_ref()
446                .unwrap()
447                .cursor
448                .as_ref()
449                .unwrap()
450                .as_str(),
451            "next-page"
452        );
453        assert_eq!(deserialized.meta.as_ref().unwrap().total, Some(100));
454        assert_eq!(deserialized.meta.as_ref().unwrap().has_more, Some(true));
455    }
456
457    #[test]
458    fn test_progress_response() {
459        #[derive(Serialize, Deserialize)]
460        struct TaskResult {
461            status: String,
462        }
463
464        let data = TaskResult {
465            status: "processing".to_string(),
466        };
467
468        let response =
469            ProgressResponse::with_progress(data, Some("task-456".into()), 0.8, Some(8), Some(10));
470
471        let json = serde_json::to_string(&response).unwrap();
472        let deserialized: ProgressResponse<TaskResult> = serde_json::from_str(&json).unwrap();
473
474        assert_eq!(deserialized.data.status, "processing");
475        assert!(deserialized.meta.is_some());
476        assert_eq!(
477            deserialized
478                .meta
479                .as_ref()
480                .unwrap()
481                .progress_token
482                .as_ref()
483                .unwrap()
484                .as_str(),
485            "task-456"
486        );
487        assert_eq!(deserialized.meta.as_ref().unwrap().progress, Some(0.8));
488        assert_eq!(deserialized.meta.as_ref().unwrap().current_step, Some(8));
489        assert_eq!(deserialized.meta.as_ref().unwrap().total_steps, Some(10));
490    }
491
492    #[test]
493    fn test_meta_is_empty() {
494        let empty_meta = Meta::new();
495        assert!(empty_meta.is_empty());
496
497        let non_empty_meta = Meta::new().set_progress_token("test");
498        assert!(!non_empty_meta.is_empty());
499    }
500
501    #[test]
502    fn test_merge_request_extras() {
503        let mut request_meta = HashMap::new();
504        request_meta.insert("customField".to_string(), json!("custom_value"));
505        request_meta.insert("userContext".to_string(), json!("user_123"));
506        request_meta.insert("progressToken".to_string(), json!("should_be_ignored"));
507        request_meta.insert("cursor".to_string(), json!("should_be_ignored"));
508
509        let meta = Meta::with_pagination(Some("page-1".into()), Some(100), true)
510            .merge_request_extras(Some(&request_meta));
511
512        // Should preserve existing structured fields
513        assert_eq!(meta.cursor.as_ref().unwrap().as_str(), "page-1");
514        assert_eq!(meta.total, Some(100));
515        assert_eq!(meta.has_more, Some(true));
516
517        // Should add custom fields to extra
518        assert_eq!(meta.extra.get("customField"), Some(&json!("custom_value")));
519        assert_eq!(meta.extra.get("userContext"), Some(&json!("user_123")));
520
521        // Should not override structured fields in extra
522        assert!(!meta.extra.contains_key("progressToken"));
523        assert!(!meta.extra.contains_key("cursor"));
524    }
525
526    #[test]
527    fn test_merge_request_extras_empty() {
528        let meta = Meta::with_cursor("test-cursor").merge_request_extras(None);
529
530        assert_eq!(meta.cursor.as_ref().unwrap().as_str(), "test-cursor");
531        assert!(meta.extra.is_empty());
532    }
533}