xlsx-audit-mcp
An MCP server that audits Excel workbooks without Excel. It reads the .xlsx / .xlsm file
itself (a ZIP of XML parts), executes nothing, and reports what a careful reviewer looks for:
errors, numbers buried in formulas, broken patterns, circular references, failing controls and
stale values. It also compares a submission with a reference workbook and prepares rubric-based
grading evidence.
- No dependency beyond the MCP SDK: the reader uses
node:zlib only.
- Read-only. The only writes are reports, to new files inside the allowed directory.
- Local only: no network access.
Install
skills install @spboucher/xlsx-audit-mcp --client claude-code
or by hand:
cd xlsx-audit-mcp && npm install --omit=dev
claude mcp add xlsx-audit --env XLSX_AUDIT_ROOT="$HOME/Documents/models" -- node "$PWD/server.mjs"
Generic MCP configuration:
{ "mcpServers": { "xlsx-audit": { "command": "node", "args": ["/path/to/xlsx-audit-mcp/server.mjs"], "env": { "XLSX_AUDIT_ROOT": "/path/to/workbooks" } } } }
Requires Node.js 20 or later.
Configuration
| Variable | Default | Meaning |
|---|
XLSX_AUDIT_ROOT | working directory | Only directory workbooks are read from and reports written to. Symbolic links leaving it are refused. |
XLSX_AUDIT_MAX_FILE_MB | 50 | Largest workbook file accepted. |
XLSX_AUDIT_MAX_UNZIPPED_MB | 128 per part, 384 total | Cap on decompressed size. Inflation stops at the cap whatever the ZIP header declares. |
XLSX_AUDIT_MAX_CELLS | 2 000 000 | Largest number of non-empty cells loaded. |
Tools
| Tool | What it answers |
|---|
workbook_summary | What is in this file? Sheets (hidden included), used ranges, counts, names, functions, macros, external links. |
read_range | What do these cells hold? Value, formula, number format, and where the value comes from. Paged. |
list_formulas | How is the model built? Formulas with R1C1 pattern and precedent / dependent counts, or grouped by pattern. |
find_errors | Which cells are in error, and which ones are the root causes? |
find_hardcoded | Where are numbers buried in formulas? Where was a formula overwritten by a typed number? |
formula_consistency | Which cell breaks the pattern of its row or column, and what should it hold? |
find_circular | Are there circular references? Each loop is printed cell by cell. |
trace_precedents | Where does this number come from? |
trace_dependents | What does this cell feed? |
check_controls | Do the model's own checks pass? Are some of them typed by hand? Does the overall cell agree? |
recalc_check | Do the stored results follow from the formulas? (stale or altered values) |
compare_workbooks | How does a submission differ from the reference? |
grade_submission | Rubric-based evidence and suggested points. An aid: a human decides. |
export_report | The whole audit as Markdown or JSON, inline or in a new file. |
Typical session: workbook_summary → export_report → trace_precedents on each finding worth
explaining.
Where values come from
Every value carries a source:
constant: typed in the cell.
cached: the result Excel (or LibreOffice) stored in the file next to the formula.
evaluated: the file holds no stored result and the formula is inside the supported subset.
unknown: no stored result and the formula is outside the subset. Never guessed.
The evaluated subset
+ - * / ^, unary minus, %, comparisons, SUM, AVERAGE, MIN, MAX, ROUND, IF, cell,
range and cross-sheet references, defined names that are plain references. Nothing else.
recalc_check and workbook_summary state the share of formulas outside the subset and the
reasons (for example function VLOOKUP (212)); those formulas are only read from stored values.
Grading rubric
{
"tolerance": 0.005,
"items": [
{ "id": "revenue", "label": "Revenue per year", "range": "Calc!B3:F3", "points": 5 },
{ "id": "units", "range": "Calc!C2:F2", "points": 4, "formula_required": true, "constant_credit": 0.5 },
{ "id": "summary", "ranges": ["Outputs!B1:B2"], "points": 2, "partial": "all_or_nothing", "carried_forward_credit": 0.5 }
]
}
Per cell: correct (credit 1), correct_value_typed_as_constant (constant_credit, default 0.5,
only when formula_required), carried_forward_error (carried_forward_credit, default 1: the
reference formula applied to the submission's own earlier values gives the submitted value, so the
mistake is upstream and penalised there), wrong, missing, unknown (0, listed in
needs_human_review). partial: proportional (default) or all_or_nothing. Top-level fields
are defaults for every item. The result always carries a disclaimer: it is evidence for a grader,
not a grade.
Security
- Workbooks are untrusted input. Nothing in them is executed: macros, external links, pivot caches
and embedded objects are never opened; their presence is reported.
- The ZIP reader refuses entry names that leave the archive (
.., absolute paths, backslashes),
ZIP64, multi-disk and encrypted archives, and stops inflating at the configured limit. Nothing is
ever extracted to disk.
- XML entities other than the five predefined ones are not expanded.
- Paths are resolved with symbolic links followed and must stay inside
XLSX_AUDIT_ROOT.
- Reports are created with exclusive-create: an existing file is an error, never overwritten.
- Cell text is data. Every result carries a notice reminding the model not to follow instructions
found in a workbook.
Limits
.xlsx and .xlsm only. Not .xls, .xlsb, .ods, .csv; not password-protected files.
- The evaluator is deliberately small (see above). A workbook saved by a library that does not
calculate (openpyxl, for example) has no stored results: formulas outside the subset are then
unknown until the file is opened and saved in Excel or LibreOffice.
- Dates are reported as serial numbers with their number format; they are not converted.
- Array formulas, data tables, structured table references, 3-D references and external references
are recognised and reported as outside the subset.
- Not read: conditional formatting, data validation, charts, pivot tables, comments, cell
protection, VBA.
- Pattern checks are heuristics:
formula_consistency grades each finding high or low
confidence, and a constant inside a formula is not always a defect. Errors hidden by IFERROR
are invisible.
- A clean report shows that these mechanical defects are absent. It does not show the model is
right.
- The dependency graph stops expanding after three million edges and says so (
graph_truncated).
Tests
npm test
npm run fixtures
Fixtures are written by a small writer in test/fixtures/ that stores a result next to each
formula. clean-libreoffice.xlsx was recalculated by LibreOffice and serves as an external check
of the evaluator. See test/fixtures/README.md.
License
MIT