XLSX Tools MCP

Un servidor MCP para leer, analizar y editar con precisión libros de Excel (.xlsx) preservando la estructura existente, los estilos, las fórmulas y la integridad de los datos. Admite operaciones de celdas, gestión de hojas y filas/columnas, formato, agregación de datos, recálculo de fórmulas y acceso seguro a archivos concurrente.

Documentación

xlsx-tools-mcp

Un servidor MCP para leer y escribir archivos Excel (.xlsx) con alta precisión, preservando la estructura, los estilos y las fórmulas existentes del archivo.

CI PyPI Version Downloads/month


Descripción general

xlsx-tools-mcp expone 20 herramientas del Protocolo de Contexto de Modelos (MCP) que brindan a un agente LLM acceso de lectura y escritura preciso y respetuoso con la estructura a archivos Excel .xlsx. Se ejecuta como un servidor MCP estándar de stdio: lo instalas y lo registras con un cliente MCP (Claude Code, OpenCode, etc.), y el agente del cliente puede listar hojas, leer rangos de celdas, buscar valores, agregar datos, escribir celdas/fórmulas, gestionar hojas/filas/columnas, aplicar estilos y forzar el recálculo de fórmulas.

Está construido sobre el principio de que editar un libro de trabajo existente no debe destruir lo que no toca.

