Skip to main content

mcpkit_rs/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))]
9pub struct RawResource {
10    /// URI representing the resource location (e.g., "file:///path/to/file" or "str:///content")
11    pub uri: String,
12    /// Name of the resource
13    pub name: String,
14    /// Human-readable title of the resource
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub title: Option<String>,
17    /// Optional description of the resource
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub description: Option<String>,
20    /// MIME type of the resource content ("text" or "blob")
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub mime_type: Option<String>,
23
24    /// The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
25    ///
26    /// This can be used by Hosts to display file sizes and estimate context window us
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub size: Option<u32>,
29    /// Optional list of icons for the resource
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub icons: Option<Vec<Icon>>,
32    /// Optional additional metadata for this resource
33    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
34    pub meta: Option<Meta>,
35}
36
37pub type Resource = Annotated<RawResource>;
38
39#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
40#[serde(rename_all = "camelCase")]
41#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
42pub struct RawResourceTemplate {
43    pub uri_template: String,
44    pub name: String,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub title: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub description: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub mime_type: Option<String>,
51    /// Optional list of icons for the resource template
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub icons: Option<Vec<Icon>>,
54}
55
56pub type ResourceTemplate = Annotated<RawResourceTemplate>;
57
58#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
59#[serde(untagged)]
60#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
61pub enum ResourceContents {
62    #[serde(rename_all = "camelCase")]
63    TextResourceContents {
64        uri: String,
65        #[serde(skip_serializing_if = "Option::is_none")]
66        mime_type: Option<String>,
67        text: String,
68        #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
69        meta: Option<Meta>,
70    },
71    #[serde(rename_all = "camelCase")]
72    BlobResourceContents {
73        uri: String,
74        #[serde(skip_serializing_if = "Option::is_none")]
75        mime_type: Option<String>,
76        blob: String,
77        #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
78        meta: Option<Meta>,
79    },
80}
81
82impl ResourceContents {
83    pub fn text(text: impl Into<String>, uri: impl Into<String>) -> Self {
84        Self::TextResourceContents {
85            uri: uri.into(),
86            mime_type: Some("text".into()),
87            text: text.into(),
88            meta: None,
89        }
90    }
91}
92
93impl RawResource {
94    /// Creates a new Resource from a URI with explicit mime type
95    pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
96        Self {
97            uri: uri.into(),
98            name: name.into(),
99            title: None,
100            description: None,
101            mime_type: None,
102            size: None,
103            icons: None,
104            meta: None,
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use serde_json;
112
113    use super::*;
114
115    #[test]
116    fn test_resource_serialization() {
117        let resource = RawResource {
118            uri: "file:///test.txt".to_string(),
119            title: None,
120            name: "test".to_string(),
121            description: Some("Test resource".to_string()),
122            mime_type: Some("text/plain".to_string()),
123            size: Some(100),
124            icons: None,
125            meta: None,
126        };
127
128        let json = serde_json::to_string(&resource).unwrap();
129        println!("Serialized JSON: {}", json);
130
131        // Verify it contains mimeType (camelCase) not mime_type (snake_case)
132        assert!(json.contains("mimeType"));
133        assert!(!json.contains("mime_type"));
134    }
135
136    #[test]
137    fn test_resource_contents_serialization() {
138        let text_contents = ResourceContents::TextResourceContents {
139            uri: "file:///test.txt".to_string(),
140            mime_type: Some("text/plain".to_string()),
141            text: "Hello world".to_string(),
142            meta: None,
143        };
144
145        let json = serde_json::to_string(&text_contents).unwrap();
146        println!("ResourceContents JSON: {}", json);
147
148        // Verify it contains mimeType (camelCase) not mime_type (snake_case)
149        assert!(json.contains("mimeType"));
150        assert!(!json.contains("mime_type"));
151    }
152
153    #[test]
154    fn test_resource_template_with_icons() {
155        let resource_template = RawResourceTemplate {
156            uri_template: "file:///{path}".to_string(),
157            name: "template".to_string(),
158            title: Some("Test Template".to_string()),
159            description: Some("A test resource template".to_string()),
160            mime_type: Some("text/plain".to_string()),
161            icons: Some(vec![Icon {
162                src: "https://example.com/icon.png".to_string(),
163                mime_type: Some("image/png".to_string()),
164                sizes: Some(vec!["48x48".to_string()]),
165            }]),
166        };
167
168        let json = serde_json::to_value(&resource_template).unwrap();
169        assert!(json["icons"].is_array());
170        assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
171        assert_eq!(json["icons"][0]["sizes"][0], "48x48");
172    }
173
174    #[test]
175    fn test_resource_template_without_icons() {
176        let resource_template = RawResourceTemplate {
177            uri_template: "file:///{path}".to_string(),
178            name: "template".to_string(),
179            title: None,
180            description: None,
181            mime_type: None,
182            icons: None,
183        };
184
185        let json = serde_json::to_value(&resource_template).unwrap();
186        assert!(json.get("icons").is_none());
187    }
188}