mcp_tools/servers/
file_operations.rs

1//! File Operations MCP Server
2//!
3//! Provides secure file system operations through MCP protocol,
4//! powered by CoderLib's permission system and file tools.
5
6use crate::common::{
7    BaseServer, McpContent, McpServerBase, McpTool, McpToolRequest, McpToolResponse,
8    ServerCapabilities, ServerConfig,
9};
10use crate::{McpToolsError, Result};
11// CoderLib integration enabled - ready for advanced tool integration
12use coderlib::permission::Permission;
13use serde_json::json;
14use std::path::PathBuf;
15use tracing::{debug, error, info};
16use uuid::Uuid;
17
18/// File Operations MCP Server
19/// Now with CoderLib integration enabled for advanced permission system
20pub struct FileOperationsServer {
21    base: BaseServer,
22    // CoderLib components can be added here when needed
23}
24
25impl FileOperationsServer {
26    /// Create new file operations server
27    pub async fn new(config: ServerConfig) -> Result<Self> {
28        info!("Creating File Operations MCP Server with CoderLib integration enabled");
29
30        let base = BaseServer::new(config).await?;
31
32        // CoderLib is now available for advanced permission system integration
33        info!("CoderLib dependency successfully enabled - Permission system available");
34
35        Ok(Self { base })
36    }
37
38    /// Get available file operation tools
39    fn get_file_tools() -> Vec<McpTool> {
40        vec![
41            McpTool {
42                name: "read_file".to_string(),
43                description: "Read contents of a file".to_string(),
44                category: "file_operations".to_string(),
45                requires_permission: true,
46                permissions: vec!["file_read".to_string()],
47                input_schema: json!({
48                    "type": "object",
49                    "properties": {
50                        "path": {
51                            "type": "string",
52                            "description": "Path to the file to read"
53                        },
54                        "encoding": {
55                            "type": "string",
56                            "description": "File encoding (default: utf-8)",
57                            "default": "utf-8"
58                        }
59                    },
60                    "required": ["path"]
61                }),
62            },
63            McpTool {
64                name: "write_file".to_string(),
65                description: "Write content to a file".to_string(),
66                category: "file_operations".to_string(),
67                requires_permission: true,
68                permissions: vec!["file_write".to_string()],
69                input_schema: json!({
70                    "type": "object",
71                    "properties": {
72                        "path": {
73                            "type": "string",
74                            "description": "Path to the file to write"
75                        },
76                        "content": {
77                            "type": "string",
78                            "description": "Content to write to the file"
79                        },
80                        "encoding": {
81                            "type": "string",
82                            "description": "File encoding (default: utf-8)",
83                            "default": "utf-8"
84                        },
85                        "create_dirs": {
86                            "type": "boolean",
87                            "description": "Create parent directories if they don't exist",
88                            "default": false
89                        }
90                    },
91                    "required": ["path", "content"]
92                }),
93            },
94            McpTool {
95                name: "list_directory".to_string(),
96                description: "List contents of a directory".to_string(),
97                category: "file_operations".to_string(),
98                requires_permission: true,
99                permissions: vec!["directory_list".to_string()],
100                input_schema: json!({
101                    "type": "object",
102                    "properties": {
103                        "path": {
104                            "type": "string",
105                            "description": "Path to the directory to list"
106                        },
107                        "recursive": {
108                            "type": "boolean",
109                            "description": "List recursively",
110                            "default": false
111                        },
112                        "include_hidden": {
113                            "type": "boolean",
114                            "description": "Include hidden files",
115                            "default": false
116                        }
117                    },
118                    "required": ["path"]
119                }),
120            },
121            McpTool {
122                name: "create_directory".to_string(),
123                description: "Create a directory".to_string(),
124                category: "file_operations".to_string(),
125                requires_permission: true,
126                permissions: vec!["directory_create".to_string()],
127                input_schema: json!({
128                    "type": "object",
129                    "properties": {
130                        "path": {
131                            "type": "string",
132                            "description": "Path to the directory to create"
133                        },
134                        "recursive": {
135                            "type": "boolean",
136                            "description": "Create parent directories if they don't exist",
137                            "default": false
138                        }
139                    },
140                    "required": ["path"]
141                }),
142            },
143            McpTool {
144                name: "delete_file".to_string(),
145                description: "Delete a file or directory".to_string(),
146                category: "file_operations".to_string(),
147                requires_permission: true,
148                permissions: vec!["file_delete".to_string()],
149                input_schema: json!({
150                    "type": "object",
151                    "properties": {
152                        "path": {
153                            "type": "string",
154                            "description": "Path to the file or directory to delete"
155                        },
156                        "recursive": {
157                            "type": "boolean",
158                            "description": "Delete recursively (for directories)",
159                            "default": false
160                        }
161                    },
162                    "required": ["path"]
163                }),
164            },
165            McpTool {
166                name: "file_info".to_string(),
167                description: "Get information about a file or directory".to_string(),
168                category: "file_operations".to_string(),
169                requires_permission: true,
170                permissions: vec!["file_read".to_string()],
171                input_schema: json!({
172                    "type": "object",
173                    "properties": {
174                        "path": {
175                            "type": "string",
176                            "description": "Path to the file or directory"
177                        }
178                    },
179                    "required": ["path"]
180                }),
181            },
182        ]
183    }
184
185    /// Handle read file request
186    async fn handle_read_file(&self, request: &McpToolRequest) -> Result<McpToolResponse> {
187        let path = request
188            .arguments
189            .get("path")
190            .and_then(|v| v.as_str())
191            .ok_or_else(|| McpToolsError::Server("Missing 'path' argument".to_string()))?;
192
193        let path_buf = PathBuf::from(path);
194
195        // TODO: Check permissions when coderlib is available
196        // self.base.check_tool_permission(
197        //     &request.session_id,
198        //     "read_file",
199        //     Permission::FileRead,
200        // ).await?;
201
202        debug!("Reading file: {}", path);
203
204        // Use standard file operations
205        match tokio::fs::read_to_string(&path_buf).await {
206            Ok(content) => {
207                let response_content = vec![
208                    McpContent::text(content),
209                    McpContent::resource_with_type(format!("file://{}", path), "text/plain"),
210                ];
211
212                Ok(self
213                    .base
214                    .create_success_response(request.id, response_content))
215            }
216            Err(e) => {
217                error!("Failed to read file {}: {}", path, e);
218                Ok(self
219                    .base
220                    .create_error_response(request.id, format!("Failed to read file: {}", e)))
221            }
222        }
223    }
224
225    /// Handle write file request
226    async fn handle_write_file(&self, request: &McpToolRequest) -> Result<McpToolResponse> {
227        let path = request
228            .arguments
229            .get("path")
230            .and_then(|v| v.as_str())
231            .ok_or_else(|| McpToolsError::Server("Missing 'path' argument".to_string()))?;
232
233        let content = request
234            .arguments
235            .get("content")
236            .and_then(|v| v.as_str())
237            .ok_or_else(|| McpToolsError::Server("Missing 'content' argument".to_string()))?;
238
239        let create_dirs = request
240            .arguments
241            .get("create_dirs")
242            .and_then(|v| v.as_bool())
243            .unwrap_or(false);
244
245        let path_buf = PathBuf::from(path);
246
247        // TODO: Check permissions when coderlib is available
248        // self.base.check_tool_permission(
249        //     &request.session_id,
250        //     "write_file",
251        //     Permission::FileWrite,
252        // ).await?;
253
254        debug!("Writing file: {} (create_dirs: {})", path, create_dirs);
255
256        // Create parent directories if requested
257        if create_dirs {
258            if let Some(parent) = path_buf.parent() {
259                tokio::fs::create_dir_all(parent).await.map_err(|e| {
260                    McpToolsError::Server(format!("Failed to create directories: {}", e))
261                })?;
262            }
263        }
264
265        // Use standard file operations
266        match tokio::fs::write(&path_buf, content).await {
267            Ok(_) => {
268                let response_content = vec![
269                    McpContent::text(format!(
270                        "Successfully wrote {} bytes to {}",
271                        content.len(),
272                        path
273                    )),
274                    McpContent::resource_with_type(format!("file://{}", path), "text/plain"),
275                ];
276
277                Ok(self
278                    .base
279                    .create_success_response(request.id, response_content))
280            }
281            Err(e) => {
282                error!("Failed to write file {}: {}", path, e);
283                Ok(self
284                    .base
285                    .create_error_response(request.id, format!("Failed to write file: {}", e)))
286            }
287        }
288    }
289
290    /// Handle list directory request
291    async fn handle_list_directory(&self, request: &McpToolRequest) -> Result<McpToolResponse> {
292        let path = request
293            .arguments
294            .get("path")
295            .and_then(|v| v.as_str())
296            .ok_or_else(|| McpToolsError::Server("Missing 'path' argument".to_string()))?;
297
298        let recursive = request
299            .arguments
300            .get("recursive")
301            .and_then(|v| v.as_bool())
302            .unwrap_or(false);
303
304        let include_hidden = request
305            .arguments
306            .get("include_hidden")
307            .and_then(|v| v.as_bool())
308            .unwrap_or(false);
309
310        let path_buf = PathBuf::from(path);
311
312        // TODO: Check permissions when coderlib is available
313        // self.base.check_tool_permission(
314        //     &request.session_id,
315        //     "list_directory",
316        //     Permission::DirectoryList,
317        // ).await?;
318
319        debug!(
320            "Listing directory: {} (recursive: {}, hidden: {})",
321            path, recursive, include_hidden
322        );
323
324        // Use standard directory operations
325        match self
326            .list_directory_impl(&path_buf, recursive, include_hidden)
327            .await
328        {
329            Ok(entries) => {
330                let entries_text = entries.join("\n");
331                let response_content = vec![
332                    McpContent::text(format!("Directory listing for {}:\n{}", path, entries_text)),
333                    McpContent::resource_with_type(format!("file://{}", path), "inode/directory"),
334                ];
335
336                Ok(self
337                    .base
338                    .create_success_response(request.id, response_content))
339            }
340            Err(e) => {
341                error!("Failed to list directory {}: {}", path, e);
342                Ok(self
343                    .base
344                    .create_error_response(request.id, format!("Failed to list directory: {}", e)))
345            }
346        }
347    }
348
349    /// Implementation for listing directory contents
350    async fn list_directory_impl(
351        &self,
352        path: &PathBuf,
353        recursive: bool,
354        include_hidden: bool,
355    ) -> std::result::Result<Vec<String>, std::io::Error> {
356        let mut entries = Vec::new();
357
358        if recursive {
359            // Use walkdir for recursive listing
360            use walkdir::WalkDir;
361            for entry in WalkDir::new(path) {
362                let entry = entry?;
363                let file_name = entry.file_name().to_string_lossy().to_string();
364
365                if !include_hidden && file_name.starts_with('.') {
366                    continue;
367                }
368
369                entries.push(entry.path().to_string_lossy().to_string());
370            }
371        } else {
372            // Simple directory listing
373            let mut dir = tokio::fs::read_dir(path).await?;
374            while let Some(entry) = dir.next_entry().await? {
375                let file_name = entry.file_name().to_string_lossy().to_string();
376
377                if !include_hidden && file_name.starts_with('.') {
378                    continue;
379                }
380
381                entries.push(entry.path().to_string_lossy().to_string());
382            }
383        }
384
385        entries.sort();
386        Ok(entries)
387    }
388}
389
390#[async_trait::async_trait]
391impl McpServerBase for FileOperationsServer {
392    async fn get_capabilities(&self) -> Result<ServerCapabilities> {
393        let mut capabilities = self.base.get_capabilities().await?;
394        capabilities.tools = Self::get_file_tools();
395        capabilities.features.push("file_operations".to_string());
396        Ok(capabilities)
397    }
398
399    async fn handle_tool_request(&self, request: McpToolRequest) -> Result<McpToolResponse> {
400        let _tracker = self.base.record_request_start(&request.session_id).await;
401
402        debug!("Handling file operation: {}", request.tool);
403
404        match request.tool.as_str() {
405            "read_file" => self.handle_read_file(&request).await,
406            "write_file" => self.handle_write_file(&request).await,
407            "list_directory" => self.handle_list_directory(&request).await,
408            // TODO: Implement remaining tools
409            _ => Ok(self
410                .base
411                .create_error_response(request.id, format!("Unknown tool: {}", request.tool))),
412        }
413    }
414
415    async fn get_stats(&self) -> Result<crate::common::ServerStats> {
416        self.base.get_stats().await
417    }
418
419    async fn initialize(&mut self) -> Result<()> {
420        info!("Initializing File Operations Server");
421        // Additional file server initialization if needed
422        Ok(())
423    }
424
425    async fn shutdown(&mut self) -> Result<()> {
426        info!("Shutting down File Operations Server");
427        // Additional file server cleanup if needed
428        Ok(())
429    }
430}