turul_mcp_builders/traits/
root_traits.rs

1//! Framework traits for MCP root construction
2//!
3//! **IMPORTANT**: These are framework features, NOT part of the MCP specification.
4
5use turul_mcp_protocol::roots::Root;
6use serde_json::Value;
7use std::collections::HashMap;
8
9pub trait HasRootMetadata {
10    /// The root URI (must start with "file://")
11    fn uri(&self) -> &str;
12
13    /// Optional human-readable name
14    fn name(&self) -> Option<&str> {
15        None
16    }
17
18    /// Optional description or additional metadata
19    fn description(&self) -> Option<&str> {
20        None
21    }
22}
23
24/// Trait for root permissions and security
25pub trait HasRootPermissions {
26    /// Check if read access is allowed for this path
27    fn can_read(&self, _path: &str) -> bool {
28        true
29    }
30
31    /// Check if write access is allowed for this path
32    fn can_write(&self, _path: &str) -> bool {
33        false // Default: read-only
34    }
35
36    /// Get maximum depth for directory traversal
37    fn max_depth(&self) -> Option<usize> {
38        None // No limit by default
39    }
40}
41
42/// Trait for root filtering and exclusions
43pub trait HasRootFiltering {
44    /// File extensions to include (None = all)
45    fn allowed_extensions(&self) -> Option<&[String]> {
46        None
47    }
48
49    /// File patterns to exclude (glob patterns)
50    fn excluded_patterns(&self) -> Option<&[String]> {
51        None
52    }
53
54    /// Check if a file should be included
55    fn should_include(&self, path: &str) -> bool {
56        // Default: include everything unless filtered
57        if let Some(patterns) = self.excluded_patterns() {
58            for pattern in patterns {
59                if path.contains(pattern) {
60                    return false;
61                }
62            }
63        }
64
65        if let Some(extensions) = self.allowed_extensions() {
66            if let Some(ext) = path.split('.').next_back() {
67                return extensions.contains(&ext.to_string());
68            }
69            return false;
70        }
71
72        true
73    }
74}
75
76/// Trait for root annotations and custom metadata
77pub trait HasRootAnnotations {
78    /// Get custom metadata
79    fn annotations(&self) -> Option<&HashMap<String, Value>> {
80        None
81    }
82
83    /// Get root-specific tags or labels
84    fn tags(&self) -> Option<&[String]> {
85        None
86    }
87}
88
89/// **Complete MCP Root Creation** - Build secure file system access boundaries.
90///
91/// This trait represents a **complete, working MCP root** that defines secure access
92/// boundaries for file system operations with permissions, filtering, and metadata.
93/// When you implement the required metadata traits, you automatically get
94/// `RootDefinition` for free via blanket implementation.
95///
96/// # What You're Building
97///
98/// A root is a secure file system boundary that:
99/// - Defines accessible file system paths for clients
100/// - Enforces security permissions and access control
101/// - Filters files and directories based on rules
102/// - Provides metadata annotations for client context
103///
104/// # How to Create a Root
105///
106/// Implement these four traits on your struct:
107///
108/// ```rust
109/// # use turul_mcp_protocol::roots::*;
110/// # use turul_mcp_builders::prelude::*;
111/// # use serde_json::{Value, json};
112/// # use std::collections::HashMap;
113///
114/// // This struct will automatically implement RootDefinition!
115/// struct ProjectRoot {
116///     base_path: String,
117///     project_name: String,
118/// }
119///
120/// impl HasRootMetadata for ProjectRoot {
121///     fn uri(&self) -> &str {
122///         &self.base_path
123///     }
124///
125///     fn name(&self) -> Option<&str> {
126///         Some(&self.project_name)
127///     }
128/// }
129///
130/// impl HasRootPermissions for ProjectRoot {
131///     fn can_read(&self, _path: &str) -> bool {
132///         true // Allow reading all files in project
133///     }
134///
135///     fn can_write(&self, path: &str) -> bool {
136///         // Only allow writing to src/ and tests/ directories
137///         path.contains("/src/") || path.contains("/tests/")
138///     }
139///
140///     fn max_depth(&self) -> Option<usize> {
141///         Some(10) // Limit depth to prevent infinite recursion
142///     }
143/// }
144///
145/// impl HasRootFiltering for ProjectRoot {
146///     fn excluded_patterns(&self) -> Option<&[String]> {
147///         static PATTERNS: &[String] = &[];
148///         None // Use default filtering
149///     }
150///
151///     fn should_include(&self, path: &str) -> bool {
152///         // Exclude hidden files and build artifacts
153///         !path.contains("/.") && !path.contains("/target/")
154///     }
155/// }
156///
157/// impl HasRootAnnotations for ProjectRoot {
158///     fn annotations(&self) -> Option<&HashMap<String, Value>> {
159///         // Static annotations for this example
160///         None
161///     }
162/// }
163///
164/// // Now you can use it with the server:
165/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
166/// let root = ProjectRoot {
167///     base_path: "file:///workspace/my-project".to_string(),
168///     project_name: "My Rust Project".to_string(),
169/// };
170///
171/// // The root automatically implements RootDefinition
172/// let protocol_root = root.to_root();
173/// let validation_result = root.validate();
174/// # Ok(())
175/// # }
176/// ```
177///
178/// # Key Benefits
179///
180/// - **Security**: Fine-grained access control for file operations
181/// - **Filtering**: Automatic exclusion of unwanted files/directories
182/// - **Metadata**: Rich annotations for client context
183/// - **MCP Compliant**: Fully compatible with MCP 2025-06-18 specification
184///
185/// # Common Use Cases
186///
187/// - Project workspace boundaries
188/// - Secure document repositories
189/// - Code review access control
190/// - Filtered file system views
191/// - Multi-tenant file access
192pub trait RootDefinition:
193    HasRootMetadata + HasRootPermissions + HasRootFiltering + HasRootAnnotations
194{
195    /// Convert this root definition to a protocol Root
196    fn to_root(&self) -> Root {
197        let mut root = Root::new(self.uri());
198        if let Some(name) = self.name() {
199            root = root.with_name(name);
200        }
201        if let Some(annotations) = self.annotations() {
202            root = root.with_meta(annotations.clone());
203        }
204        root
205    }
206
207    /// Validate this root definition
208    fn validate(&self) -> Result<(), String> {
209        if !self.uri().starts_with("file://") {
210            return Err("Root URI must start with 'file://'".to_string());
211        }
212        Ok(())
213    }
214}
215
216// Blanket implementation: any type implementing the fine-grained traits automatically gets RootDefinition
217impl<T> RootDefinition for T where
218    T: HasRootMetadata + HasRootPermissions + HasRootFiltering + HasRootAnnotations
219{
220}