XLSX Tools MCP

Um servidor MCP para ler, analisar e editar com precisão pastas de trabalho do Excel (.xlsx), preservando a estrutura existente, estilos, fórmulas e a integridade dos dados. Suporta operações de células, gerenciamento de planilhas e de linhas/colunas, formatação, agregação de dados, recálculo de fórmulas e acesso seguro e concorrente a arquivos.

Documentação

xlsx-tools-mcp

Um servidor MCP para ler e escrever arquivos Excel (.xlsx) com alta precisão, preservando a estrutura, estilos e fórmulas existentes do arquivo.

CI PyPI Version Downloads/month Listed on mcpservers.org


Visão Geral

xlsx-tools-mcp expõe 20 ferramentas do Model Context Protocol (MCP) que dão a um agente LLM acesso preciso de leitura e escrita que preserva a estrutura de arquivos Excel .xlsx. Ele roda como um servidor MCP padrão via stdio: você o instala e registra com um cliente MCP (Claude Code, OpenCode, etc.), e o agente do cliente pode listar planilhas, ler intervalos de células, pesquisar valores, agregar dados, escrever células/fórmulas, gerenciar planilhas/linhas/colunas, aplicar estilos e forçar o recálculo de fórmulas.

Ele é construído em torno do princípio de que editar uma pasta de trabalho existente não deve destruir o que não toca.

Recursos

  • Escritas que preservam a estrutura via openpyxl — as escritas carregam a pasta de trabalho existente e a salvam de volta, preservando estilos, células mescladas, comentários e qualquer aspecto que a edição não toque.
  • Resultados de fórmulas nunca desatualizados via recálculo do LibreOffice — openpyxl escreve strings de fórmulas, mas nunca as avalia. Após cada escrita de valor/fórmula, o servidor executa uma passagem headless do LibreOffice para recalcular resultados reais e então retorna errors_found — quaisquer valores de erro do Excel (#REF!, #DIV/0!, #N/A, …) produzidos pelo recálculo.
  • Leituras rápidas via python-calamine — um parser baseado em Rust para inferência de tipos precisa e rápida, com fallback automático para openpyxl quando você precisa de fórmulas/estilos/comentários ou quando o calamine não consegue analisar o arquivo.
  • Agrupamento/agregação baseado em pandasaggregate_sheet agrupa e agrega sobre o caminho de leitura normal, então células mescladas e estilos no intervalo de origem são preservados antes do achatamento.
  • Bloqueio por arquivo — chamadas de ferramentas concorrentes (ou outros processos) que tocam a mesma pasta de trabalho são serializadas via um arquivo <path>.lock irmão (filelock), então escritas nunca se intercalam e corrompem o arquivo.
  • Proteção contra bombas XML — o pacote defusedxml é uma dependência automática; openpyxl o detecta e usa seu parser XML endurecido, então XML xlsx hostil não pode se expandir em exaustão de recursos.
  • Pré-carregar arquivos na inicialização — defina XLSX_MCP_FILES para pré-carregar uma ou mais pastas de trabalho; as ferramentas podem então ser chamadas com path omitido ou com um alias curto em vez de um caminho completo do sistema de arquivos.

Estatísticas de Download

Downloads/month Downloads/week

Ao vivo via pypistats.org, downloads sem espelhamento. Estes contam eventos de download, não usuários únicos ou instalações — um usuário pode acionar muitos downloads (CI, reinstalações, reconstruções Docker, espelhamentos).


Arquitetura

┌──────────────────────── 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

A camada de io (io/) é deliberadamente desacoplada do transporte MCP (server.py). Cada ferramenta MCP é um wrapper fino que resolve o caminho alvo, adquire o bloqueio por arquivo e chama uma função da camada de io. Isso mantém a lógica central independente do MCP, para que possa ser testada diretamente (veja tests/).

A compensação do recálculo

Após uma escrita que toca valores de células ou fórmulas, o servidor executa soffice --headless --convert-to xlsx no arquivo para que cada fórmula obtenha um valor computado real. Este ciclo reexporta a pasta de trabalho inteira e recalcula fórmulas — é uma compensação, não uma garantia de preservação bit a bit. Recursos que openpyxl preservaria de outra forma podem não sobreviver de forma idêntica: tabelas dinâmicas, gráficos, validação de dados, alguns formatos e alguns nomes definidos.

Se você está trabalhando em uma pasta de trabalho estruturalmente complexa onde esse risco importa, você pode passar recalculate=False nas ferramentas de escrita de valor/fórmula (write_cells, append_rows, insert_rows, delete_rows, insert_columns, delete_columns) para salvar apenas com openpyxl e pular o ciclo completamente.


Requisitos

  • Python ≥ 3.10
  • LibreOfficeopcional, mas recomendado. Necessário apenas para recálculo de fórmulas. Sem ele, as escritas ainda funcionam (salvas via openpyxl), mas as fórmulas não são recalculadas e um aviso é retornado no campo message.

Instale o LibreOffice:

# macOS
brew install --cask libreoffice

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

O servidor encontra o LibreOffice verificando soffice / libreoffice em PATH e o local de instalação padrão do macOS (/Applications/LibreOffice.app/Contents/MacOS/soffice).


Instalação

O servidor fala transporte stdio (MCP padrão): após a instalação, ele aguarda um cliente MCP conectar e chamar ferramentas. Você normalmente não o executa você mesmo; você o registra com um cliente.

1. Do PyPI via uvx (recomendado — sem clone)

uvx xlsx-tools-mcp

uvx busca e executa o pacote publicado sem poluir seu projeto. Esta é a maneira mais simples de ativar um cliente MCP (veja os trechos de configuração abaixo).

2. Do código-fonte

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. Via pip

pip install xlsx-tools-mcp

Isso instala o ponto de entrada do console, então você pode executar o servidor diretamente:

xlsx-tools-mcp

Configuração para clientes MCP

O registro mais simples para cada cliente usa uvx xlsx-tools-mcp (sem clone, sempre a versão publicada).

Claude Code

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

Ou via .mcp.json no seu projeto:

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

OpenCode

Em opencode.json (projeto) ou ~/.config/opencode/opencode.json (global):

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

Ao executar a partir de um clone do código-fonte

Se você clonou o repositório em vez de instalar do PyPI, aponte o cliente para seu checkout local trocando uvx xlsx-tools-mcp pela forma dinâmica uv run (use o caminho absoluto para o clone):

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
    }
  }
}

