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
use crate::config::{OutputProfile, RecalcBackendKind, ServerConfig, TransportKind};
use crate::core;
use crate::core::types::{CellEdit, RecalculateOutcome};
use crate::model::WorkbookId;
use crate::state::AppState;
use crate::tools::filters::WorkbookFilter;
use anyhow::{Result, anyhow};
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;

#[derive(Debug, Default, Clone)]
pub struct StatelessRuntime;

impl StatelessRuntime {
    pub fn normalize_existing_file(&self, path: &Path) -> Result<PathBuf> {
        core::read::normalize_existing_file(path)
    }

    pub fn normalize_destination_path(&self, path: &Path) -> Result<PathBuf> {
        core::read::normalize_destination_path(path)
    }

    pub fn copy_file(&self, source: &Path, dest: &Path) -> Result<u64> {
        fs::copy(source, dest).map_err(Into::into)
    }

    pub fn apply_edits(&self, path: &Path, sheet_name: &str, edits: &[CellEdit]) -> Result<()> {
        core::write::apply_edits_to_file(path, sheet_name, edits)
    }

    pub fn diff_json(&self, original: &Path, modified: &Path) -> Result<Value> {
        core::diff::diff_workbooks_json(original, modified)
    }

    pub async fn recalculate_file(&self, path: &Path) -> Result<RecalculateOutcome> {
        #[cfg(not(feature = "recalc"))]
        {
            let _ = path;
            core::recalc::unavailable()?;
            unreachable!();
        }

        #[cfg(feature = "recalc")]
        {
            let backend = core::recalc::select_backend_from_env()?;
            core::recalc::execute_with_backend(path, Some(30_000), backend).await
        }
    }

    pub async fn open_state_for_file(&self, path: &Path) -> Result<(Arc<AppState>, WorkbookId)> {
        let absolute = self.normalize_existing_file(path)?;
        let config = Arc::new(self.build_cli_config(&absolute));
        let state = Arc::new(AppState::new(config));

        let workbook_list = state.list_workbooks(WorkbookFilter::default())?;
        let workbook_id = workbook_list
            .workbooks
            .first()
            .map(|entry| entry.workbook_id.clone())
            .ok_or_else(|| anyhow!("no workbook found at '{}'", absolute.display()))?;
        Ok((state, workbook_id))
    }

    fn build_cli_config(&self, file: &Path) -> ServerConfig {
        let workspace_root = file
            .parent()
            .map(Path::to_path_buf)
            .unwrap_or_else(|| PathBuf::from("."));
        ServerConfig {
            workspace_root,
            screenshot_dir: PathBuf::from("screenshots"),
            path_mappings: Vec::new(),
            cache_capacity: 2,
            supported_extensions: vec!["xlsx".into(), "xlsm".into(), "xls".into(), "xlsb".into()],
            single_workbook: Some(file.to_path_buf()),
            enabled_tools: None,
            transport: TransportKind::Stdio,
            http_bind_address: "127.0.0.1:8079"
                .parse()
                .expect("hardcoded bind address is valid"),
            recalc_enabled: false,
            recalc_backend: RecalcBackendKind::Auto,
            vba_enabled: false,
            max_concurrent_recalcs: 1,
            tool_timeout_ms: Some(30_000),
            max_response_bytes: Some(1_000_000),
            output_profile: OutputProfile::Verbose,
            max_payload_bytes: Some(65_536),
            max_cells: Some(10_000),
            max_items: Some(500),
            allow_overwrite: true,
        }
    }
}