mcp_host/content/
resource.rs

1//! Resource content type for MCP resources/read responses
2//!
3//! ResourceContent automatically includes uri and mimeType fields as required
4//! by the MCP spec for resources/read responses.
5
6use serde::Serialize;
7use serde_json::Value;
8
9use super::annotations::Annotations;
10use super::types::Content;
11
12/// Resource content for MCP resources/read responses
13///
14/// Automatically includes uri and mimeType per MCP spec.
15/// Use this type for Resource::read() return values.
16///
17/// # Examples
18///
19/// ```rust,ignore
20/// // Simple text resource
21/// ResourceContent::text("file:///path", "content here")
22///
23/// // With specific MIME type
24/// ResourceContent::text("file:///path", "content")
25///     .with_mime_type("application/json")
26///
27/// // Binary resource
28/// ResourceContent::blob("file:///path", base64_data, "image/png")
29/// ```
30#[derive(Debug, Clone, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct ResourceContent {
33    /// Resource URI (required)
34    pub uri: String,
35
36    /// MIME type
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub mime_type: Option<String>,
39
40    /// Text content (mutually exclusive with blob)
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub text: Option<String>,
43
44    /// Binary content as base64 (mutually exclusive with text)
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub blob: Option<String>,
47}
48
49impl ResourceContent {
50    /// Create a text resource with default text/plain MIME type
51    ///
52    /// # Examples
53    ///
54    /// ```rust,ignore
55    /// let content = ResourceContent::text("file:///doc.txt", "Hello, world!");
56    /// ```
57    pub fn text(uri: impl Into<String>, text: impl Into<String>) -> Self {
58        Self {
59            uri: uri.into(),
60            mime_type: Some("text/plain".to_string()),
61            text: Some(text.into()),
62            blob: None,
63        }
64    }
65
66    /// Set or override the MIME type
67    ///
68    /// # Examples
69    ///
70    /// ```rust,ignore
71    /// let content = ResourceContent::text("file:///data.json", json_string)
72    ///     .with_mime_type("application/json");
73    /// ```
74    pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
75        self.mime_type = Some(mime_type.into());
76        self
77    }
78
79    /// Create a binary resource with base64-encoded data
80    ///
81    /// # Examples
82    ///
83    /// ```rust,ignore
84    /// let content = ResourceContent::blob(
85    ///     "file:///image.png",
86    ///     base64_data,
87    ///     "image/png"
88    /// );
89    /// ```
90    pub fn blob(
91        uri: impl Into<String>,
92        data: impl Into<String>,
93        mime_type: impl Into<String>,
94    ) -> Self {
95        Self {
96            uri: uri.into(),
97            mime_type: Some(mime_type.into()),
98            text: None,
99            blob: Some(data.into()),
100        }
101    }
102}
103
104impl Content for ResourceContent {
105    fn content_type(&self) -> &'static str {
106        "resource"
107    }
108
109    fn to_value(&self) -> Value {
110        // serde_json handles the camelCase conversion via #[serde(rename_all)]
111        serde_json::to_value(self).expect("ResourceContent should always serialize")
112    }
113
114    fn annotations(&self) -> Option<&Annotations> {
115        None
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_text_resource() {
125        let content = ResourceContent::text("file:///test.txt", "Hello");
126
127        assert_eq!(content.uri, "file:///test.txt");
128        assert_eq!(content.mime_type, Some("text/plain".to_string()));
129        assert_eq!(content.text, Some("Hello".to_string()));
130        assert_eq!(content.blob, None);
131    }
132
133    #[test]
134    fn test_text_resource_with_mime_type() {
135        let content =
136            ResourceContent::text("file:///data.json", "{}").with_mime_type("application/json");
137
138        assert_eq!(content.mime_type, Some("application/json".to_string()));
139    }
140
141    #[test]
142    fn test_blob_resource() {
143        let content = ResourceContent::blob("file:///img.png", "base64data", "image/png");
144
145        assert_eq!(content.uri, "file:///img.png");
146        assert_eq!(content.mime_type, Some("image/png".to_string()));
147        assert_eq!(content.text, None);
148        assert_eq!(content.blob, Some("base64data".to_string()));
149    }
150
151    #[test]
152    fn test_serialization_camel_case() {
153        let content = ResourceContent::text("file:///test", "data").with_mime_type("text/plain");
154
155        let value = content.to_value();
156        let obj = value.as_object().unwrap();
157
158        // Should use camelCase for mimeType
159        assert!(obj.contains_key("uri"));
160        assert!(obj.contains_key("mimeType"));
161        assert!(obj.contains_key("text"));
162        assert!(!obj.contains_key("mime_type")); // snake_case should NOT be present
163    }
164
165    #[test]
166    fn test_content_type() {
167        let content = ResourceContent::text("uri", "text");
168        assert_eq!(content.content_type(), "resource");
169    }
170}