Características

  • Escrituras que preservan la estructura mediante openpyxl — las escrituras cargan el libro de trabajo existente y lo guardan de nuevo, preservando estilos, celdas combinadas, comentarios y cualquier aspecto que la edición no toque.
  • Resultados de fórmulas nunca obsoletos mediante recálculo con LibreOffice — openpyxl escribe cadenas de fórmulas pero nunca las evalúa. Después de cada escritura de valor/fórmula, el servidor ejecuta una pasada de LibreOffice sin interfaz gráfica para recalcular los resultados reales y luego devuelve errors_found — cualquier valor de error de Excel (#REF!, #DIV/0!, #N/A, …) producido por el recálculo.
  • Lecturas rápidas mediante python-calamine — un analizador basado en Rust para una inferencia de tipos precisa y rápida, con un respaldo automático de openpyxl cuando necesitas fórmulas/estilos/comentarios o cuando calamine no puede analizar el archivo.
  • Agrupación/agregación basada en pandasaggregate_sheet agrupa y agrega sobre la ruta de lectura normal, de modo que las celdas combinadas y el estilo en el rango de origen se preservan antes del aplanamiento.
  • Bloqueo por archivo — las llamadas de herramientas concurrentes (u otros procesos) que tocan el mismo libro de trabajo se serializan mediante un archivo <path>.lock hermano (filelock), de modo que las escrituras nunca se intercalan ni corrompen el archivo.
  • Protección contra bombas XML — el paquete defusedxml es una dependencia automática; openpyxl lo detecta y usa su analizador XML endurecido, de modo que un XML xlsx hostil no puede expandirse hasta agotar los recursos.
  • Precarga de archivos al inicio — establece XLSX_MCP_FILES para precargar uno o más libros de trabajo; las herramientas pueden entonces llamarse con path omitido o con un alias corto en lugar de una ruta completa del sistema de archivos.

Estadísticas de descargas

Downloads/month Downloads/week

En vivo a través de pypistats.org, descargas sin espejos. Estas cuentan eventos de descarga, no usuarios únicos ni instalaciones — un solo usuario puede generar muchas descargas (CI, reinstalaciones, reconstrucciones de Docker, espejos).


Arquitectura

┌──────────────────────── Supervisor (MCP transport, stdio)
│  src/xlsx_tools_mcp/server.py     20 MCP tools + instructions
│  src/xlsx_tools_mcp/settings.py   env vars, preloaded files, path resolution
│  src/xlsx_tools_mcp/locking.py    per-file <path>.lock serialization
│  src/xlsx_tools_mcp/errors.py     domain error types
│  src/xlsx_tools_mcp/recalc.py     LibreOffice headless recalc + error scanning
│
├─ Read path
│  src/xlsx_tools_mcp/io/reader.py      calamine primary → openpyxl fallback
│  src/xlsx_tools_mcp/io/transform.py   pandas aggregation on read results
│
└─ Write path
   src/xlsx_tools_mcp/io/writer.py      openpyxl → LibreOffice recalc → scan errors

La capa de E/S (io/) está deliberadamente desacoplada del transporte MCP (server.py). Cada herramienta MCP es un envoltorio delgado que resuelve la ruta de destino, toma el bloqueo por archivo y llama a una función de la capa de E/S. Esto mantiene la lógica central independiente de MCP, de modo que puede probarse directamente (consulta tests/).

La compensación del recálculo

Después de una escritura que toca valores de celdas o fórmulas, el servidor ejecuta soffice --headless --convert-to xlsx sobre el archivo para que cada fórmula obtenga un valor calculado real. Este ciclo de ida y vuelta recalcula las fórmulas pero re-exporta todo el libro de trabajo — es una compensación, no una garantía de preservación bit a bit. Las características que openpyxl de otro modo preservaría pueden no sobrevivir de forma idéntica: tablas dinámicas, gráficos, validación de datos, algunos formatos y algunos nombres definidos.

Si trabajas en un libro de trabajo estructuralmente complejo donde ese riesgo importa, puedes pasar recalculate=False en las herramientas de escritura de valores/fórmulas (write_cells, append_rows, insert_rows, delete_rows, insert_columns, delete_columns) para guardar solo con openpyxl y omitir el ciclo de ida y vuelta por completo.


Requisitos

  • Python ≥ 3.10
  • LibreOfficeopcional pero recomendado. Solo se necesita para el recálculo de fórmulas. Sin él, las escrituras aún se realizan correctamente (guardadas mediante openpyxl) pero las fórmulas no se recalculan y se devuelve una advertencia en el campo message.

Instala LibreOffice:

# macOS
brew install --cask libreoffice

# Debian / Ubuntu
sudo apt-get install -y libreoffice-calc

El servidor encuentra LibreOffice verificando soffice / libreoffice en PATH y la ubicación estándar de instalación de macOS (/Applications/LibreOffice.app/Contents/MacOS/soffice).


Instalación

El servidor habla transporte stdio (MCP estándar): después de la instalación espera a que un cliente MCP se conecte y llame a las herramientas. Normalmente no lo ejecutas tú mismo; lo registras con un cliente.

1. Desde PyPI mediante uvx (recomendado — sin clonar)

uvx xlsx-tools-mcp

uvx obtiene y ejecuta el paquete publicado sin ensuciar tu proyecto. Esta es la forma más sencilla de activar un cliente MCP (consulta los fragmentos de configuración a continuación).

2. Desde el código fuente

git clone https://github.com/ruriazz/xlsx-tools-mcp.git
cd xlsx-tools-mcp
uv sync
# run the server (useful for local dev / debugging):
uv run xlsx-tools-mcp

3. Mediante pip

pip install xlsx-tools-mcp

Esto instala el punto de entrada de consola, de modo que puedes ejecutar el servidor directamente:

xlsx-tools-mcp

Configuración para clientes MCP

El registro más sencillo para cada cliente usa uvx xlsx-tools-mcp (sin clonar, siempre la versión publicada).

Claude Code

claude mcp add xlsx-tools-mcp -- uvx xlsx-tools-mcp

O mediante .mcp.json en tu proyecto:

{
  "mcpServers": {
    "xlsx-tools-mcp": { "command": "uvx", "args": ["xlsx-tools-mcp"] }
  }
}

OpenCode

En opencode.json (proyecto) o ~/.config/opencode/opencode.json (global):

{
  "mcp": {
    "xlsx-tools-mcp": { "type": "local", "command": ["uvx", "xlsx-tools-mcp"], "enabled": true }
  }
}

Cuando se ejecuta desde un clon del código fuente

Si clonaste el repositorio en lugar de instalarlo desde PyPI, apunta el cliente a tu copia local intercambiando uvx xlsx-tools-mcp por la forma dinámica uv run (usa la ruta absoluta al clon):

Claude Code .mcp.json:

{
  "mcpServers": {
    "xlsx-tools-mcp": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/xlsx-reader", "run", "xlsx-tools-mcp"]
    }
  }
}

OpenCode:

{
  "mcp": {
    "xlsx-tools-mcp": {
      "type": "local",
      "command": ["uv", "--directory", "/absolute/path/to/xlsx-reader", "run", "xlsx-tools-mcp"],
      "enabled": true
    }
  }
}

Reemplaza /absolute/path/to/xlsx-reader con la ubicación real de tu clon.


Precarga de archivos (XLSX_MCP_FILES)

Establece la variable de entorno XLSX_MCP_FILES en la sección env de la configuración del servidor MCP (no en tu shell interactivo — el servidor lo lanza el cliente) para precargar libros de trabajo al inicio. Formato: entradas alias=absolute/path separadas por comas, o rutas absolutas simples:

XLSX_MCP_FILES=name=/abs/path/to/name.xlsx,report=/data/report.xlsx

Las rutas simples obtienen un alias que por defecto es el nombre del archivo:

XLSX_MCP_FILES=/abs/path/to/sales.xlsx

