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 std::collections::HashMap;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
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(progress: f64, current_step: Option<u64>, total_steps: Option<u64>) -> Self {
166        Self {
167            progress: Some(progress.clamp(0.0, 1.0)),
168            current_step,
169            total_steps,
170            ..Default::default()
171        }
172    }
173
174    /// Add progress token
175    pub fn set_progress_token(mut self, token: impl Into<ProgressToken>) -> Self {
176        self.progress_token = Some(token.into());
177        self
178    }
179
180    /// Add cursor
181    pub fn set_cursor(mut self, cursor: impl Into<Cursor>) -> Self {
182        self.cursor = Some(cursor.into());
183        self
184    }
185
186    /// Set pagination info
187    pub fn set_pagination(mut self, cursor: Option<Cursor>, total: Option<u64>, has_more: bool) -> Self {
188        self.cursor = cursor;
189        self.total = total;
190        self.has_more = Some(has_more);
191        self
192    }
193
194    /// Set progress info
195    pub fn set_progress(mut self, progress: f64, current_step: Option<u64>, total_steps: Option<u64>) -> Self {
196        self.progress = Some(progress.clamp(0.0, 1.0));
197        self.current_step = current_step;
198        self.total_steps = total_steps;
199        self
200    }
201
202    /// Set estimated remaining time
203    pub fn set_estimated_remaining(mut self, seconds: f64) -> Self {
204        self.estimated_remaining_seconds = Some(seconds);
205        self
206    }
207
208    /// Add extra metadata
209    pub fn add_extra(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
210        self.extra.insert(key.into(), value.into());
211        self
212    }
213
214    /// Check if meta has any content
215    pub fn is_empty(&self) -> bool {
216        self.progress_token.is_none() &&
217        self.cursor.is_none() &&
218        self.total.is_none() &&
219        self.has_more.is_none() &&
220        self.estimated_remaining_seconds.is_none() &&
221        self.progress.is_none() &&
222        self.current_step.is_none() &&
223        self.total_steps.is_none() &&
224        self.extra.is_empty()
225    }
226}
227
228/// Trait for types that can include _meta fields
229pub trait WithMeta {
230    /// Get the _meta field
231    fn meta(&self) -> Option<&Meta>;
232    
233    /// Set the _meta field
234    fn set_meta(&mut self, meta: Option<Meta>);
235    
236    /// Add or update _meta field with builder pattern
237    fn with_meta(mut self, meta: Meta) -> Self 
238    where 
239        Self: Sized,
240    {
241        self.set_meta(Some(meta));
242        self
243    }
244}
245
246/// Helper for pagination responses
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct PaginatedResponse<T> {
249    /// The actual response data
250    #[serde(flatten)]
251    pub data: T,
252    
253    /// Pagination metadata
254    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
255    pub meta: Option<Meta>,
256}
257
258impl<T> PaginatedResponse<T> {
259    pub fn new(data: T) -> Self {
260        Self {
261            data,
262            meta: None,
263        }
264    }
265
266    pub fn with_pagination(data: T, cursor: Option<Cursor>, total: Option<u64>, has_more: bool) -> Self {
267        Self {
268            data,
269            meta: Some(Meta::with_pagination(cursor, total, has_more)),
270        }
271    }
272}
273
274impl<T> WithMeta for PaginatedResponse<T> {
275    fn meta(&self) -> Option<&Meta> {
276        self.meta.as_ref()
277    }
278
279    fn set_meta(&mut self, meta: Option<Meta>) {
280        self.meta = meta;
281    }
282}
283
284/// Helper for progress responses
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct ProgressResponse<T> {
287    /// The actual response data
288    #[serde(flatten)]
289    pub data: T,
290    
291    /// Progress metadata
292    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
293    pub meta: Option<Meta>,
294}
295
296impl<T> ProgressResponse<T> {
297    pub fn new(data: T) -> Self {
298        Self {
299            data,
300            meta: None,
301        }
302    }
303
304    pub fn with_progress(
305        data: T, 
306        progress_token: Option<ProgressToken>,
307        progress: f64,
308        current_step: Option<u64>,
309        total_steps: Option<u64>
310    ) -> Self {
311        let mut meta = Meta::with_progress(progress, current_step, total_steps);
312        if let Some(token) = progress_token {
313            meta = meta.set_progress_token(token);
314        }
315        
316        Self {
317            data,
318            meta: Some(meta),
319        }
320    }
321}
322
323impl<T> WithMeta for ProgressResponse<T> {
324    fn meta(&self) -> Option<&Meta> {
325        self.meta.as_ref()
326    }
327
328    fn set_meta(&mut self, meta: Option<Meta>) {
329        self.meta = meta;
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use serde_json::json;
337
338    #[test]
339    fn test_progress_token() {
340        let token = ProgressToken::new("task-123");
341        assert_eq!(token.as_str(), "task-123");
342        
343        let from_string: ProgressToken = "task-456".into();
344        assert_eq!(from_string.as_str(), "task-456");
345    }
346
347    #[test]
348    fn test_cursor() {
349        let cursor = Cursor::new("page-2");
350        assert_eq!(cursor.as_str(), "page-2");
351        
352        let from_string: Cursor = "page-3".into();
353        assert_eq!(from_string.as_str(), "page-3");
354    }
355
356    #[test]
357    fn test_meta_creation() {
358        let meta = Meta::new()
359            .set_progress_token("task-123")
360            .set_progress(0.5, Some(5), Some(10))
361            .add_extra("custom_field", "custom_value");
362        
363        assert_eq!(meta.progress_token.as_ref().unwrap().as_str(), "task-123");
364        assert_eq!(meta.progress, Some(0.5));
365        assert_eq!(meta.current_step, Some(5));
366        assert_eq!(meta.total_steps, Some(10));
367        assert_eq!(meta.extra.get("custom_field"), Some(&json!("custom_value")));
368    }
369
370    #[test]
371    fn test_meta_serialization() {
372        let meta = Meta::with_progress_token("task-123")
373            .set_cursor("page-1")
374            .set_progress(0.75, Some(3), Some(4));
375        
376        let json = serde_json::to_string(&meta).unwrap();
377        let deserialized: Meta = serde_json::from_str(&json).unwrap();
378        
379        assert_eq!(meta.progress_token, deserialized.progress_token);
380        assert_eq!(meta.cursor, deserialized.cursor);
381        assert_eq!(meta.progress, deserialized.progress);
382    }
383
384    #[test]
385    fn test_paginated_response() {
386        #[derive(Serialize, Deserialize)]
387        struct TestData {
388            items: Vec<String>,
389        }
390        
391        let data = TestData {
392            items: vec!["item1".to_string(), "item2".to_string()],
393        };
394        
395        let response = PaginatedResponse::with_pagination(
396            data,
397            Some("next-page".into()),
398            Some(100),
399            true
400        );
401        
402        let json = serde_json::to_string(&response).unwrap();
403        let deserialized: PaginatedResponse<TestData> = serde_json::from_str(&json).unwrap();
404        
405        assert_eq!(deserialized.data.items.len(), 2);
406        assert!(deserialized.meta.is_some());
407        assert_eq!(deserialized.meta.as_ref().unwrap().cursor.as_ref().unwrap().as_str(), "next-page");
408        assert_eq!(deserialized.meta.as_ref().unwrap().total, Some(100));
409        assert_eq!(deserialized.meta.as_ref().unwrap().has_more, Some(true));
410    }
411
412    #[test]
413    fn test_progress_response() {
414        #[derive(Serialize, Deserialize)]
415        struct TaskResult {
416            status: String,
417        }
418        
419        let data = TaskResult {
420            status: "processing".to_string(),
421        };
422        
423        let response = ProgressResponse::with_progress(
424            data,
425            Some("task-456".into()),
426            0.8,
427            Some(8),
428            Some(10)
429        );
430        
431        let json = serde_json::to_string(&response).unwrap();
432        let deserialized: ProgressResponse<TaskResult> = serde_json::from_str(&json).unwrap();
433        
434        assert_eq!(deserialized.data.status, "processing");
435        assert!(deserialized.meta.is_some());
436        assert_eq!(deserialized.meta.as_ref().unwrap().progress_token.as_ref().unwrap().as_str(), "task-456");
437        assert_eq!(deserialized.meta.as_ref().unwrap().progress, Some(0.8));
438        assert_eq!(deserialized.meta.as_ref().unwrap().current_step, Some(8));
439        assert_eq!(deserialized.meta.as_ref().unwrap().total_steps, Some(10));
440    }
441
442    #[test]
443    fn test_meta_is_empty() {
444        let empty_meta = Meta::new();
445        assert!(empty_meta.is_empty());
446        
447        let non_empty_meta = Meta::new().set_progress_token("test");
448        assert!(!non_empty_meta.is_empty());
449    }
450}