turul_mcp_protocol_2025_06_18/
roots.rs1use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct Root {
13 pub uri: String,
15 #[serde(skip_serializing_if = "Option::is_none")]
17 pub name: Option<String>,
18 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
20 pub meta: Option<HashMap<String, Value>>,
21}
22
23impl Root {
24 pub fn new(uri: impl Into<String>) -> Self {
25 Self {
26 uri: uri.into(),
27 name: None,
28 meta: None,
29 }
30 }
31
32 pub fn with_name(mut self, name: impl Into<String>) -> Self {
33 self.name = Some(name.into());
34 self
35 }
36
37 pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
38 self.meta = Some(meta);
39 self
40 }
41
42 pub fn validate(&self) -> Result<(), String> {
44 if !self.uri.starts_with("file://") {
45 return Err("Root URI must start with 'file://'".to_string());
46 }
47 Ok(())
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct ListRootsParams {
55 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
57 pub meta: Option<HashMap<String, Value>>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct ListRootsRequest {
64 pub method: String,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub params: Option<ListRootsParams>,
69}
70
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct ListRootsResult {
76 pub roots: Vec<Root>,
78 #[serde(
80 default,
81 skip_serializing_if = "Option::is_none",
82 alias = "_meta",
83 rename = "_meta"
84 )]
85 pub meta: Option<HashMap<String, Value>>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct RootsListChangedParams {
92 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
94 pub meta: Option<HashMap<String, Value>>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct RootsListChangedNotification {
101 pub method: String,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub params: Option<RootsListChangedParams>,
106}
107
108impl ListRootsParams {
109 pub fn new() -> Self {
110 Self {
111 meta: None,
112 }
113 }
114
115 pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
116 self.meta = Some(meta);
117 self
118 }
119}
120
121impl Default for ListRootsParams {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl ListRootsRequest {
128 pub fn new() -> Self {
129 Self {
130 method: "roots/list".to_string(),
131 params: None,
132 }
133 }
134
135 pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
136 self.params = Some(ListRootsParams::new().with_meta(meta));
137 self
138 }
139}
140
141
142impl ListRootsResult {
143 pub fn new(roots: Vec<Root>) -> Self {
144 Self {
145 roots,
146 meta: None,
147 }
148 }
149
150 pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
151 self.meta = Some(meta);
152 self
153 }
154}
155
156impl RootsListChangedParams {
157 pub fn new() -> Self {
158 Self {
159 meta: None,
160 }
161 }
162
163 pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
164 self.meta = Some(meta);
165 self
166 }
167}
168
169impl Default for RootsListChangedParams {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175impl RootsListChangedNotification {
176 pub fn new() -> Self {
177 Self {
178 method: "notifications/roots/list_changed".to_string(),
179 params: None,
180 }
181 }
182
183 pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
184 self.params = Some(RootsListChangedParams::new().with_meta(meta));
185 self
186 }
187}
188
189use crate::traits::*;
191
192impl Params for ListRootsParams {}
193impl Params for RootsListChangedParams {}
194
195impl HasMetaParam for ListRootsParams {
196 fn meta(&self) -> Option<&HashMap<String, Value>> {
197 self.meta.as_ref()
198 }
199}
200
201impl HasMetaParam for RootsListChangedParams {
202 fn meta(&self) -> Option<&HashMap<String, Value>> {
203 self.meta.as_ref()
204 }
205}
206
207impl HasMethod for ListRootsRequest {
208 fn method(&self) -> &str {
209 &self.method
210 }
211}
212
213impl HasParams for ListRootsRequest {
214 fn params(&self) -> Option<&dyn Params> {
215 self.params.as_ref().map(|p| p as &dyn Params)
216 }
217}
218
219impl HasMethod for RootsListChangedNotification {
220 fn method(&self) -> &str {
221 &self.method
222 }
223}
224
225impl HasParams for RootsListChangedNotification {
226 fn params(&self) -> Option<&dyn Params> {
227 self.params.as_ref().map(|p| p as &dyn Params)
228 }
229}
230
231impl HasData for ListRootsResult {
232 fn data(&self) -> HashMap<String, Value> {
233 let mut data = HashMap::new();
234 data.insert("roots".to_string(), serde_json::to_value(&self.roots).unwrap_or(Value::Null));
235 data
236 }
237}
238
239impl HasMeta for ListRootsResult {
240 fn meta(&self) -> Option<HashMap<String, Value>> {
241 self.meta.clone()
242 }
243}
244
245impl RpcResult for ListRootsResult {}
246
247pub trait HasRootMetadata {
253 fn uri(&self) -> &str;
255
256 fn name(&self) -> Option<&str> {
258 None
259 }
260
261 fn description(&self) -> Option<&str> {
263 None
264 }
265}
266
267pub trait HasRootPermissions {
269 fn can_read(&self, _path: &str) -> bool {
271 true
272 }
273
274 fn can_write(&self, _path: &str) -> bool {
276 false }
278
279 fn max_depth(&self) -> Option<usize> {
281 None }
283}
284
285pub trait HasRootFiltering {
287 fn allowed_extensions(&self) -> Option<&[String]> {
289 None
290 }
291
292 fn excluded_patterns(&self) -> Option<&[String]> {
294 None
295 }
296
297 fn should_include(&self, path: &str) -> bool {
299 if let Some(patterns) = self.excluded_patterns() {
301 for pattern in patterns {
302 if path.contains(pattern) {
303 return false;
304 }
305 }
306 }
307
308 if let Some(extensions) = self.allowed_extensions() {
309 if let Some(ext) = path.split('.').last() {
310 return extensions.contains(&ext.to_string());
311 }
312 return false;
313 }
314
315 true
316 }
317}
318
319pub trait HasRootAnnotations {
321 fn annotations(&self) -> Option<&HashMap<String, Value>> {
323 None
324 }
325
326 fn tags(&self) -> Option<&[String]> {
328 None
329 }
330}
331
332pub trait RootDefinition:
334 HasRootMetadata +
335 HasRootPermissions +
336 HasRootFiltering +
337 HasRootAnnotations
338{
339 fn to_root(&self) -> Root {
341 let mut root = Root::new(self.uri());
342 if let Some(name) = self.name() {
343 root = root.with_name(name);
344 }
345 if let Some(annotations) = self.annotations() {
346 root = root.with_meta(annotations.clone());
347 }
348 root
349 }
350
351 fn validate(&self) -> Result<(), String> {
353 if !self.uri().starts_with("file://") {
354 return Err("Root URI must start with 'file://'".to_string());
355 }
356 Ok(())
357 }
358}
359
360impl<T> RootDefinition for T
362where
363 T: HasRootMetadata + HasRootPermissions + HasRootFiltering + HasRootAnnotations
364{}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use serde_json::json;
370
371 #[test]
372 fn test_root_creation() {
373 let mut root = Root::new("file:///home/user/project")
374 .with_name("My Project");
375
376 let meta = HashMap::from([
377 ("version".to_string(), json!("1.0")),
378 ("type".to_string(), json!("workspace")),
379 ]);
380 root = root.with_meta(meta.clone());
381
382 assert_eq!(root.uri, "file:///home/user/project");
383 assert_eq!(root.name, Some("My Project".to_string()));
384 assert_eq!(root.meta, Some(meta));
385 }
386
387 #[test]
388 fn test_root_validation() {
389 let valid_root = Root::new("file:///valid/path");
390 assert!(valid_root.validate().is_ok());
391
392 let invalid_root = Root::new("http://invalid/path");
393 assert!(invalid_root.validate().is_err());
394 }
395
396 #[test]
397 fn test_list_roots_request() {
398 let request = ListRootsRequest::new();
399 assert_eq!(request.method, "roots/list");
400 }
401
402 #[test]
403 fn test_list_roots_result() {
404 let roots = vec![
405 Root::new("file:///path1").with_name("Root 1"),
406 Root::new("file:///path2").with_name("Root 2"),
407 ];
408
409 let result = ListRootsResult::new(roots.clone());
410 assert_eq!(result.roots.len(), 2);
411 assert_eq!(result.roots[0].name, Some("Root 1".to_string()));
412 }
413
414 #[test]
415 fn test_roots_list_changed_notification() {
416 let notification = RootsListChangedNotification::new();
417 assert_eq!(notification.method, "notifications/roots/list_changed");
418 }
419
420 #[test]
421 fn test_serialization() {
422 let root = Root::new("file:///test/path").with_name("Test Root");
423 let json = serde_json::to_string(&root).unwrap();
424 assert!(json.contains("file:///test/path"));
425 assert!(json.contains("Test Root"));
426
427 let parsed: Root = serde_json::from_str(&json).unwrap();
428 assert_eq!(parsed.uri, "file:///test/path");
429 assert_eq!(parsed.name, Some("Test Root".to_string()));
430 }
431
432 #[test]
433 fn test_list_roots_request_matches_typescript_spec() {
434 let mut meta = HashMap::new();
436 meta.insert("requestId".to_string(), json!("req-123"));
437
438 let request = ListRootsRequest::new()
439 .with_meta(meta);
440
441 let json_value = serde_json::to_value(&request).unwrap();
442
443 assert_eq!(json_value["method"], "roots/list");
444 assert!(json_value["params"].is_object());
445 assert_eq!(json_value["params"]["_meta"]["requestId"], "req-123");
446 }
447
448 #[test]
449 fn test_list_roots_result_matches_typescript_spec() {
450 let mut meta = HashMap::new();
452 meta.insert("totalCount".to_string(), json!(2));
453
454 let roots = vec![
455 Root::new("file:///path1").with_name("Root 1"),
456 Root::new("file:///path2").with_name("Root 2"),
457 ];
458
459 let result = ListRootsResult::new(roots)
460 .with_meta(meta);
461
462 let json_value = serde_json::to_value(&result).unwrap();
463
464 assert!(json_value["roots"].is_array());
465 assert_eq!(json_value["roots"].as_array().unwrap().len(), 2);
466 assert_eq!(json_value["roots"][0]["uri"], "file:///path1");
467 assert_eq!(json_value["roots"][0]["name"], "Root 1");
468 assert_eq!(json_value["_meta"]["totalCount"], 2);
469 }
470
471 #[test]
472 fn test_roots_list_changed_notification_matches_typescript_spec() {
473 let mut meta = HashMap::new();
475 meta.insert("timestamp".to_string(), json!("2025-01-01T00:00:00Z"));
476
477 let notification = RootsListChangedNotification::new()
478 .with_meta(meta);
479
480 let json_value = serde_json::to_value(¬ification).unwrap();
481
482 assert_eq!(json_value["method"], "notifications/roots/list_changed");
483 assert!(json_value["params"].is_object());
484 assert_eq!(json_value["params"]["_meta"]["timestamp"], "2025-01-01T00:00:00Z");
485 }
486
487 #[test]
488 fn test_optional_params_serialization() {
489 let request = ListRootsRequest::new();
491 let json_value = serde_json::to_value(&request).unwrap();
492
493 assert_eq!(json_value["method"], "roots/list");
494 assert!(json_value["params"].is_null() || !json_value.as_object().unwrap().contains_key("params"));
496
497 let notification = RootsListChangedNotification::new();
499 let json_value = serde_json::to_value(¬ification).unwrap();
500
501 assert_eq!(json_value["method"], "notifications/roots/list_changed");
502 assert!(json_value["params"].is_null() || !json_value.as_object().unwrap().contains_key("params"));
503 }
504}