Substitua /absolute/path/to/xlsx-reader pela localização real do seu clone.


Pré-carregando arquivos (XLSX_MCP_FILES)

Defina a variável de ambiente XLSX_MCP_FILES na seção env da configuração do servidor MCP (não no seu shell interativo — o servidor é iniciado pelo cliente) para pré-carregar pastas de trabalho na inicialização. Formato: entradas alias=absolute/path separadas por vírgula, ou caminhos absolutos simples:

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

Caminhos simples recebem um alias padrão baseado no nome do arquivo:

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

Com alias/filename como o alias:

  • Um arquivo configurado → cada ferramenta pode ser chamada com path completamente omitido.
  • Múltiplos arquivos configurados → passe o alias (ou nome do arquivo) como path.
  • list_configured_files() retorna o mapeamento alias → caminho absoluto.
  • Caminhos absolutos e relativos simples ainda funcionam para arquivos que você não pré-carregou.

Claude Code — .mcp.json com pré-carregamento:

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

OpenCode com pré-carregamento:

{
  "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
    }
  }
}

Referência de ferramentas

Todas as 20 ferramentas. Salvo indicação em contrário, path aceita um caminho do sistema de arquivos, um alias/nome de arquivo pré-carregado, ou pode ser omitido quando exatamente um arquivo está pré-carregado. create_workbook é a exceção — seu path é obrigatório porque um novo arquivo nunca é pré-carregado.

Formato da resposta (todas as ferramentas de escrita): cada ferramenta de escrita retorna {"saved": bool, "recalculated": bool, "errors_found": list, "message": str}. Quando não vazio, errors_found é uma lista de {"sheet": "...", "cell": "B2", "error": "#DIV/0!"}.

Inspecionar / Ler

FerramentaDescrição
list_configured_files()Lista arquivos pré-carregados na inicialização via XLSX_MCP_FILES, como um mapa alias → caminho absoluto. Chame isso primeiro se não tiver certeza do que está disponível.
list_sheets(path?)Lista cada planilha na pasta de trabalho com contagens aproximadas de linhas/colunas (calamine).
get_workbook_info(path?)Metadados no nível da pasta de trabalho: dimensões exatas por planilha, max_row/max_column, estado da planilha, a planilha ativa e nomes definidos.
read_sheet(sheet, cell_range?, max_rows?, path?)Lê valores de células como uma matriz 2D endereçada absolutamente a partir de A1. cell_range é um intervalo opcional no estilo A1 (ex.: "B2:F20"); omita para ler toda a área usada. max_rows opcionalmente limita o número de linhas retornadas.
get_cell(sheet, cell, path?)Detalhe completo para uma única célula: valor (computado em cache), fórmula, formato numérico, fonte (negrito/itálico/tamanho/cor), cor de preenchimento, estado de mesclagem, comentário.
search_workbook(query, sheet?, match_case?, limit?, path?)Pesquisa de substring em uma ou todas as planilhas. sheet restringe a uma planilha; match_case=True torna sensível a maiúsculas/minúsculas; limit limita correspondências. Retorna {"sheet", "cell", "value"}.
aggregate_sheet(sheet, group_by, agg, cell_range?, has_header?, path?)Agrupa e agrega com pandas. group_by é uma lista de nomes de colunas (tirados da linha de cabeçalho); agg mapeia nome da coluna → função de agregação, ex.: {"amount": "sum"}. has_header=True (padrão) lê nomes de colunas da primeira linha. Retorna {columns, records, row_count}.

Escrita

