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 std::path::PathBuf;
use std::sync::Arc;

use anyhow::Result;
use rmcp::ServerHandler;
use spreadsheet_mcp::tools::vba::{VbaModuleSourceParams, VbaProjectSummaryParams};
use spreadsheet_mcp::tools::{ListWorkbooksParams, list_workbooks};
use spreadsheet_mcp::{SpreadsheetServer, tools};

mod support;

#[tokio::test(flavor = "current_thread")]
async fn vba_tools_parse_xlsm_fixture() -> Result<()> {
    let workspace = support::TestWorkspace::new();
    let fixture =
        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/test_files/vba_minimal.xlsm");
    workspace.copy_workbook(&fixture, "macro.xlsm");

    let config = workspace.config_with(|cfg| {
        cfg.vba_enabled = true;
        if !cfg.supported_extensions.iter().any(|ext| ext == "xlsm") {
            cfg.supported_extensions.push("xlsm".to_string());
        }
    });
    let state = support::app_state_with_config(config);

    let list = list_workbooks(
        state.clone(),
        ListWorkbooksParams {
            slug_prefix: None,
            folder: None,
            path_glob: None,
            limit: None,
            offset: None,
            include_paths: None,
        },
    )
    .await?;
    assert_eq!(list.workbooks.len(), 1);
    let workbook_id = list.workbooks[0].workbook_id.clone();

    let summary = tools::vba::vba_project_summary(
        state.clone(),
        VbaProjectSummaryParams {
            workbook_or_fork_id: workbook_id.clone(),
            max_modules: None,
            include_references: Some(false),
        },
    )
    .await?;
    assert!(summary.has_vba);
    assert!(!summary.modules.is_empty());

    let module_name = summary.modules[0].name.clone();
    let source = tools::vba::vba_module_source(
        state,
        VbaModuleSourceParams {
            workbook_or_fork_id: workbook_id,
            module_name: module_name.clone(),
            offset_lines: 0,
            limit_lines: 20,
        },
    )
    .await?;
    assert_eq!(source.module_name, module_name);
    assert!(!source.source.trim().is_empty());

    Ok(())
}

#[tokio::test(flavor = "current_thread")]
async fn server_instructions_reflect_vba_flag() -> Result<()> {
    let workspace = support::TestWorkspace::new();

    let disabled_config = workspace.config_with(|cfg| {
        cfg.vba_enabled = false;
    });
    let disabled = SpreadsheetServer::new(Arc::new(disabled_config)).await?;
    let disabled_info = disabled.get_info();
    let disabled_instructions = disabled_info.instructions.unwrap_or_default();
    assert!(disabled_instructions.contains("VBA tools disabled"));

    let enabled_config = workspace.config_with(|cfg| {
        cfg.vba_enabled = true;
    });
    let enabled = SpreadsheetServer::new(Arc::new(enabled_config)).await?;
    let enabled_info = enabled.get_info();
    let enabled_instructions = enabled_info.instructions.unwrap_or_default();
    assert!(enabled_instructions.contains("vba_project_summary"));

    Ok(())
}