Skip to main content

prism_mcp_rs/protocol/
roots_types.rs

1//! Roots Types for MCP Protocol (2025-11-25)
2//!
3//! Module provides the Roots feature types for file system access,
4//! allowing servers to request access to specific directories or files.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9// ============================================================================
10// Roots Types (2025-11-25)
11// ============================================================================
12
13/// Represents a root directory or file that the server can operate on.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct Root {
16    /// The URI identifying the root. This *must* start with file:///for now.
17    /// This restriction may be relaxed in future versions of the protocol to allow
18    /// other URI schemes.
19    #[serde(rename = "uri")]
20    pub uri: String,
21
22    /// An optional name for the root. This can be used to provide a human-readable
23    /// identifier for the root, which may be useful for display purposes or for
24    /// referencing the root in other parts of the application.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub name: Option<String>,
27
28    /// Metadata field for future extensions
29    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
30    pub meta: Option<HashMap<String, serde_json::Value>>,
31}
32
33impl Root {
34    /// Create a new Root with just a URI
35    pub fn new(uri: String) -> Self {
36        Self {
37            uri,
38            name: None,
39            meta: None,
40        }
41    }
42
43    /// Create a new Root with a URI and name
44    pub fn with_name(uri: String, name: String) -> Self {
45        Self {
46            uri,
47            name: Some(name),
48            meta: None,
49        }
50    }
51
52    /// Validate that the URI is properly formatted
53    pub fn validate(&self) -> Result<(), String> {
54        if !self.uri.starts_with("file:///") {
55            return Err(format!(
56                "Root URI must start with 'file:///', got: {}",
57                self.uri
58            ));
59        }
60        Ok(())
61    }
62}
63
64/// Request for listing roots (sent from server to client)
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
66pub struct ListRootsRequest {
67    /// Method name (always "roots/list")
68    #[serde(default = "default_roots_list_method")]
69    pub method: String,
70
71    /// Request parameters (usually empty for roots/list)
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub params: Option<serde_json::Value>,
74}
75
76fn default_roots_list_method() -> String {
77    "roots/list".to_string()
78}
79
80impl ListRootsRequest {
81    /// Create a new ListRootsRequest
82    pub fn new() -> Self {
83        Self {
84            method: "roots/list".to_string(),
85            params: None,
86        }
87    }
88}
89
90/// Result of listing roots (sent from client to server)
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub struct ListRootsResult {
93    /// Array of Root objects representing available roots
94    pub roots: Vec<Root>,
95
96    /// Metadata field for future extensions
97    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
98    pub meta: Option<HashMap<String, serde_json::Value>>,
99}
100
101impl ListRootsResult {
102    /// Create a new ListRootsResult with the given roots
103    pub fn new(roots: Vec<Root>) -> Self {
104        Self { roots, meta: None }
105    }
106
107    /// Create an empty result
108    pub fn empty() -> Self {
109        Self {
110            roots: Vec::new(),
111            meta: None,
112        }
113    }
114}
115
116/// Notification that the list of roots has changed
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
118pub struct RootsListChangedNotification {
119    /// Method name (always "notifications/roots/list_changed")
120    #[serde(default = "default_roots_list_changed_method")]
121    pub method: String,
122
123    /// Notification parameters (usually empty)
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub params: Option<serde_json::Value>,
126}
127
128fn default_roots_list_changed_method() -> String {
129    "notifications/roots/list_changed".to_string()
130}
131
132impl RootsListChangedNotification {
133    /// Create a new RootsListChangedNotification
134    pub fn new() -> Self {
135        Self {
136            method: "notifications/roots/list_changed".to_string(),
137            params: None,
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_root_creation() {
148        let root = Root::new("file:///home/user/projects".to_string());
149        assert_eq!(root.uri, "file:///home/user/projects");
150        assert_eq!(root.name, None);
151        assert_eq!(root.meta, None);
152
153        let root_with_name = Root::with_name(
154            "file:///home/user/documents".to_string(),
155            "Documents".to_string(),
156        );
157        assert_eq!(root_with_name.uri, "file:///home/user/documents");
158        assert_eq!(root_with_name.name, Some("Documents".to_string()));
159    }
160
161    #[test]
162    fn test_root_validation() {
163        let valid_root = Root::new("file:///home/user".to_string());
164        assert!(valid_root.validate().is_ok());
165
166        let invalid_root = Root::new("http://example.com".to_string());
167        assert!(invalid_root.validate().is_err());
168    }
169
170    #[test]
171    fn test_root_serialization() {
172        let root = Root::with_name("file:///workspace".to_string(), "Workspace".to_string());
173
174        let json = serde_json::to_value(&root).unwrap();
175        assert_eq!(json["uri"], "file:///workspace");
176        assert_eq!(json["name"], "Workspace");
177        assert!(json.get("_meta").is_none());
178    }
179
180    #[test]
181    fn test_list_roots_request() {
182        let request = ListRootsRequest::new();
183        assert_eq!(request.method, "roots/list");
184        assert!(request.params.is_none());
185
186        let json = serde_json::to_value(&request).unwrap();
187        assert_eq!(json["method"], "roots/list");
188    }
189
190    #[test]
191    fn test_list_roots_result() {
192        let roots = vec![
193            Root::new("file:///home/user".to_string()),
194            Root::with_name("file:///workspace".to_string(), "Work".to_string()),
195        ];
196
197        let result = ListRootsResult::new(roots.clone());
198        assert_eq!(result.roots.len(), 2);
199        assert_eq!(result.roots[0].uri, "file:///home/user");
200        assert_eq!(result.roots[1].name, Some("Work".to_string()));
201
202        let json = serde_json::to_value(&result).unwrap();
203        assert!(json["roots"].is_array());
204        assert_eq!(json["roots"][0]["uri"], "file:///home/user");
205    }
206
207    #[test]
208    fn test_roots_list_changed_notification() {
209        let notification = RootsListChangedNotification::new();
210        assert_eq!(notification.method, "notifications/roots/list_changed");
211        assert!(notification.params.is_none());
212
213        let json = serde_json::to_value(&notification).unwrap();
214        assert_eq!(json["method"], "notifications/roots/list_changed");
215    }
216}