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
| Variable | Required | Meaning |
|---|---|---|
PAPERLESS_TOKEN | yes | The session JWT — see above |
PAPERLESS_USER_ID | no | 10-char account id; whoami discovers it |
PAPERLESS_BASE_URL | no | Defaults to https://api.paperless.tax/ |
From source
npm install && npm run build && npm test
Tools
| Tool | Endpoint | Confidence |
|---|---|---|
whoami | GET users/get/params | confirmed live |
pending_documents | GET documents/get/pending/{uid} | confirmed live |
list_documents | POST documents/get/bydate | confirmed live |
get_document | GET documents/get/document/{id} | confirmed live |
get_download_url | POST documents/get/downloadurl | confirmed live |
raw_request | anything | escape 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:
- The controller prefix is not optional.
POST /bydateis a 404; the route isPOST documents/get/bydate. Same fordownloadurl,searchdp,document/{id}. - 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.
bydatetakesYYMMperiod strings, not ISO dates.sStart: "2506"is June 2025. Give it a date and you get a narrower result set rather than an error.- 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.