aether-mcp-servers 0.4.16

Built-in MCP tool servers for the Aether AI agent framework
Documentation
use std::io::ErrorKind;
use std::path::{Path, PathBuf};

use thiserror::Error;
use tokio::fs::{create_dir_all, write};

#[derive(Debug, Error)]
pub enum FileError {
    #[error("File does not exist: {path}")]
    NotFound { path: String },

    #[error("Failed to read file {path}: {reason}")]
    ReadFailed { path: String, reason: String },

    #[error("Failed to write to file {path}: {reason}")]
    WriteFailed { path: String, reason: String },

    #[error("Failed to create directories for {path}: {reason}")]
    CreateDirFailed { path: String, reason: String },

    #[error("Invalid offset for file {path}: offset must be 1-indexed (start from 1)")]
    InvalidOffset { path: String },

    #[error("String replacement failed for file {path}: string '{pattern}' not found")]
    PatternNotFound { path: String, pattern: String },

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

pub struct WriteTextFileResult {
    pub path: PathBuf,
    pub bytes_written: usize,
}

pub struct EditTextFileResult {
    pub path: PathBuf,
    pub original_content: String,
    pub updated_content: String,
    pub replacements_made: usize,
}

pub async fn read_text_file(path: &Path) -> Result<String, FileError> {
    tokio::fs::read_to_string(path).await.map_err(|error| match error.kind() {
        ErrorKind::NotFound => FileError::NotFound { path: display_path(path) },
        _ => FileError::ReadFailed { path: display_path(path), reason: error.to_string() },
    })
}

pub async fn write_text_file(path: &Path, content: &str) -> Result<WriteTextFileResult, FileError> {
    if let Some(parent) = path.parent()
        && let Err(error) = create_dir_all(parent).await
    {
        return Err(FileError::CreateDirFailed { path: display_path(path), reason: error.to_string() });
    }

    if let Err(error) = write(path, content).await {
        return Err(FileError::WriteFailed { path: display_path(path), reason: error.to_string() });
    }

    Ok(WriteTextFileResult { path: path.to_path_buf(), bytes_written: content.len() })
}

pub async fn edit_text_file(
    path: &Path,
    old_string: &str,
    new_string: &str,
    replace_all: bool,
) -> Result<EditTextFileResult, FileError> {
    let original_content = read_text_file(path).await?;
    let (updated_content, replacements_made) = if replace_all {
        let count = original_content.matches(old_string).count();
        (original_content.replace(old_string, new_string), count)
    } else if original_content.contains(old_string) {
        (original_content.replacen(old_string, new_string, 1), 1)
    } else {
        (original_content.clone(), 0)
    };

    if replacements_made == 0 {
        return Err(FileError::PatternNotFound { path: display_path(path), pattern: old_string.to_string() });
    }

    if let Err(error) = write(path, &updated_content).await {
        return Err(FileError::WriteFailed { path: display_path(path), reason: error.to_string() });
    }

    Ok(EditTextFileResult { path: path.to_path_buf(), original_content, updated_content, replacements_made })
}

fn display_path(path: &Path) -> String {
    path.to_string_lossy().into_owned()
}