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