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 anyhow::{Context, Result, bail};
use std::path::Path;
use tokio::process::Command;
use tokio::sync::OnceCell;

const IMAGE_NAME: &str = "spreadsheet-mcp-full";
const IMAGE_TAG: &str = "test";

static IMAGE_BUILT: OnceCell<String> = OnceCell::const_new();

pub fn image_tag() -> String {
    format!("{}:{}", IMAGE_NAME, IMAGE_TAG)
}

async fn build_image(tag: &str) -> Result<()> {
    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let workspace_root = manifest_dir
        .parent()
        .and_then(|p| p.parent())
        .expect("workspace root is two levels above crate manifest");
    let dockerfile_path = workspace_root.join("Dockerfile.full");

    let output = Command::new("docker")
        .args([
            "build",
            "-f",
            dockerfile_path.to_str().unwrap(),
            "-t",
            tag,
            "-q",
            workspace_root.to_str().unwrap(),
        ])
        .output()
        .await
        .context("failed to run docker build")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("docker build failed: {}", stderr);
    }

    Ok(())
}

pub async fn ensure_image() -> Result<String> {
    IMAGE_BUILT
        .get_or_try_init(|| async {
            let tag = image_tag();
            build_image(&tag).await?;
            Ok(tag)
        })
        .await
        .cloned()
}