1use crate::common::{
7 BaseServer, McpContent, McpServerBase, McpTool, McpToolRequest, McpToolResponse,
8 ServerCapabilities, ServerConfig,
9};
10use crate::{McpToolsError, Result};
11use coderlib::permission::Permission;
13use serde_json::json;
14use std::path::PathBuf;
15use tracing::{debug, error, info};
16use uuid::Uuid;
17
18pub struct FileOperationsServer {
21 base: BaseServer,
22 }
24
25impl FileOperationsServer {
26 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 info!("CoderLib dependency successfully enabled - Permission system available");
34
35 Ok(Self { base })
36 }
37
38 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 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 debug!("Reading file: {}", path);
203
204 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 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 debug!("Writing file: {} (create_dirs: {})", path, create_dirs);
255
256 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 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 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 debug!(
320 "Listing directory: {} (recursive: {}, hidden: {})",
321 path, recursive, include_hidden
322 );
323
324 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 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::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 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 _ => 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 Ok(())
423 }
424
425 async fn shutdown(&mut self) -> Result<()> {
426 info!("Shutting down File Operations Server");
427 Ok(())
429 }
430}