paperless-tax-mcp

July 25, 2026 · View on GitHub

An unofficial MCP server and CLI for paperless.tax, the Israeli bookkeeping/expense platform.

⚠️ Not affiliated with, endorsed by, or supported by Paperless. This talks to the app's own private backend, not a published developer API, and can break without notice. Use it against your own account.

The value here is as much docs/api-surface.md as the code — no public documentation of this API exists anywhere, so the map is the point.

Authentication

Use the session JWT the web app holds after login, as Authorization: Bearer <jwt>.

The self-service API key from the UI's API/webhooks modal is not a credential for these endpoints — it returns 401 under every scheme, and paying for the API add-on does not change that. Do not buy the add-on expecting it to help.

The good news, contrary to what you would assume of a "session" token: its exp is about ten months out, so one interactive login covers scheduled work for most of a year. Login is passwordless (email → entry code), so a fresh token needs a browser once; after that it can be lifted from the browser profile. docs/api-surface.md has both.

Use as an MCP server

Register with any MCP client — Claude Code, Claude Desktop, or a gateway. No install step:

{
  "mcpServers": {
    "paperless-tax": {
      "command": "npx",
      "args": ["-y", "paperless-tax-mcp"],
      "env": { "PAPERLESS_TOKEN": "..." }
    }
  }
}

Claude Code, in one line:

claude mcp add paperless-tax --env PAPERLESS_TOKEN=... -- npx -y paperless-tax-mcp

Use as a CLI

The same client, for exports and poking at the API. Install it once so the paperless-tax binary is on your PATH:

npm install -g paperless-tax-mcp
export PAPERLESS_TOKEN=...

paperless-tax whoami                       # verify the token, print the account id
paperless-tax probe                        # hit each read endpoint, report what answers
paperless-tax list --type 1                # expenses only
paperless-tax list --from 2025-01 --to 2025-12
paperless-tax export --out export/         # files plus metadata sidecars
paperless-tax raw POST documents/get/downloadurl --json '{"sDocID":"..."}'

To run it without installing, --package is required:

npx --package paperless-tax-mcp paperless-tax export --out export/

Plain npx paperless-tax … would look for a package named paperless-tax, which is not this one. And npx paperless-tax-mcp paperless-tax … is worse than an error: with two binaries in the package, npx runs the one matching the package name — the MCP server — treating paperless-tax as an argument. It waits on stdio, reads EOF, and exits 0 having done nothing, which is indistinguishable from a successful run that exported nothing.

export writes {docid}.pdf plus a {docid}.json sidecar under export/expenses/{YYYY}/{YYYY-MM}/, with a manifest.jsonl index. It is resumable — documents already on disk are skipped — and the manifest is rewritten from scratch each run, so it describes the whole export rather than only what that run fetched.

--from/--to accept YYYY-MM-DD, YYYY-MM or the API's native YYMM. Omit them to get everything: the default range is the app's own unbounded 1001..2912.

Use as a library

import { PaperlessClient } from "paperless-tax-mcp/client";
import { Exporter } from "paperless-tax-mcp/export";

const client = new PaperlessClient({ token });
const docs = await client.byDate();            // whole history, one call
const pdfUrl = await client.downloadUrl(docs[0].id);

Configuration

VariableRequiredMeaning
PAPERLESS_TOKENyesThe session JWT — see above
PAPERLESS_USER_IDno10-char account id; whoami discovers it
PAPERLESS_BASE_URLnoDefaults to https://api.paperless.tax/

From source

npm install && npm run build && npm test

Tools

ToolEndpointConfidence
whoamiGET users/get/paramsconfirmed live
pending_documentsGET documents/get/pending/{uid}confirmed live
list_documentsPOST documents/get/bydateconfirmed live
get_documentGET documents/get/document/{id}confirmed live
get_download_urlPOST documents/get/downloadurlconfirmed live
raw_requestanythingescape hatch for mapping new surface

Four things that will bite you

All documented at length in docs/api-surface.md, and all of them cost real time to find:

  1. The controller prefix is not optional. POST /bydate is a 404; the route is POST documents/get/bydate. Same for downloadurl, searchdp, document/{id}.
  2. A 401 does not mean the route exists. Auth runs before routing, so unauthenticated every path 401s — including nonsense ones. This is what produced the earlier, wrong version of the endpoint map. Never infer existence from a 401 here.
  3. bydate takes YYMM period strings, not ISO dates. sStart: "2506" is June 2025. Give it a date and you get a narrower result set rather than an error.
  4. A 204 is not success. Send no credentials at all and you get the same 204. Control-test every auth scheme against a deliberately invalid credential.

Also worth knowing: documents/get/bydate returns an entire account history in one unpaginated response, and its records are identical to what the per-document detail endpoint returns — so a per-document loop is pure waste. And export/download with iExportType: 8 (Backup) answers 503 NotImplementedException; there is no whole-account backup on that route.

Contributing

Corrections to docs/api-surface.md are the most valuable contribution — especially anything that moves a row from inferred to confirmed. Please mark what you verified versus what you assumed, and never include account identifiers, tokens, or ingest addresses in an issue or PR.

Licence

MIT.