spreadsheet-mcp 0.10.1

Stateful MCP server for spreadsheet analysis and editing — token-efficient tools for LLM agents to read, profile, edit, and recalculate .xlsx workbooks
Documentation
#![allow(dead_code)]
pub mod builders;
pub mod docker;
pub mod mcp;

use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::Result;
use spreadsheet_mcp::state::AppState;
use spreadsheet_mcp::{OutputProfile, ServerConfig, SpreadsheetServer, TransportKind};
use tempfile::{TempDir, tempdir};
use umya_spreadsheet::{self, Spreadsheet};

const DEFAULT_EXTENSIONS: &[&str] = &["xlsx", "xlsm", "xls", "xlsb"];

#[allow(dead_code)]
pub fn build_workbook<F>(f: F) -> PathBuf
where
    F: FnOnce(&mut Spreadsheet),
{
    let tmp = tempdir().expect("tempdir");
    let path = tmp.path().join("fixture.xlsx");
    write_workbook_to_path(&path, f);
    std::mem::forget(tmp);
    path
}

#[allow(dead_code)]
pub fn write_workbook_to_path<F>(path: &Path, f: F)
where
    F: FnOnce(&mut Spreadsheet),
{
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).expect("create dir");
    }
    let mut book = umya_spreadsheet::new_file();
    f(&mut book);
    umya_spreadsheet::writer::xlsx::write(&book, path).expect("write workbook");
}

pub struct TestWorkspace {
    _tempdir: TempDir,
    root: PathBuf,
}

impl TestWorkspace {
    pub fn new() -> Self {
        let tempdir = tempdir().expect("tempdir");
        let root = tempdir.path().to_path_buf();
        Self {
            _tempdir: tempdir,
            root,
        }
    }

    pub fn root(&self) -> &Path {
        &self.root
    }

    pub fn path(&self, name: &str) -> PathBuf {
        self.root.join(name)
    }

    pub fn create_workbook<F>(&self, name: &str, f: F) -> PathBuf
    where
        F: FnOnce(&mut Spreadsheet),
    {
        let path = self.path(name);
        write_workbook_to_path(&path, f);
        path
    }

    pub fn copy_workbook(&self, src: &Path, name: &str) -> PathBuf {
        let dest = self.path(name);
        std::fs::copy(src, &dest).expect("copy workbook");
        dest
    }

    pub fn config(&self) -> ServerConfig {
        ServerConfig {
            workspace_root: self.root.clone(),
            screenshot_dir: self.root.join("screenshots"),
            path_mappings: Vec::new(),
            cache_capacity: 8,
            supported_extensions: DEFAULT_EXTENSIONS
                .iter()
                .map(|ext| ext.to_string())
                .collect(),
            single_workbook: None,
            enabled_tools: None,
            transport: TransportKind::Http,
            http_bind_address: "127.0.0.1:8079".parse().unwrap(),
            recalc_enabled: false,
            recalc_backend: spreadsheet_mcp::config::RecalcBackendKind::Auto,
            vba_enabled: false,
            max_concurrent_recalcs: 2,
            tool_timeout_ms: Some(30_000),
            max_response_bytes: Some(1_000_000),
            output_profile: OutputProfile::TokenDense,
            max_payload_bytes: Some(65_536),
            max_cells: Some(10_000),
            max_items: Some(500),
            allow_overwrite: false,
        }
    }

    pub fn config_with<F>(&self, configure: F) -> ServerConfig
    where
        F: FnOnce(&mut ServerConfig),
    {
        let mut config = self.config();
        configure(&mut config);
        config
    }

    pub fn app_state(&self) -> Arc<AppState> {
        let config = Arc::new(self.config());
        Arc::new(AppState::new(config))
    }

    pub async fn server(&self) -> Result<SpreadsheetServer> {
        let config = Arc::new(self.config());
        SpreadsheetServer::new(config).await
    }
}

pub fn app_state_with_config(config: ServerConfig) -> Arc<AppState> {
    let config = Arc::new(config);
    Arc::new(AppState::new(config))
}

pub fn touch_file(path: &Path) {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).expect("create dir");
    }
    std::fs::write(path, b"test").expect("write file");
}