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.
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 pandas —
aggregate_sheetagrupa 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>.lockhermano (filelock), de modo que las escrituras nunca se intercalan ni corrompen el archivo. - Protección contra bombas XML — el paquete
defusedxmles una dependencia automática; openpyxl lo detecta y usa su analizador XML endurecido, de modo que un XMLxlsxhostil no puede expandirse hasta agotar los recursos. - Precarga de archivos al inicio — establece
XLSX_MCP_FILESpara precargar uno o más libros de trabajo; las herramientas pueden entonces llamarse conpathomitido o con un alias corto en lugar de una ruta completa del sistema de archivos.
Estadísticas de descargas
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
- LibreOffice — opcional 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
pathomitido 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_foundes una lista de{"sheet": "...", "cell": "B2", "error": "#DIV/0!"}.
Inspeccionar / Leer
| Herramienta | Descripció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
| Herramienta | Descripció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 XML —
defusedxmles una dependencia automática de este paquete. openpyxl lo detecta automáticamente y usa su analizador XML endurecido, de modo que un.xlsxmalicioso (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>.lockhermano (mediantefilelock). 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álculo —
XLSX_MCP_RECALC_TIMEOUT(segundos, por defecto60) limita cuánto tiempo puede ejecutarse la pasada de recálculo de LibreOffice. - Tiempo de espera de bloqueo —
XLSX_MCP_LOCK_TIMEOUT(segundos, por defecto10) limita cuánto tiempo esperará una herramienta para adquirir el bloqueo por archivo antes de fallar.
Solución de problemas
errors_foundestá vacío aunque mi fórmula está rota — probablemente no se ejecutó el recálculo. Comprueba el campomessage: 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, perorecalculatedseráfalseymessageindica que el recálculo agotó el tiempo. LockTimeoutErroren acceso concurrente — otra operación mantiene el bloqueo. AumentaXLSX_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.
pathrequerido / no hay archivo configurado — llamaste a una herramienta sinpathpero no hay (o hay múltiples) archivos precargados. Precarga un archivo medianteXLSX_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