FerramentaDescrição
create_workbook(path, sheets?, overwrite?)Cria uma nova pasta de trabalho .xlsx/.xlsm. sheets padrão é ["Sheet1"]. overwrite=True substitui um arquivo existente. path é obrigatório (novos arquivos nunca são pré-carregados).
write_cells(sheet, cells, create_sheet_if_missing?, recalculate?, path?)Escreve valores e/ou fórmulas em células específicas. cells é uma lista de {"cell": "A1", "value": ...} ou {"cell": "B1", "formula": "=A1*2"}. Opcionalmente crie a planilha primeiro; recalculate=True (padrão) executa o recálculo do LibreOffice.
append_rows(sheet, rows, create_sheet_if_missing?, recalculate?, path?)Acrescenta linhas após a última linha usada. rows é uma lista de linhas, cada uma uma lista de valores de células em ordem de coluna.
create_sheet(sheet, index?, path?)Adiciona uma nova planilha vazia. index é uma posição de inserção baseada em zero; omita para acrescentar no final.
delete_sheet(sheet, path?)Exclui uma planilha. Falha se for a única planilha restante.
insert_rows(sheet, start_row, count?, recalculate?, path?)Insere linhas em branco antes de start_row (baseado em 1), deslocando linhas existentes para baixo. count padrão é 1.
delete_rows(sheet, start_row, count?, recalculate?, path?)Exclui linhas começando em start_row (baseado em 1), deslocando linhas abaixo para cima. count padrão é 1.
insert_columns(sheet, start_column, count?, recalculate?, path?)Insere colunas em branco antes de start_column (baseado em 1), deslocando colunas existentes para a direita. count padrão é 1.
delete_columns(sheet, start_column, count?, recalculate?, path?)Exclui colunas começando em start_column (baseado em 1), deslocando colunas à direita para a esquerda. count padrão é 1.
merge_cells(sheet, cell_range, path?)Mescla um intervalo retangular (ex.: "A1:C1") em uma única célula.
unmerge_cells(sheet, cell_range, path?)Desfaz uma mesclagem em um intervalo previamente mesclado.
set_cell_style(sheet, cell_range, style, path?)Aplica formatação a um intervalo (ex.: "A1:D1"). Chaves style: bold, italic, font_size, font_color (RGB hex, ex.: "FF0000"), bg_color (RGB hex), horizontal, vertical (alinhamento), border ("thin", "medium", "thick", …), number_format (ex.: "#,##0.00").
recalculate_workbook(path?)Força uma passagem de recálculo headless do LibreOffice e relata quaisquer erros de fórmula encontrados.

Exemplo de payload — write_cells

Uma chamada escrevendo uma fórmula e um valor:

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

Resposta correspondente:

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

Se uma fórmula tocada por isso produziu um erro, errors_found pareceria:

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

Segurança e concorrência

  • Proteção contra bombas XMLdefusedxml é uma dependência automática deste pacote. openpyxl o auto-detecta e usa seu parser XML endurecido, então um .xlsx malicioso (um zip de XML) não pode acionar exaustão de recursos por expansão de entidades. Nenhuma configuração necessária.
  • Bloqueio por arquivo — cada leitura/escrita adquire um arquivo <path>.lock irmão (via filelock). Chamadas de ferramentas concorrentes ou outros processos que tocam a mesma pasta de trabalho são serializados para que escritas nunca se intercalem e corrompam o arquivo.
  • Timeout de recálculoXLSX_MCP_RECALC_TIMEOUT (segundos, padrão 60) limita quanto tempo a passagem de recálculo do LibreOffice pode executar.
  • Timeout de bloqueioXLSX_MCP_LOCK_TIMEOUT (segundos, padrão 10) limita quanto tempo uma ferramenta esperará para adquirir o bloqueio por arquivo antes de falhar.

Solução de problemas

  • errors_found está vazio mesmo com minha fórmula quebrada — o recálculo provavelmente não foi executado. Verifique o campo message: se ele disser que o LibreOffice não foi encontrado, o arquivo foi salvo via openpyxl como está e as fórmulas não foram recalculadas (os valores em cache podem estar desatualizados). Instale o LibreOffice (veja Requisitos).
  • O recálculo está lento ou expira — aumente XLSX_MCP_RECALC_TIMEOUT (padrão 60s). Em caso de timeout, o arquivo ainda é salvo, mas recalculated será false e message indica que o recálculo expirou.
  • LockTimeoutError em acesso concorrente — outra operação está segurando o bloqueio. Aumente XLSX_MCP_LOCK_TIMEOUT (padrão 10s) ou tente novamente quando a outra operação terminar.
  • "Sheet not found" — a mensagem de erro lista os nomes das planilhas disponíveis, para que você possa escolher a correta.
  • path obrigatório / nenhum arquivo configurado — você chamou uma ferramenta sem path, mas nenhum (ou múltiplos) arquivo está pré-carregado. Pré-carregue um arquivo via XLSX_MCP_FILES, passe um alias explícito ou passe um caminho bruto.

Desenvolvimento / Contribuição

Veja CONTRIBUTING.md. Execute a suíte de testes com:

uv run pytest