use super::super::{McpToolCall, McpToolResult};
use crate::utils::truncation::format_content_with_line_numbers;
use anyhow::{anyhow, Result};
use serde_json::json;
use std::path::Path;
use tokio::fs as tokio_fs;
fn format_file_content_with_numbers(lines: &[&str], line_range: Option<(usize, i64)>) -> String {
format_content_with_line_numbers(lines, 1, line_range)
}
pub async fn view_file_spec(
call: &McpToolCall,
path: &Path,
line_range: Option<(usize, i64)>,
) -> Result<McpToolResult> {
if !path.exists() {
return Ok(McpToolResult {
tool_name: "text_editor".to_string(),
tool_id: call.tool_id.clone(),
result: json!({
"content": [{
"type": "text",
"text": "File not found"
}],
"isError": true
}),
});
}
if path.is_dir() {
let mut entries = Vec::new();
let read_dir = tokio_fs::read_dir(path)
.await
.map_err(|e| anyhow!("Permission denied. Cannot read directory: {}", e))?;
let mut dir_entries = read_dir;
while let Some(entry) = dir_entries
.next_entry()
.await
.map_err(|e| anyhow!("Error reading directory: {}", e))?
{
let name = entry.file_name().to_string_lossy().to_string();
let is_dir = entry
.file_type()
.await
.map_err(|e| anyhow!("Error reading file type: {}", e))?
.is_dir();
entries.push(if is_dir { format!("{}/", name) } else { name });
}
entries.sort();
let content = entries.join("\n");
return Ok(McpToolResult {
tool_name: "text_editor".to_string(),
tool_id: call.tool_id.clone(),
result: json!({
"content": [{
"type": "text",
"text": content
}],
"isError": false
}),
});
}
if !path.is_file() {
return Ok(McpToolResult {
tool_name: "text_editor".to_string(),
tool_id: call.tool_id.clone(),
result: json!({
"content": [{
"type": "text",
"text": "Path is not a file"
}],
"isError": true
}),
});
}
let metadata = tokio_fs::metadata(path)
.await
.map_err(|e| anyhow!("Permission denied. Cannot read file: {}", e))?;
if metadata.len() > 1024 * 1024 * 5 {
return Ok(McpToolResult {
tool_name: "text_editor".to_string(),
tool_id: call.tool_id.clone(),
result: json!({
"content": [{
"type": "text",
"text": "File is too large (>5MB)"
}],
"isError": true
}),
});
}
let content = tokio_fs::read_to_string(path)
.await
.map_err(|e| anyhow!("Permission denied. Cannot read file: {}", e))?;
let lines: Vec<&str> = content.lines().collect();
let content_with_numbers = format_file_content_with_numbers(&lines, line_range);
if content_with_numbers.starts_with("Start line")
|| content_with_numbers.starts_with("Start line")
{
return Ok(McpToolResult {
tool_name: "text_editor".to_string(),
tool_id: call.tool_id.clone(),
result: json!({
"content": [{
"type": "text",
"text": content_with_numbers
}],
"isError": true
}),
});
}
Ok(McpToolResult {
tool_name: "text_editor".to_string(),
tool_id: call.tool_id.clone(),
result: json!({
"content": [{
"type": "text",
"text": content_with_numbers
}],
"isError": false
}),
})
}
pub async fn create_file_spec(
call: &McpToolCall,
path: &Path,
content: &str,
) -> Result<McpToolResult> {
if path.exists() {
return Ok(McpToolResult::error(
call.tool_name.clone(),
call.tool_id.clone(),
"File already exists".to_string(),
));
}
if let Some(parent) = path.parent() {
if !parent.exists() {
tokio_fs::create_dir_all(parent)
.await
.map_err(|e| anyhow!("Permission denied. Cannot create directories: {}", e))?;
}
}
tokio_fs::write(path, content)
.await
.map_err(|e| anyhow!("Permission denied. Cannot write to file: {}", e))?;
Ok(McpToolResult {
tool_name: "text_editor".to_string(),
tool_id: call.tool_id.clone(),
result: json!({
"content": format!("File created successfully with {} bytes", content.len()),
"path": path.to_string_lossy(),
"size": content.len()
}),
})
}
pub async fn view_many_files_spec(call: &McpToolCall, paths: &[String]) -> Result<McpToolResult> {
let mut result_parts = Vec::new();
let mut success_count = 0;
for path_str in paths {
let path = Path::new(&path_str);
let path_display = path.display().to_string();
result_parts.push(path_display.clone());
if !path.exists() {
result_parts.push("✗ File does not exist".to_string());
result_parts.push("".to_string()); continue;
}
if path.is_dir() {
let mut entries = Vec::new();
if let Ok(mut read_dir) = tokio_fs::read_dir(path).await {
while let Ok(Some(entry)) = read_dir.next_entry().await {
let name = entry.file_name().to_string_lossy().to_string();
if let Ok(file_type) = entry.file_type().await {
entries.push(if file_type.is_dir() {
format!("{}/", name)
} else {
name
});
}
}
entries.sort();
result_parts.push(entries.join("\n"));
} else {
result_parts.push("✗ Permission denied. Cannot read directory".to_string());
}
result_parts.push("".to_string()); continue;
}
if !path.is_file() {
result_parts.push("✗ Path is not a file".to_string());
result_parts.push("".to_string()); continue;
}
let _metadata = match tokio_fs::metadata(path).await {
Ok(meta) => {
if meta.len() > 1024 * 1024 * 5 {
result_parts.push("✗ File is too large (>5MB)".to_string());
result_parts.push("".to_string()); continue;
}
meta
}
Err(_) => {
result_parts.push("✗ Permission denied. Cannot read file".to_string());
result_parts.push("".to_string()); continue;
}
};
if let Ok(sample) = tokio_fs::read(&path).await {
let sample_size = sample.len().min(512);
let null_count = sample[..sample_size].iter().filter(|&&b| b == 0).count();
if null_count > sample_size / 10 {
result_parts.push("✗ Binary file skipped".to_string());
result_parts.push("".to_string()); continue;
}
}
let content = match tokio_fs::read_to_string(path).await {
Ok(content) => content,
Err(_) => {
result_parts.push("✗ Permission denied. Cannot read file".to_string());
result_parts.push("".to_string()); continue;
}
};
let lines: Vec<&str> = content.lines().collect();
let content_with_numbers = format_file_content_with_numbers(&lines, None);
result_parts.push(content_with_numbers);
result_parts.push("".to_string()); success_count += 1;
}
if result_parts.last() == Some(&"".to_string()) {
result_parts.pop();
}
let final_content = result_parts.join("\n");
Ok(McpToolResult {
tool_name: "text_editor".to_string(),
tool_id: call.tool_id.clone(),
result: json!({
"content": [{
"type": "text",
"text": final_content
}],
"isError": success_count == 0
}),
})
}
pub async fn view_many_files(call: &McpToolCall, paths: &[String]) -> Result<McpToolResult> {
let mut result_parts = Vec::new();
let mut success_count = 0;
for path_str in paths {
let path = Path::new(&path_str);
let path_display = path.display().to_string();
result_parts.push(path_display.clone());
if !path.exists() {
result_parts.push("✗ File does not exist".to_string());
result_parts.push("".to_string()); continue;
}
if path.is_dir() {
let mut entries = Vec::new();
if let Ok(mut read_dir) = tokio_fs::read_dir(path).await {
while let Ok(Some(entry)) = read_dir.next_entry().await {
let name = entry.file_name().to_string_lossy().to_string();
if let Ok(file_type) = entry.file_type().await {
entries.push(if file_type.is_dir() {
format!("{}/", name)
} else {
name
});
}
}
entries.sort();
result_parts.push(entries.join("\n"));
} else {
result_parts.push("✗ Permission denied. Cannot read directory".to_string());
}
result_parts.push("".to_string()); continue;
}
if !path.is_file() {
result_parts.push("✗ Path is not a file".to_string());
result_parts.push("".to_string()); continue;
}
let _metadata = match tokio_fs::metadata(path).await {
Ok(meta) => {
if meta.len() > 1024 * 1024 * 5 {
result_parts.push("✗ File is too large (>5MB)".to_string());
result_parts.push("".to_string()); continue;
}
meta
}
Err(_) => {
result_parts.push("✗ Permission denied. Cannot read file".to_string());
result_parts.push("".to_string()); continue;
}
};
if let Ok(sample) = tokio_fs::read(&path).await {
let sample_size = sample.len().min(512);
let null_count = sample[..sample_size].iter().filter(|&&b| b == 0).count();
if null_count > sample_size / 10 {
result_parts.push("✗ Binary file skipped".to_string());
result_parts.push("".to_string()); continue;
}
}
let content = match tokio_fs::read_to_string(path).await {
Ok(content) => content,
Err(_) => {
result_parts.push("✗ Permission denied. Cannot read file".to_string());
result_parts.push("".to_string()); continue;
}
};
let lines: Vec<&str> = content.lines().collect();
let content_with_numbers = format_file_content_with_numbers(&lines, None);
result_parts.push(content_with_numbers);
result_parts.push("".to_string()); success_count += 1;
}
if result_parts.last() == Some(&"".to_string()) {
result_parts.pop();
}
let final_content = result_parts.join("\n");
Ok(McpToolResult {
tool_name: "view_many".to_string(),
tool_id: call.tool_id.clone(),
result: json!({
"content": [{
"type": "text",
"text": final_content
}],
"isError": success_count == 0
}),
})
}