mcp_host/content/
annotations.rs

1//! Content annotations
2//!
3//! Annotations provide metadata about content (audience, priority, timestamps, etc.)
4//! Based on MCP 2025-11-25 spec and Go mcphost implementation
5
6use serde::{Deserialize, Serialize};
7
8/// Content annotations
9#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
10#[serde(rename_all = "camelCase")]
11pub struct Annotations {
12    /// Target audience for this content
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub audience: Option<Vec<String>>,
15
16    /// Priority level (0.0 to 1.0, higher = more important)
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub priority: Option<f64>,
19
20    /// Timestamp when content was last modified (ISO 8601)
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub last_modified: Option<String>,
23
24    /// Language code (e.g., "en", "es", "fr")
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub language: Option<String>,
27}
28
29impl Annotations {
30    /// Create new empty annotations
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Set audience
36    pub fn with_audience(mut self, audience: Vec<String>) -> Self {
37        self.audience = Some(audience);
38        self
39    }
40
41    /// Add single audience member
42    pub fn add_audience(mut self, audience: impl Into<String>) -> Self {
43        self.audience
44            .get_or_insert_with(Vec::new)
45            .push(audience.into());
46        self
47    }
48
49    /// Set priority (clamped to 0.0-1.0)
50    pub fn with_priority(mut self, priority: f64) -> Self {
51        self.priority = Some(priority.clamp(0.0, 1.0));
52        self
53    }
54
55    /// Set last modified timestamp
56    pub fn with_last_modified(mut self, timestamp: impl Into<String>) -> Self {
57        self.last_modified = Some(timestamp.into());
58        self
59    }
60
61    /// Set language
62    pub fn with_language(mut self, language: impl Into<String>) -> Self {
63        self.language = Some(language.into());
64        self
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_annotations_builder() {
74        let ann = Annotations::new()
75            .with_priority(0.8)
76            .add_audience("user")
77            .add_audience("admin")
78            .with_language("en");
79
80        assert_eq!(ann.priority, Some(0.8));
81        assert_eq!(
82            ann.audience,
83            Some(vec!["user".to_string(), "admin".to_string()])
84        );
85        assert_eq!(ann.language, Some("en".to_string()));
86    }
87
88    #[test]
89    fn test_priority_clamping() {
90        let ann = Annotations::new().with_priority(1.5);
91        assert_eq!(ann.priority, Some(1.0));
92
93        let ann = Annotations::new().with_priority(-0.5);
94        assert_eq!(ann.priority, Some(0.0));
95    }
96
97    #[test]
98    fn test_serialization() {
99        let ann = Annotations::new().with_priority(0.9).with_language("en");
100
101        let json = serde_json::to_value(&ann).unwrap();
102        assert_eq!(json["priority"], 0.9);
103        assert_eq!(json["language"], "en");
104        assert!(json.get("audience").is_none()); // Not serialized if None
105    }
106}