mcp_host/content/
types.rs

1//! Content types for MCP messages
2//!
3//! Provides polymorphic content system inspired by Go mcphost
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use super::annotations::Annotations;
9
10/// Content type trait
11pub trait Content: Send + Sync {
12    /// Get the content type identifier
13    fn content_type(&self) -> &'static str;
14
15    /// Convert to JSON value
16    fn to_value(&self) -> Value;
17
18    /// Get annotations if present
19    fn annotations(&self) -> Option<&Annotations>;
20}
21
22/// Text content
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24pub struct TextContent {
25    /// Content type
26    #[serde(rename = "type")]
27    pub r#type: String,
28
29    /// The text
30    pub text: String,
31
32    /// Optional annotations
33    #[serde(skip_serializing_if = "Option::is_none", flatten)]
34    pub annotations: Option<Annotations>,
35}
36
37impl TextContent {
38    /// Create new text content
39    pub fn new(text: impl Into<String>) -> Self {
40        Self {
41            r#type: "text".to_string(),
42            text: text.into(),
43            annotations: None,
44        }
45    }
46
47    /// Set annotations
48    pub fn with_annotations(mut self, annotations: Annotations) -> Self {
49        self.annotations = Some(annotations);
50        self
51    }
52}
53
54impl Content for TextContent {
55    fn content_type(&self) -> &'static str {
56        "text"
57    }
58
59    fn to_value(&self) -> Value {
60        serde_json::to_value(self).unwrap()
61    }
62
63    fn annotations(&self) -> Option<&Annotations> {
64        self.annotations.as_ref()
65    }
66}
67
68/// Image content
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70pub struct ImageContent {
71    /// Content type
72    #[serde(rename = "type")]
73    pub r#type: String,
74
75    /// Base64-encoded image data
76    pub data: String,
77
78    /// MIME type (e.g., "image/png")
79    #[serde(rename = "mimeType")]
80    pub mime_type: String,
81
82    /// Optional annotations
83    #[serde(skip_serializing_if = "Option::is_none", flatten)]
84    pub annotations: Option<Annotations>,
85}
86
87impl ImageContent {
88    /// Create new image content
89    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
90        Self {
91            r#type: "image".to_string(),
92            data: data.into(),
93            mime_type: mime_type.into(),
94            annotations: None,
95        }
96    }
97
98    /// Set annotations
99    pub fn with_annotations(mut self, annotations: Annotations) -> Self {
100        self.annotations = Some(annotations);
101        self
102    }
103}
104
105impl Content for ImageContent {
106    fn content_type(&self) -> &'static str {
107        "image"
108    }
109
110    fn to_value(&self) -> Value {
111        serde_json::to_value(self).unwrap()
112    }
113
114    fn annotations(&self) -> Option<&Annotations> {
115        self.annotations.as_ref()
116    }
117}
118
119/// Audio content
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121pub struct AudioContent {
122    /// Content type
123    #[serde(rename = "type")]
124    pub r#type: String,
125
126    /// Base64-encoded audio data
127    pub data: String,
128
129    /// MIME type (e.g., "audio/wav")
130    #[serde(rename = "mimeType")]
131    pub mime_type: String,
132
133    /// Optional annotations
134    #[serde(skip_serializing_if = "Option::is_none", flatten)]
135    pub annotations: Option<Annotations>,
136}
137
138impl AudioContent {
139    /// Create new audio content
140    pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
141        Self {
142            r#type: "audio".to_string(),
143            data: data.into(),
144            mime_type: mime_type.into(),
145            annotations: None,
146        }
147    }
148
149    /// Set annotations
150    pub fn with_annotations(mut self, annotations: Annotations) -> Self {
151        self.annotations = Some(annotations);
152        self
153    }
154}
155
156impl Content for AudioContent {
157    fn content_type(&self) -> &'static str {
158        "audio"
159    }
160
161    fn to_value(&self) -> Value {
162        serde_json::to_value(self).unwrap()
163    }
164
165    fn annotations(&self) -> Option<&Annotations> {
166        self.annotations.as_ref()
167    }
168}
169
170/// Resource link content
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
172pub struct ResourceLink {
173    /// Content type
174    #[serde(rename = "type")]
175    pub r#type: String,
176
177    /// Resource URI
178    pub uri: String,
179
180    /// Optional title
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub title: Option<String>,
183
184    /// Optional annotations
185    #[serde(skip_serializing_if = "Option::is_none", flatten)]
186    pub annotations: Option<Annotations>,
187}
188
189impl ResourceLink {
190    /// Create new resource link
191    pub fn new(uri: impl Into<String>) -> Self {
192        Self {
193            r#type: "resource".to_string(),
194            uri: uri.into(),
195            title: None,
196            annotations: None,
197        }
198    }
199
200    /// Set title
201    pub fn with_title(mut self, title: impl Into<String>) -> Self {
202        self.title = Some(title.into());
203        self
204    }
205
206    /// Set annotations
207    pub fn with_annotations(mut self, annotations: Annotations) -> Self {
208        self.annotations = Some(annotations);
209        self
210    }
211}
212
213impl Content for ResourceLink {
214    fn content_type(&self) -> &'static str {
215        "resource"
216    }
217
218    fn to_value(&self) -> Value {
219        serde_json::to_value(self).unwrap()
220    }
221
222    fn annotations(&self) -> Option<&Annotations> {
223        self.annotations.as_ref()
224    }
225}
226
227/// Content array helper
228pub type ContentArray = Vec<Box<dyn Content>>;
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_text_content() {
236        let content = TextContent::new("Hello, world!");
237        assert_eq!(content.content_type(), "text");
238        assert_eq!(content.text, "Hello, world!");
239        assert!(content.annotations.is_none());
240    }
241
242    #[test]
243    fn test_text_with_annotations() {
244        let ann = Annotations::new().with_priority(0.9);
245        let content = TextContent::new("Important!").with_annotations(ann.clone());
246
247        assert_eq!(content.annotations, Some(ann));
248    }
249
250    #[test]
251    fn test_image_content() {
252        let content = ImageContent::new("base64data", "image/png");
253        assert_eq!(content.content_type(), "image");
254        assert_eq!(content.data, "base64data");
255        assert_eq!(content.mime_type, "image/png");
256    }
257
258    #[test]
259    fn test_resource_link() {
260        let link = ResourceLink::new("file://test.txt").with_title("Test File");
261
262        assert_eq!(link.content_type(), "resource");
263        assert_eq!(link.uri, "file://test.txt");
264        assert_eq!(link.title, Some("Test File".to_string()));
265    }
266
267    #[test]
268    fn test_serialization() {
269        let content = TextContent::new("test");
270        let json = content.to_value();
271
272        assert_eq!(json["text"], "test");
273        assert_eq!(json["type"], "text");
274    }
275}