Con alias/filename como alias:

  • Un archivo configurado → cada herramienta puede llamarse con path omitido por completo.
  • Varios archivos configurados → pasa el alias (o nombre de archivo) como path.
  • list_configured_files() devuelve el mapeo alias → ruta absoluta.
  • Las rutas absolutas y relativas sin procesar aún funcionan para archivos que no precargaste.

Claude Code — .mcp.json con precarga:

{
  "mcpServers": {
    "xlsx-tools-mcp": {
      "command": "uvx",
      "args": ["xlsx-tools-mcp"],
      "env": {
        "XLSX_MCP_FILES": "report=/data/report.xlsx,sales=/data/sales.xlsx"
      }
    }
  }
}

OpenCode con precarga:

{
  "mcp": {
    "xlsx-tools-mcp": {
      "type": "local",
      "command": ["uvx", "xlsx-tools-mcp"],
      "env": { "XLSX_MCP_FILES": "report=/data/report.xlsx,sales=/data/sales.xlsx" },
      "enabled": true
    }
  }
}

Referencia de herramientas

Las 20 herramientas. Salvo que se indique, path acepta una ruta del sistema de archivos, un alias/nombre de archivo precargado, o puede omitirse cuando exactamente un archivo está precargado. create_workbook es la excepción — su path es obligatorio porque un archivo nuevo nunca está precargado.

Forma de la respuesta (todas las herramientas de escritura): cada herramienta de escritura devuelve {"saved": bool, "recalculated": bool, "errors_found": list, "message": str}. Cuando no está vacío, errors_found es una lista de {"sheet": "...", "cell": "B2", "error": "#DIV/0!"}.

Inspeccionar / Leer

HerramientaDescripción
list_configured_files()Lista los archivos precargados al inicio mediante XLSX_MCP_FILES, como un mapa alias → ruta absoluta. Llama a esta primero si no estás seguro de qué está disponible.
list_sheets(path?)Lista cada hoja del libro de trabajo con recuentos aproximados de filas/columnas (calamine).
get_workbook_info(path?)Metadatos a nivel de libro de trabajo: dimensiones exactas por hoja, max_row/max_column, estado de la hoja, la hoja activa y nombres definidos.
read_sheet(sheet, cell_range?, max_rows?, path?)Lee valores de celdas como una matriz 2D direccionada absolutamente desde A1. cell_range es un rango opcional de estilo A1 (p. ej. "B2:F20"); omítelo para leer el área usada completa. max_rows limita opcionalmente el número de filas devueltas.
get_cell(sheet, cell, path?)Detalle completo de una sola celda: valor (resultado calculado en caché), fórmula, formato de número, fuente (negrita/cursiva/tamaño/color), color de relleno, estado de combinación, comentario.
search_workbook(query, sheet?, match_case?, limit?, path?)Búsqueda de subcadenas en una o todas las hojas. sheet restringe a una hoja; match_case=True la hace sensible a mayúsculas; limit limita las coincidencias. Devuelve {"sheet", "cell", "value"}.
aggregate_sheet(sheet, group_by, agg, cell_range?, has_header?, path?)Agrupa y agrega con pandas. group_by es una lista de nombres de columnas (tomados de la fila de encabezados); agg mapea nombre de columna → función de agregación, p. ej. {"amount": "sum"}. has_header=True (por defecto) lee los nombres de columna de la primera fila. Devuelve {columns, records, row_count}.

Escritura

