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::cli::OutputFormat;
use anyhow::{Result, bail};
use serde::Serialize;

pub fn ensure_output_supported(format: OutputFormat) -> Result<()> {
    match format {
        OutputFormat::Json => Ok(()),
        OutputFormat::Csv => {
            bail!("csv output is not implemented yet for this CLI; use --format json")
        }
    }
}

#[derive(Debug, Serialize)]
pub struct ErrorEnvelope {
    pub code: String,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub did_you_mean: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub try_this: Option<String>,
}

pub fn envelope_for(error: &anyhow::Error) -> ErrorEnvelope {
    let message = error.to_string();

    if let Some((requested, suggested)) = parse_sheet_suggestion(&message) {
        return ErrorEnvelope {
            code: "SHEET_NOT_FOUND".to_string(),
            message: format!("sheet '{}' was not found", requested),
            did_you_mean: Some(suggested),
            try_this: Some(
                "run `agent-spreadsheet list-sheets <file>` to inspect valid names".to_string(),
            ),
        };
    }

    if message.contains("does not exist") {
        return ErrorEnvelope {
            code: "FILE_NOT_FOUND".to_string(),
            message,
            did_you_mean: None,
            try_this: Some("check the workbook path and permissions".to_string()),
        };
    }

    if message.contains("at least one range") {
        return ErrorEnvelope {
            code: "INVALID_ARGUMENT".to_string(),
            message,
            did_you_mean: None,
            try_this: Some("pass one or more A1 ranges, for example: `A1:C10`".to_string()),
        };
    }

    if message.contains("at least one edit") {
        return ErrorEnvelope {
            code: "INVALID_ARGUMENT".to_string(),
            message,
            did_you_mean: None,
            try_this: Some("add one or more edits like `A1=42` or `B2==SUM(A1:A1)`".to_string()),
        };
    }

    if message.contains("invalid shorthand edit") {
        return ErrorEnvelope {
            code: "INVALID_EDIT_SYNTAX".to_string(),
            message,
            did_you_mean: None,
            try_this: Some(
                "use `<cell>=<value>` for values or `<cell>==<formula>` for formulas".to_string(),
            ),
        };
    }

    if message.contains("csv output is not implemented") {
        return ErrorEnvelope {
            code: "OUTPUT_FORMAT_UNSUPPORTED".to_string(),
            message,
            did_you_mean: Some("json".to_string()),
            try_this: Some("re-run with `--format json`".to_string()),
        };
    }

    ErrorEnvelope {
        code: "COMMAND_FAILED".to_string(),
        message,
        did_you_mean: None,
        try_this: None,
    }
}

fn parse_sheet_suggestion(message: &str) -> Option<(String, String)> {
    let prefix = "sheet '";
    let not_found = "' not found; did you mean '";
    let suffix = "' ?";

    let start = message.find(prefix)? + prefix.len();
    let rest = &message[start..];
    let mid = rest.find(not_found)?;
    let requested = &rest[..mid];
    let suggestion_start = start + mid + not_found.len();
    let suggestion_rest = &message[suggestion_start..];
    let suggestion_end = suggestion_rest.find(suffix)?;
    let suggested = &suggestion_rest[..suggestion_end];
    Some((requested.to_string(), suggested.to_string()))
}