excel-mcp-server 0.1.0

An MCP server that exposes Excel manipulation tools over stdio and streamable HTTP transports
Documentation
use std::path::Path;
use std::time::Instant;

use super::common::workbook_not_found;
use crate::engines::zavora;
use crate::store::{WorkbookEntry, WorkbookStore};
use crate::types::inputs::*;
use crate::types::responses::*;

pub fn create_workbook(store: &mut WorkbookStore) -> Result<String, anyhow::Error> {
    if store.is_full() {
        return Ok(error(
            ErrorCategory::CapacityExceeded,
            "Workbook store is at maximum capacity",
            "Save and close an existing workbook first to free a slot.",
        ));
    }
    let wb = zavora::create_workbook();
    let entry = WorkbookEntry {
        id: String::new(),
        data: wb,
        read_only: false,
        last_access: Instant::now(),
    };
    let id = store.insert(entry).map_err(|e| anyhow::anyhow!("{}", e))?;
    let info = WorkbookInfo {
        workbook_id: id,
        engine: "zavora-xlsx".to_string(),
        sheets: vec![SheetSummary {
            name: "Sheet1".to_string(),
            dimensions: None,
            row_count: None,
            col_count: None,
        }],
    };
    Ok(success("Workbook created successfully", info))
}

pub fn open_workbook(
    store: &mut WorkbookStore,
    input: OpenWorkbookInput,
) -> Result<String, anyhow::Error> {
    if store.is_full() {
        return Ok(error(
            ErrorCategory::CapacityExceeded,
            "Workbook store is at maximum capacity",
            "Save and close an existing workbook first to free a slot.",
        ));
    }
    let path = Path::new(&input.file_path);
    if !path.exists() {
        return Ok(error(
            ErrorCategory::NotFound,
            &format!("File not found: {}", input.file_path),
            "Check the file path and try again.",
        ));
    }
    let wb = match zavora::open_workbook(&input.file_path, input.read_only) {
        Ok(wb) => wb,
        Err(e) => {
            return Ok(error(
                ErrorCategory::IoError,
                &format!("Failed to open file: {e}"),
                "Check the file is a valid Excel file and try again.",
            ))
        }
    };
    let sheets = zavora::sheet_summaries(&wb);
    let mode = if input.read_only { "read-only" } else { "edit" };
    let entry = WorkbookEntry {
        id: String::new(),
        data: wb,
        read_only: input.read_only,
        last_access: Instant::now(),
    };
    let id = store.insert(entry).map_err(|e| anyhow::anyhow!("{}", e))?;
    Ok(success(
        &format!("Workbook opened in {mode} mode"),
        WorkbookInfo {
            workbook_id: id,
            engine: "zavora-xlsx".to_string(),
            sheets,
        },
    ))
}

pub fn save_workbook(
    store: &mut WorkbookStore,
    input: SaveWorkbookInput,
) -> Result<String, anyhow::Error> {
    let entry = match store.get_mut(&input.workbook_id) {
        Some(e) => e,
        None => return Ok(workbook_not_found(store, &input.workbook_id)),
    };
    if entry.read_only {
        return Ok(error(
            ErrorCategory::EngineUnsupported,
            "Read-only workbooks cannot be saved",
            "Reopen the file in edit mode (read_only: false) to make changes and save.",
        ));
    }
    match zavora::save_workbook(&mut entry.data, &input.file_path) {
        Ok(()) => Ok(success_no_data(&format!(
            "Workbook saved to {}",
            input.file_path
        ))),
        Err(e) => Ok(error(
            ErrorCategory::IoError,
            &format!("Failed to save: {e}"),
            "Check the file path is writable.",
        )),
    }
}

pub fn close_workbook(
    store: &mut WorkbookStore,
    input: CloseWorkbookInput,
) -> Result<String, anyhow::Error> {
    match store.remove(&input.workbook_id) {
        Some(_) => Ok(success_no_data(&format!(
            "Workbook '{}' closed successfully",
            input.workbook_id
        ))),
        None => Ok(workbook_not_found(store, &input.workbook_id)),
    }
}