Skip to main content

rmcp/model/
resource.rs

1use serde::{Deserialize, Serialize};
2
3use super::{Annotated, Icon, Meta};
4
5/// Represents a resource in the extension with metadata
6#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
7#[serde(rename_all = "camelCase")]
8#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
10pub struct RawResource {
11    /// URI representing the resource location (e.g., "file:///path/to/file" or "str:///content")
12    pub uri: String,
13    /// Name of the resource
14    pub name: String,
15    /// Human-readable title of the resource
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub title: Option<String>,
18    /// Optional description of the resource
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub description: Option<String>,
21    /// MIME type of the resource content ("text" or "blob")
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub mime_type: Option<String>,
24
25    /// The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
26    ///
27    /// This can be used by Hosts to display file sizes and estimate context window us
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub size: Option<u32>,
30    /// Optional list of icons for the resource
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub icons: Option<Vec<Icon>>,
33    /// Optional additional metadata for this resource
34    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
35    pub meta: Option<Meta>,
36}
37
38pub type Resource = Annotated<RawResource>;
39
40#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
41#[serde(rename_all = "camelCase")]
42#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
43#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
44pub struct RawResourceTemplate {
45    pub uri_template: String,
46    pub name: String,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub title: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub description: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub mime_type: Option<String>,
53    /// Optional list of icons for the resource template
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub icons: Option<Vec<Icon>>,
56}
57
58pub type ResourceTemplate = Annotated<RawResourceTemplate>;
59
60#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
61#[serde(untagged)]
62#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
63#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
64pub enum ResourceContents {
65    #[serde(rename_all = "camelCase")]
66    TextResourceContents {
67        uri: String,
68        #[serde(skip_serializing_if = "Option::is_none")]
69        mime_type: Option<String>,
70        text: String,
71        #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
72        meta: Option<Meta>,
73    },
74    #[serde(rename_all = "camelCase")]
75    BlobResourceContents {
76        uri: String,
77        #[serde(skip_serializing_if = "Option::is_none")]
78        mime_type: Option<String>,
79        blob: String,
80        #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
81        meta: Option<Meta>,
82    },
83}
84
85impl ResourceContents {
86    /// Create text resource contents.
87    pub fn text(text: impl Into<String>, uri: impl Into<String>) -> Self {
88        Self::TextResourceContents {
89            uri: uri.into(),
90            mime_type: Some("text".into()),
91            text: text.into(),
92            meta: None,
93        }
94    }
95
96    /// Create blob resource contents.
97    pub fn blob(blob: impl Into<String>, uri: impl Into<String>) -> Self {
98        Self::BlobResourceContents {
99            uri: uri.into(),
100            mime_type: None,
101            blob: blob.into(),
102            meta: None,
103        }
104    }
105
106    /// Set the MIME type on this resource contents.
107    pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
108        match &mut self {
109            Self::TextResourceContents { mime_type: mt, .. } => *mt = Some(mime_type.into()),
110            Self::BlobResourceContents { mime_type: mt, .. } => *mt = Some(mime_type.into()),
111        }
112        self
113    }
114
115    /// Set the metadata on this resource contents.
116    pub fn with_meta(mut self, meta: Meta) -> Self {
117        match &mut self {
118            Self::TextResourceContents { meta: m, .. } => *m = Some(meta),
119            Self::BlobResourceContents { meta: m, .. } => *m = Some(meta),
120        }
121        self
122    }
123}
124
125impl RawResource {
126    /// Creates a new Resource from a URI with explicit mime type
127    pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
128        Self {
129            uri: uri.into(),
130            name: name.into(),
131            title: None,
132            description: None,
133            mime_type: None,
134            size: None,
135            icons: None,
136            meta: None,
137        }
138    }
139
140    /// Set the human-readable title.
141    pub fn with_title(mut self, title: impl Into<String>) -> Self {
142        self.title = Some(title.into());
143        self
144    }
145
146    /// Set the description.
147    pub fn with_description(mut self, description: impl Into<String>) -> Self {
148        self.description = Some(description.into());
149        self
150    }
151
152    /// Set the MIME type.
153    pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
154        self.mime_type = Some(mime_type.into());
155        self
156    }
157
158    /// Set the size in bytes.
159    pub fn with_size(mut self, size: u32) -> Self {
160        self.size = Some(size);
161        self
162    }
163
164    /// Set the icons.
165    pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
166        self.icons = Some(icons);
167        self
168    }
169
170    /// Set the metadata.
171    pub fn with_meta(mut self, meta: Meta) -> Self {
172        self.meta = Some(meta);
173        self
174    }
175}
176
177impl RawResourceTemplate {
178    /// Creates a new RawResourceTemplate with a URI template and name.
179    pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
180        Self {
181            uri_template: uri_template.into(),
182            name: name.into(),
183            title: None,
184            description: None,
185            mime_type: None,
186            icons: None,
187        }
188    }
189
190    /// Set the human-readable title.
191    pub fn with_title(mut self, title: impl Into<String>) -> Self {
192        self.title = Some(title.into());
193        self
194    }
195
196    /// Set the description.
197    pub fn with_description(mut self, description: impl Into<String>) -> Self {
198        self.description = Some(description.into());
199        self
200    }
201
202    /// Set the MIME type.
203    pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
204        self.mime_type = Some(mime_type.into());
205        self
206    }
207
208    /// Set the icons.
209    pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
210        self.icons = Some(icons);
211        self
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use serde_json;
218
219    use super::*;
220    use crate::model::IconTheme;
221
222    #[test]
223    fn test_resource_serialization() {
224        let resource = RawResource {
225            uri: "file:///test.txt".to_string(),
226            title: None,
227            name: "test".to_string(),
228            description: Some("Test resource".to_string()),
229            mime_type: Some("text/plain".to_string()),
230            size: Some(100),
231            icons: None,
232            meta: None,
233        };
234
235        let json = serde_json::to_string(&resource).unwrap();
236        println!("Serialized JSON: {}", json);
237
238        // Verify it contains mimeType (camelCase) not mime_type (snake_case)
239        assert!(json.contains("mimeType"));
240        assert!(!json.contains("mime_type"));
241    }
242
243    #[test]
244    fn test_resource_contents_serialization() {
245        let text_contents = ResourceContents::TextResourceContents {
246            uri: "file:///test.txt".to_string(),
247            mime_type: Some("text/plain".to_string()),
248            text: "Hello world".to_string(),
249            meta: None,
250        };
251
252        let json = serde_json::to_string(&text_contents).unwrap();
253        println!("ResourceContents JSON: {}", json);
254
255        // Verify it contains mimeType (camelCase) not mime_type (snake_case)
256        assert!(json.contains("mimeType"));
257        assert!(!json.contains("mime_type"));
258    }
259
260    #[test]
261    fn test_resource_template_with_icons() {
262        let resource_template = RawResourceTemplate {
263            uri_template: "file:///{path}".to_string(),
264            name: "template".to_string(),
265            title: Some("Test Template".to_string()),
266            description: Some("A test resource template".to_string()),
267            mime_type: Some("text/plain".to_string()),
268            icons: Some(vec![Icon {
269                src: "https://example.com/icon.png".to_string(),
270                mime_type: Some("image/png".to_string()),
271                sizes: Some(vec!["48x48".to_string()]),
272                theme: Some(IconTheme::Light),
273            }]),
274        };
275
276        let json = serde_json::to_value(&resource_template).unwrap();
277        assert!(json["icons"].is_array());
278        assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
279        assert_eq!(json["icons"][0]["sizes"][0], "48x48");
280        assert_eq!(json["icons"][0]["theme"], "light");
281    }
282
283    #[test]
284    fn test_resource_template_without_icons() {
285        let resource_template = RawResourceTemplate {
286            uri_template: "file:///{path}".to_string(),
287            name: "template".to_string(),
288            title: None,
289            description: None,
290            mime_type: None,
291            icons: None,
292        };
293
294        let json = serde_json::to_value(&resource_template).unwrap();
295        assert!(json.get("icons").is_none());
296    }
297}