HerramientaDescripción
create_workbook(path, sheets?, overwrite?)Crea un nuevo libro de trabajo .xlsx/.xlsm. sheets por defecto es ["Sheet1"]. overwrite=True reemplaza un archivo existente. path es obligatorio (los archivos nuevos nunca están precargados).
write_cells(sheet, cells, create_sheet_if_missing?, recalculate?, path?)Escribe valores y/o fórmulas en celdas específicas. cells es una lista de {"cell": "A1", "value": ...} o {"cell": "B1", "formula": "=A1*2"}. Opcionalmente crea la hoja primero; recalculate=True (por defecto) ejecuta el recálculo de LibreOffice.
append_rows(sheet, rows, create_sheet_if_missing?, recalculate?, path?)Agrega filas después de la última fila usada. rows es una lista de filas, cada una una lista de valores de celda en orden de columna.
create_sheet(sheet, index?, path?)Agrega una nueva hoja vacía. index es una posición de inserción basada en cero; omítelo para agregar al final.
delete_sheet(sheet, path?)Elimina una hoja. Falla si es la única hoja restante.
insert_rows(sheet, start_row, count?, recalculate?, path?)Inserta filas en blanco antes de start_row (basado en 1), desplazando las filas existentes hacia abajo. count por defecto es 1.
delete_rows(sheet, start_row, count?, recalculate?, path?)Elimina filas comenzando en start_row (basado en 1), desplazando las filas inferiores hacia arriba. count por defecto es 1.
insert_columns(sheet, start_column, count?, recalculate?, path?)Inserta columnas en blanco antes de start_column (basado en 1), desplazando las columnas existentes hacia la derecha. count por defecto es 1.
delete_columns(sheet, start_column, count?, recalculate?, path?)Elimina columnas comenzando en start_column (basado en 1), desplazando las columnas de la derecha hacia la izquierda. count por defecto es 1.
merge_cells(sheet, cell_range, path?)Combina un rango rectangular (p. ej. "A1:C1") en una sola celda.
unmerge_cells(sheet, cell_range, path?)Deshace una combinación en un rango previamente combinado.
set_cell_style(sheet, cell_range, style, path?)Aplica formato a un rango (p. ej. "A1:D1"). Claves de style: bold, italic, font_size, font_color (RGB hexadecimal, p. ej. "FF0000"), bg_color (RGB hexadecimal), horizontal, vertical (alineación), border ("thin", "medium", "thick", …), number_format (p. ej. "#,##0.00").
recalculate_workbook(path?)Fuerza una pasada de recálculo de LibreOffice sin interfaz gráfica e informa cualquier error de fórmula encontrado.

Ejemplo de carga útil — write_cells

Una llamada que escribe una fórmula y un valor:

{
  "sheet": "Sheet1",
  "cells": [
    { "cell": "A1", "value": 100 },
    { "cell": "B1", "formula": "=A1*2" }
  ],
  "recalculate": true,
  "path": "/data/budget.xlsx"
}

Respuesta correspondiente:

{
  "saved": true,
  "recalculated": true,
  "errors_found": [],
  "message": "Recalculated with LibreOffice headless."
}

Si una fórmula tocada por esto produjo un error, errors_found se vería así:

{
  "saved": true,
  "recalculated": true,
  "errors_found": [
    { "sheet": "Sheet1", "cell": "C5", "error": "#DIV/0!" }
  ],
  "message": "Recalculated with LibreOffice headless."
}

Seguridad y concurrencia

  • Protección contra bombas XMLdefusedxml es una dependencia automática de este paquete. openpyxl lo detecta automáticamente y usa su analizador XML endurecido, de modo que un .xlsx malicioso (un zip de XML) no puede desencadenar agotamiento de recursos por expansión de entidades. No se necesita configuración.
  • Bloqueo por archivo — cada lectura/escritura adquiere un archivo <path>.lock hermano (mediante filelock). Las llamadas de herramientas concurrentes u otros procesos que tocan el mismo libro de trabajo se serializan de modo que las escrituras nunca se intercalan ni corrompen el archivo.
  • Tiempo de espera de recálculoXLSX_MCP_RECALC_TIMEOUT (segundos, por defecto 60) limita cuánto tiempo puede ejecutarse la pasada de recálculo de LibreOffice.
  • Tiempo de espera de bloqueoXLSX_MCP_LOCK_TIMEOUT (segundos, por defecto 10) limita cuánto tiempo esperará una herramienta para adquirir el bloqueo por archivo antes de fallar.

Solución de problemas

  • errors_found está vacío aunque mi fórmula está rota — probablemente no se ejecutó el recálculo. Comprueba el campo message: si dice que no se encontró LibreOffice, el archivo se guardó tal cual mediante openpyxl y las fórmulas no se recalcularon (los valores en caché pueden estar desactualizados). Instala LibreOffice (consulta Requisitos).
  • El recálculo es lento o se agota el tiempo — aumenta XLSX_MCP_RECALC_TIMEOUT (por defecto 60s). En caso de tiempo de espera, el archivo se guarda igualmente, pero recalculated será false y message indica que el recálculo agotó el tiempo.
  • LockTimeoutError en acceso concurrente — otra operación mantiene el bloqueo. Aumenta XLSX_MCP_LOCK_TIMEOUT (por defecto 10s), o reintenta cuando la otra operación termine.
  • "Hoja no encontrada" — el mensaje de error lista los nombres de hojas disponibles, para que puedas elegir la correcta.
  • path requerido / no hay archivo configurado — llamaste a una herramienta sin path pero no hay (o hay múltiples) archivos precargados. Precarga un archivo mediante XLSX_MCP_FILES, pasa un alias explícito o pasa una ruta sin procesar.

Desarrollo / Contribución

Consulta CONTRIBUTING.md. Ejecuta la suite de pruebas con:

uv run pytest