{"id":"1ed0a21e-8123-4c92-b92b-2019684e1ed2","slug":"facturas-inbox-mcp","kind":"mcp","name":"facturas-inbox-mcp","summary":"MCP server that turns a folder of Spanish invoice PDFs into validated data: classify, extract NIF/IVA/IRPF with arithmetic checks, and summarize IVA per quarter. Local, read-only.","owner":"pedrozapatadev","repoUrl":"https://github.com/pedrozapatadev/facturas-inbox-mcp","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-08T13:48:16.000Z","addedAt":"2026-10-14T05:52:06.621Z","logoUrl":"https://avatars.githubusercontent.com/u/97449142?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","data-analytics","memory-knowledge"],"tags":["ai-agents","invoices","iva","spain","typescript"],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-21T01:57:05.013Z","sample":false,"url":"https://conduits.dev/tools/facturas-inbox-mcp","language":"TypeScript","transports":["stdio"],"install":{"npm":"facturas-inbox-mcp"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# facturas-inbox-mcp\n\n[![CI](https://github.com/pedrozapatadev/facturas-inbox-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/pedrozapatadev/facturas-inbox-mcp/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n![MCP SDK v2](https://img.shields.io/badge/MCP_SDK-v2-black)\n![Node ≥ 20.12](https://img.shields.io/badge/node-%E2%89%A520.12-green)\n\nAn [MCP](https://modelcontextprotocol.io) server that turns a folder of Spanish business documents\ninto validated, structured data an AI agent can reason over. It classifies each PDF (factura,\nrectificativa, ticket, nómina, modelo AEAT…), extracts the invoice fields (NIF/CIF, base imponible,\nIVA per rate, IRPF, total), **checks the arithmetic and the tax-ID checksums**, and computes the IVA\nsoportado / repercutido position for a quarter.\n\nLocal and offline. No API keys. It reads only the folders you give it.\n\n> Built for the everyday job of a Spanish *gestoría*: a client drops a month of PDFs in a folder and\n> someone has to work out what is what, what adds up and what the VAT position is.\n\n## 30-second demo\n\nClaude Code, Sonnet, pointed at the synthetic [`examples/inbox`](examples/inbox) (11 documents,\nincluding a wrong total, a ticket, a credit note, a payslip, a modelo 303 and a scanned page).\nReal session; the answer is trimmed for length (full, unedited transcript in [`docs/demo-transcript.md`](docs/demo-transcript.md)):\n\n```text\n> I'm the bookkeeper for Gestoría Demo Ficticia SL (CIF B87654323). Process the invoice\n  inbox and tell me our IVA position for Q3 2026, and flag anything I need to look at\n  by hand. Answer in English, briefly.\n\n  ⏺ facturas - scan_folder {}\n  ⏺ facturas - vat_summary {\"own_tax_id\":\"B87654323\",\"quarter\":\"2026-Q3\"}\n  ⏺ facturas - validate_tax_id {\"tax_id\":\"B87654323\"}\n\nQ3 2026 IVA position: €243.00 to pay (a pagar), provisional.\n\n- IVA repercutido: €504.00, from 1 issued invoice (GD-2026-0088, base €2,400 at 21%)\n- IVA soportado: €261.00, from 4 received documents (base €1,400): €231 at 21% and\n  €30 at 10%\n- The July papelería invoice (€52.50 IVA) is cancelled out by the September credit note.\n\nCheck by hand:\n1. Papelería PE-2026/0455 (30 Jul): the total says €1,250 but base + IVA comes to\n   €1,210. Excluded. If the supplier corrects it, your position drops to about €33.\n2. Gasolinera ticket 88213 (14 Aug, €60): IVA not broken out and no recipient NIF.\n   Excluded. Probably not deductible without a full invoice.\n3. Scanned PDF (5 Sep): no text layer, so it needs OCR or a manual read. It could be\n   a missing Q3 invoice.\n4. Credit note PE-2026/R-0019: classified at 0.69 confidence. Confirm it really is a\n   rectificativa of PE-2026/0412.\n5. Laura's advisory invoice (3 Aug): €150 IRPF withheld, which goes on modelo 111,\n   not the IVA return.\n   …\n```\n\n**5 turns · 23 s · $0.13.** Every number comes from a tool result, not from the model doing\narithmetic. Documents that fail a check are excluded and listed with a reason, so nothing is\nsilently counted or silently dropped.\n\n## Tools\n\n| Tool | What it does | Hints |\n|---|---|---|\n| `scan_folder` | Classify and extract every document in a folder; one compact row each, paginated with `limit` / `cursor` | read-only |\n| `classify_document` | Label one document, with a confidence score and the signals behind the label | read-only |\n| `extract_invoice` | Invoice number, date, issuer/recipient NIF, base, VAT lines, IRPF, total, plus every cross-check. `response_format: concise \\| detailed` | read-only |\n| `validate_tax_id` | NIF / NIE / CIF checksum validation. Says the expected control character when wrong | read-only |\n| `vat_summary` | IVA repercutido vs. soportado for a `quarter` (`2026-Q3`) or a `from`/`to` range, by rate, with the exclusions listed | read-only |\n| `export_csv` | Write the scan to a new `.csv` file (`;`-separated, decimal comma, opens directly in Spanish Excel). Never overwrites | writes a new file |\n\nEvery tool publishes an `outputSchema` and returns `structuredContent`, and the SDK validates both\ndirections. All tools set full [annotations](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)\n(`readOnlyHint`, `destructiveHint: false`, `idempotentHint`, `openWorldHint: false`), so clients can\nauto-approve the read-only ones.\n\n### The checks\n\n`extract_invoice` returns `status: \"ok\"` only when every check passes:\n\n| Check | Rule |\n|---|---|\n| `issuer_tax_id` | A checksum-valid NIF/NIE/CIF is present before the client section |\n| `issue_date` | A real calendar date was found (`15/07/2026`, `22 de septiembre de 2026`, …) |\n| `vat_rate_standard` | Every VAT rate is a Spanish rate (0, 4, 5, 10, 21 %) |\n| `vat_math` | base × rate = VAT, per rate when the invoice breaks bases out (±1 cent) |\n| `irpf_math` | base × IRPF rate = withholding (±1 cent) |\n| `total_math` | base + VAT − IRPF = total |\n\nAll arithmetic is in integer cents.\n\n## Install\n\nRequires Node.js 20.12 or newer.\n\n### Try it in 30 seconds (bundled sample data)\n\n```bash\nclaude mcp add facturas-demo -- npx -y facturas-inbox-mcp --demo\n```\n\n`--demo` serves the synthetic inbox that ships with the package. Then ask Claude:\n*\"I'm the bookkeeper for CIF B87654323. What's our IVA position for Q3 2026, and what should I check\nby hand?\"*\n\n### Claude Code\n\n```bash\nclaude mcp add facturas -- npx -y facturas-inbox-mcp ~/Documents/facturas\n```\n\n### Claude Desktop\n\nAdd this to `claude_desktop_config.json` (Settings → Developer → Edit Config):\n\n```json\n{\n  \"mcpServers\": {\n    \"facturas\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"facturas-inbox-mcp\", \"/Users/you/Documents/facturas\"]\n    }\n  }\n}\n```\n\n### Any other MCP client\n\nIt's a standard stdio server: `npx -y facturas-inbox-mcp <folder> [more folders…]`, or\n`npx -y facturas-inbox-mcp --demo`. Folders can also come from `FACTURAS_INBOX_DIRS` (separated\nlike `PATH`).\n\n### From source, with the sample inbox\n\n```bash\ngit clone https://github.com/pedrozapatadev/facturas-inbox-mcp && cd facturas-inbox-mcp\nnpm install && npm run build\nclaude mcp add facturas -- node \"$PWD/dist/index.js\" \"$PWD/examples/inbox\"\n```\n\nThen ask: *\"I'm the bookkeeper for CIF B87654323. What's our IVA position for Q3 2026?\"*\n\nTo poke at the tools without an LLM, run `npm run inspect` (MCP Inspector).\n\n## Design decisions\n\n- **Deterministic core, no model inside.** The agent is already a language model. What it lacks are\n  tools whose answers it can trust. Extraction is rule-based and every result carries its evidence:\n  classifier signals, check details, warnings. The server can explain itself, and the same input\n  always gives the same output.\n- **Fail loudly, not silently.** Fields that weren't found are `null`, not guessed. Documents that\n  fail a check are excluded from totals *and listed with the reason*. Errors are returned as\n  `isError` results that say how to fix the call (\"Pass `force: true` to try anyway\", \"Run OCR\n  first\").\n- **Token-efficient.** `scan_folder` returns compact rows and paginates. `extract_invoice` is concise\n  by default. `export_csv` returns a path and counts, not the data.\n- **Sandboxed filesystem.** Paths are resolved with `realpath` and must sit inside a folder given at\n  startup, so `../` traversal and symlinks that point outside are rejected (and tested). The only\n  write is `export_csv`, which creates new `.csv` files and never overwrites.\n- **Untrusted document text.** The server instructions tell the agent never to follow instructions\n  found inside a document. Raw text is only returned on request, labelled `untrusted_text_preview`.\n  CSV cells that start with `=`, `+`, `-` or `@` are neutralized against spreadsheet formula\n  injection. Exports are created `0600`.\n- **Bounded work per document.** Files are limited to 20 MB and PDFs to 200 pages. Text is cut at\n  1M characters before analysis. The extraction regexes are linear-time; a ReDoS case has a\n  regression test.\n- **Threat model.** The attacker controls document *contents*, not the filesystem. Paths that are\n  missing and paths outside the folders get the same error, so the tool can't be used to probe what\n  exists elsewhere. Someone who can already write to the inbox could race the check-then-read (a\n  TOCTOU window); that is out of scope.\n- **stdout is the protocol.** All logging goes to stderr. An end-to-end test drives the built binary\n  over real stdio, and it would fail on a stray `console.log`.\n\n## Limitations\n\nHonest scope. This is a showcase-sized tool, not accounting software.\n\n- **PDFs need a text layer.** Scanned images are reported as `unreadable` with an OCR hint\n  (e.g. [`ocrmypdf`](https://github.com/ocrmypdf/OCRmyPDF)); there is no built-in OCR.\n- **Heuristic extraction** tuned to common Spanish invoice layouts (labelled fields like\n  *Base imponible*, *IVA 21 %*, *Total factura*). Unusual layouts come back as `needs_review`\n  rather than wrong.\n- **Not handled yet:** recargo de equivalencia, intra-EU / reverse-charge invoices, Facturae XML, and\n  counterparty names (only tax IDs).\n- **Checksum validation only.** A valid NIF is well-formed; it is not confirmed as registered with\n  AEAT or VIES.\n- **Not an e-invoicing or VeriFactu tool.** It reads received paperwork; it does not issue invoices\n  or file returns. `vat_summary` is a working figure for review, not a modelo 303.\n\n## Development\n\n```bash\nnpm install\nnpm run fixtures    # regenerate examples/inbox (synthetic data, valid-checksum fictional IDs)\nnpm run typecheck\nnpm test            # unit + in-memory MCP client + stdio end-to-end (build first for the e2e)\nnpm run build\nnpm run inspect     # MCP Inspector UI against the sample inbox\n```\n\n```\nsrc/\n  index.ts        stdio entry point, CLI args → allowed folders\n  server.ts       the six tools: schemas, annotations, error mapping\n  schemas.ts      zod input/output schemas\n  pipeline.ts     read → classify → extract, shared by all tools\n  classify.ts     rule-based document classifier with explainable signals\n  extract.ts      invoice field extraction + arithmetic checks\n  taxid.ts        NIF / NIE / CIF validation\n  amounts.ts      Spanish amounts and dates, in integer cents\n  vat-summary.ts  repercutido / soportado aggregation\n  documents.ts    sandboxed filesystem + PDF text extraction\n  csv.ts          Excel-friendly CSV export\ntest/             MCP-level tests (in-memory client, stdio e2e, sandbox escapes)\n```\n\nBuilt on the official [MCP TypeScript SDK v2](https://ts.sdk.modelcontextprotocol.io/v2/) (`@modelcontextprotocol/server`),\n[zod](https://zod.dev) and [unpdf](https://github.com/unjs/unpdf).\n\n### Releasing\n\n1. Bump `version` in `package.json` and both `version` fields in `server.json`.\n2. `npm publish` (runs build + tests first; needs npm 2FA).\n3. `gh workflow run publish-mcp-registry.yml` lists the new version in the\n   [MCP Registry](https://registry.modelcontextprotocol.io) via GitHub OIDC (no secrets).\n\n## Roadmap\n\n- Optional OCR pass for scanned documents\n- Counterparty names, recargo de equivalencia, intra-EU invoices\n- Facturae XML input\n- VIES lookup as an opt-in, clearly network-marked tool\n- Streamable HTTP transport for remote deployment\n\n## License\n\n[MIT](LICENSE) © 2026 Pedro Zapata Medal. All sample documents are synthetic: the companies, people\nand tax IDs are fictional.\n"}