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}