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::Result;
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde::Serialize;
use serde::ser::Error as _;
use serde_json::Value;
use std::borrow::Cow;

pub fn to_pruned_value<T: Serialize>(value: &T) -> Result<Value> {
    let mut json = serde_json::to_value(value)?;
    prune_non_structural_empties(&mut json);
    Ok(json)
}

#[derive(Debug, Clone)]
pub struct Pruned<T>(pub T);

impl<T: Serialize> Serialize for Pruned<T> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut value = serde_json::to_value(&self.0).map_err(S::Error::custom)?;
        prune_non_structural_empties(&mut value);
        value.serialize(serializer)
    }
}

impl<T: JsonSchema> JsonSchema for Pruned<T> {
    fn schema_name() -> Cow<'static, str> {
        T::schema_name()
    }

    fn json_schema(generator: &mut SchemaGenerator) -> Schema {
        T::json_schema(generator)
    }
}

pub fn prune_non_structural_empties(value: &mut Value) {
    match value {
        Value::Object(map) => {
            let mut remove_keys = Vec::new();
            for (key, child) in map.iter_mut() {
                prune_non_structural_empties(child);
                if child.is_null() {
                    remove_keys.push(key.clone());
                    continue;
                }
                if let Value::Object(obj) = child
                    && obj.is_empty()
                {
                    remove_keys.push(key.clone());
                    continue;
                }
                if let Value::Array(arr) = child
                    && arr.is_empty()
                {
                    remove_keys.push(key.clone());
                }
            }
            for key in remove_keys {
                map.remove(&key);
            }
        }
        Value::Array(items) => {
            for child in items.iter_mut() {
                prune_non_structural_empties(child);
            }
        }
        _ => {}
    }
}