mcp_probe_core/messages/
resources.rs

1//! Resource-related message types for MCP resource discovery and access.
2//!
3//! This module provides types for:
4//! - Resource discovery (listing available resources)
5//! - Resource access (reading resource content)
6//! - Resource subscriptions (watching for changes)
7//! - Resource content handling (text, binary, etc.)
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12
13/// Request to list available resources from the server.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct ListResourcesRequest {
16    /// Optional cursor for pagination
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub cursor: Option<String>,
19}
20
21/// Response containing the list of available resources.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct ListResourcesResponse {
24    /// List of available resources
25    pub resources: Vec<Resource>,
26
27    /// Optional cursor for next page of results
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub next_cursor: Option<String>,
30}
31
32/// Resource definition including metadata and access information.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Resource {
35    /// Unique URI identifying the resource
36    pub uri: String,
37
38    /// Human-readable name of the resource
39    pub name: String,
40
41    /// Description of what the resource contains
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub description: Option<String>,
44
45    /// MIME type of the resource content
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub mime_type: Option<String>,
48}
49
50impl Resource {
51    /// Create a new resource definition.
52    pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
53        Self {
54            uri: uri.into(),
55            name: name.into(),
56            description: None,
57            mime_type: None,
58        }
59    }
60
61    /// Set the description for this resource.
62    pub fn with_description(mut self, description: impl Into<String>) -> Self {
63        self.description = Some(description.into());
64        self
65    }
66
67    /// Set the MIME type for this resource.
68    pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
69        self.mime_type = Some(mime_type.into());
70        self
71    }
72}
73
74/// Request to read the content of a specific resource.
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub struct ReadResourceRequest {
77    /// URI of the resource to read
78    pub uri: String,
79}
80
81/// Response containing the content of a resource.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct ReadResourceResponse {
84    /// Content of the resource
85    #[serde(default)]
86    pub contents: Vec<ResourceContent>,
87}
88
89/// Content of a resource.
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(tag = "type")]
92pub enum ResourceContent {
93    /// Text content
94    #[serde(rename = "text")]
95    Text {
96        /// The text content
97        text: String,
98
99        /// URI of the resource
100        uri: String,
101
102        /// MIME type of the content
103        #[serde(rename = "mimeType")]
104        #[serde(skip_serializing_if = "Option::is_none")]
105        mime_type: Option<String>,
106    },
107
108    /// Binary content (base64 encoded)
109    #[serde(rename = "blob")]
110    Blob {
111        /// Base64 encoded binary data
112        blob: String,
113
114        /// URI of the resource
115        uri: String,
116
117        /// MIME type of the content
118        #[serde(rename = "mimeType")]
119        #[serde(skip_serializing_if = "Option::is_none")]
120        mime_type: Option<String>,
121    },
122}
123
124impl ResourceContent {
125    /// Create text content.
126    pub fn text(uri: impl Into<String>, text: impl Into<String>) -> Self {
127        Self::Text {
128            text: text.into(),
129            uri: uri.into(),
130            mime_type: None,
131        }
132    }
133
134    /// Create text content with MIME type.
135    pub fn text_with_mime_type(
136        uri: impl Into<String>,
137        text: impl Into<String>,
138        mime_type: impl Into<String>,
139    ) -> Self {
140        Self::Text {
141            text: text.into(),
142            uri: uri.into(),
143            mime_type: Some(mime_type.into()),
144        }
145    }
146
147    /// Create binary content.
148    pub fn blob(uri: impl Into<String>, blob: impl Into<String>) -> Self {
149        Self::Blob {
150            blob: blob.into(),
151            uri: uri.into(),
152            mime_type: None,
153        }
154    }
155
156    /// Create binary content with MIME type.
157    pub fn blob_with_mime_type(
158        uri: impl Into<String>,
159        blob: impl Into<String>,
160        mime_type: impl Into<String>,
161    ) -> Self {
162        Self::Blob {
163            blob: blob.into(),
164            uri: uri.into(),
165            mime_type: Some(mime_type.into()),
166        }
167    }
168
169    /// Get the URI of this content.
170    pub fn uri(&self) -> &str {
171        match self {
172            Self::Text { uri, .. } => uri,
173            Self::Blob { uri, .. } => uri,
174        }
175    }
176
177    /// Get the MIME type of this content.
178    pub fn mime_type(&self) -> Option<&str> {
179        match self {
180            Self::Text { mime_type, .. } => mime_type.as_deref(),
181            Self::Blob { mime_type, .. } => mime_type.as_deref(),
182        }
183    }
184}
185
186/// Request to subscribe to changes in a resource.
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188pub struct SubscribeRequest {
189    /// URI of the resource to subscribe to
190    pub uri: String,
191}
192
193/// Request to unsubscribe from changes in a resource.
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct UnsubscribeRequest {
196    /// URI of the resource to unsubscribe from
197    pub uri: String,
198}
199
200/// Notification that a resource has been updated.
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct ResourceUpdatedNotification {
203    /// URI of the updated resource
204    pub uri: String,
205
206    /// Additional metadata about the update
207    #[serde(flatten)]
208    pub metadata: HashMap<String, Value>,
209}
210
211impl ResourceUpdatedNotification {
212    /// Create a new resource updated notification.
213    pub fn new(uri: impl Into<String>) -> Self {
214        Self {
215            uri: uri.into(),
216            metadata: HashMap::new(),
217        }
218    }
219
220    /// Add metadata to the notification.
221    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
222        self.metadata.insert(key.into(), value);
223        self
224    }
225}
226
227/// Notification that the list of resources has changed.
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
229pub struct ResourceListChangedNotification {
230    /// Additional metadata about the change
231    #[serde(flatten)]
232    pub metadata: HashMap<String, Value>,
233}
234
235impl ResourceListChangedNotification {
236    /// Create a new resource list changed notification.
237    pub fn new() -> Self {
238        Self::default()
239    }
240
241    /// Add metadata to the notification.
242    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
243        self.metadata.insert(key.into(), value);
244        self
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use serde_json::json;
252
253    #[test]
254    fn test_resource_creation() {
255        let resource = Resource::new("file:///path/to/file.txt", "file.txt")
256            .with_description("A text file")
257            .with_mime_type("text/plain");
258
259        assert_eq!(resource.uri, "file:///path/to/file.txt");
260        assert_eq!(resource.name, "file.txt");
261        assert_eq!(resource.description, Some("A text file".to_string()));
262        assert_eq!(resource.mime_type, Some("text/plain".to_string()));
263    }
264
265    #[test]
266    fn test_list_resources_request() {
267        let request = ListResourcesRequest { cursor: None };
268        let json = serde_json::to_string(&request).unwrap();
269        let deserialized: ListResourcesRequest = serde_json::from_str(&json).unwrap();
270        assert_eq!(request, deserialized);
271    }
272
273    #[test]
274    fn test_read_resource_request() {
275        let request = ReadResourceRequest {
276            uri: "file:///path/to/file.txt".to_string(),
277        };
278
279        let json = serde_json::to_string(&request).unwrap();
280        let deserialized: ReadResourceRequest = serde_json::from_str(&json).unwrap();
281        assert_eq!(request, deserialized);
282    }
283
284    #[test]
285    fn test_resource_content_text() {
286        let content =
287            ResourceContent::text_with_mime_type("file:///test.txt", "Hello world", "text/plain");
288
289        let json = serde_json::to_value(&content).unwrap();
290        assert_eq!(json["type"], "text");
291        assert_eq!(json["text"], "Hello world");
292        assert_eq!(json["mimeType"], "text/plain");
293        assert_eq!(content.uri(), "file:///test.txt");
294        assert_eq!(content.mime_type(), Some("text/plain"));
295    }
296
297    #[test]
298    fn test_resource_content_blob() {
299        let content =
300            ResourceContent::blob_with_mime_type("file:///test.png", "base64data", "image/png");
301
302        let json = serde_json::to_value(&content).unwrap();
303        assert_eq!(json["type"], "blob");
304        assert_eq!(json["blob"], "base64data");
305        assert_eq!(json["mimeType"], "image/png");
306        assert_eq!(content.uri(), "file:///test.png");
307        assert_eq!(content.mime_type(), Some("image/png"));
308    }
309
310    #[test]
311    fn test_resource_updated_notification() {
312        let notification = ResourceUpdatedNotification::new("file:///test.txt")
313            .with_metadata("timestamp", json!("2024-01-01T00:00:00Z"));
314
315        assert_eq!(notification.uri, "file:///test.txt");
316        assert_eq!(
317            notification.metadata.get("timestamp"),
318            Some(&json!("2024-01-01T00:00:00Z"))
319        );
320    }
321}