use crate::common::{
BaseServer, McpContent, McpServerBase, McpTool, McpToolRequest, McpToolResponse,
ServerCapabilities, ServerConfig,
};
use crate::{McpToolsError, Result};
use coderlib::permission::Permission;
use serde_json::json;
use std::path::PathBuf;
use tracing::{debug, error, info};
use uuid::Uuid;
pub struct FileOperationsServer {
base: BaseServer,
}
impl FileOperationsServer {
pub async fn new(config: ServerConfig) -> Result<Self> {
info!("Creating File Operations MCP Server with CoderLib integration enabled");
let base = BaseServer::new(config).await?;
info!("CoderLib dependency successfully enabled - Permission system available");
Ok(Self { base })
}
fn get_file_tools() -> Vec<McpTool> {
vec![
McpTool {
name: "read_file".to_string(),
description: "Read contents of a file".to_string(),
category: "file_operations".to_string(),
requires_permission: true,
permissions: vec!["file_read".to_string()],
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to read"
},
"encoding": {
"type": "string",
"description": "File encoding (default: utf-8)",
"default": "utf-8"
}
},
"required": ["path"]
}),
},
McpTool {
name: "write_file".to_string(),
description: "Write content to a file".to_string(),
category: "file_operations".to_string(),
requires_permission: true,
permissions: vec!["file_write".to_string()],
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to write"
},
"content": {
"type": "string",
"description": "Content to write to the file"
},
"encoding": {
"type": "string",
"description": "File encoding (default: utf-8)",
"default": "utf-8"
},
"create_dirs": {
"type": "boolean",
"description": "Create parent directories if they don't exist",
"default": false
}
},
"required": ["path", "content"]
}),
},
McpTool {
name: "list_directory".to_string(),
description: "List contents of a directory".to_string(),
category: "file_operations".to_string(),
requires_permission: true,
permissions: vec!["directory_list".to_string()],
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory to list"
},
"recursive": {
"type": "boolean",
"description": "List recursively",
"default": false
},
"include_hidden": {
"type": "boolean",
"description": "Include hidden files",
"default": false
}
},
"required": ["path"]
}),
},
McpTool {
name: "create_directory".to_string(),
description: "Create a directory".to_string(),
category: "file_operations".to_string(),
requires_permission: true,
permissions: vec!["directory_create".to_string()],
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory to create"
},
"recursive": {
"type": "boolean",
"description": "Create parent directories if they don't exist",
"default": false
}
},
"required": ["path"]
}),
},
McpTool {
name: "delete_file".to_string(),
description: "Delete a file or directory".to_string(),
category: "file_operations".to_string(),
requires_permission: true,
permissions: vec!["file_delete".to_string()],
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file or directory to delete"
},
"recursive": {
"type": "boolean",
"description": "Delete recursively (for directories)",
"default": false
}
},
"required": ["path"]
}),
},
McpTool {
name: "file_info".to_string(),
description: "Get information about a file or directory".to_string(),
category: "file_operations".to_string(),
requires_permission: true,
permissions: vec!["file_read".to_string()],
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file or directory"
}
},
"required": ["path"]
}),
},
]
}
async fn handle_read_file(&self, request: &McpToolRequest) -> Result<McpToolResponse> {
let path = request
.arguments
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| McpToolsError::Server("Missing 'path' argument".to_string()))?;
let path_buf = PathBuf::from(path);
debug!("Reading file: {}", path);
match tokio::fs::read_to_string(&path_buf).await {
Ok(content) => {
let response_content = vec![
McpContent::text(content),
McpContent::resource_with_type(format!("file://{}", path), "text/plain"),
];
Ok(self
.base
.create_success_response(request.id, response_content))
}
Err(e) => {
error!("Failed to read file {}: {}", path, e);
Ok(self
.base
.create_error_response(request.id, format!("Failed to read file: {}", e)))
}
}
}
async fn handle_write_file(&self, request: &McpToolRequest) -> Result<McpToolResponse> {
let path = request
.arguments
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| McpToolsError::Server("Missing 'path' argument".to_string()))?;
let content = request
.arguments
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| McpToolsError::Server("Missing 'content' argument".to_string()))?;
let create_dirs = request
.arguments
.get("create_dirs")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let path_buf = PathBuf::from(path);
debug!("Writing file: {} (create_dirs: {})", path, create_dirs);
if create_dirs {
if let Some(parent) = path_buf.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|e| {
McpToolsError::Server(format!("Failed to create directories: {}", e))
})?;
}
}
match tokio::fs::write(&path_buf, content).await {
Ok(_) => {
let response_content = vec![
McpContent::text(format!(
"Successfully wrote {} bytes to {}",
content.len(),
path
)),
McpContent::resource_with_type(format!("file://{}", path), "text/plain"),
];
Ok(self
.base
.create_success_response(request.id, response_content))
}
Err(e) => {
error!("Failed to write file {}: {}", path, e);
Ok(self
.base
.create_error_response(request.id, format!("Failed to write file: {}", e)))
}
}
}
async fn handle_list_directory(&self, request: &McpToolRequest) -> Result<McpToolResponse> {
let path = request
.arguments
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| McpToolsError::Server("Missing 'path' argument".to_string()))?;
let recursive = request
.arguments
.get("recursive")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let include_hidden = request
.arguments
.get("include_hidden")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let path_buf = PathBuf::from(path);
debug!(
"Listing directory: {} (recursive: {}, hidden: {})",
path, recursive, include_hidden
);
match self
.list_directory_impl(&path_buf, recursive, include_hidden)
.await
{
Ok(entries) => {
let entries_text = entries.join("\n");
let response_content = vec![
McpContent::text(format!("Directory listing for {}:\n{}", path, entries_text)),
McpContent::resource_with_type(format!("file://{}", path), "inode/directory"),
];
Ok(self
.base
.create_success_response(request.id, response_content))
}
Err(e) => {
error!("Failed to list directory {}: {}", path, e);
Ok(self
.base
.create_error_response(request.id, format!("Failed to list directory: {}", e)))
}
}
}
async fn list_directory_impl(
&self,
path: &PathBuf,
recursive: bool,
include_hidden: bool,
) -> std::result::Result<Vec<String>, std::io::Error> {
let mut entries = Vec::new();
if recursive {
use walkdir::WalkDir;
for entry in WalkDir::new(path) {
let entry = entry?;
let file_name = entry.file_name().to_string_lossy().to_string();
if !include_hidden && file_name.starts_with('.') {
continue;
}
entries.push(entry.path().to_string_lossy().to_string());
}
} else {
let mut dir = tokio::fs::read_dir(path).await?;
while let Some(entry) = dir.next_entry().await? {
let file_name = entry.file_name().to_string_lossy().to_string();
if !include_hidden && file_name.starts_with('.') {
continue;
}
entries.push(entry.path().to_string_lossy().to_string());
}
}
entries.sort();
Ok(entries)
}
}
#[async_trait::async_trait]
impl McpServerBase for FileOperationsServer {
async fn get_capabilities(&self) -> Result<ServerCapabilities> {
let mut capabilities = self.base.get_capabilities().await?;
capabilities.tools = Self::get_file_tools();
capabilities.features.push("file_operations".to_string());
Ok(capabilities)
}
async fn handle_tool_request(&self, request: McpToolRequest) -> Result<McpToolResponse> {
let _tracker = self.base.record_request_start(&request.session_id).await;
debug!("Handling file operation: {}", request.tool);
match request.tool.as_str() {
"read_file" => self.handle_read_file(&request).await,
"write_file" => self.handle_write_file(&request).await,
"list_directory" => self.handle_list_directory(&request).await,
_ => Ok(self
.base
.create_error_response(request.id, format!("Unknown tool: {}", request.tool))),
}
}
async fn get_stats(&self) -> Result<crate::common::ServerStats> {
self.base.get_stats().await
}
async fn initialize(&mut self) -> Result<()> {
info!("Initializing File Operations Server");
Ok(())
}
async fn shutdown(&mut self) -> Result<()> {
info!("Shutting down File Operations Server");
Ok(())
}
}