Skip to main content

rmcp/model/
resource.rs

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