paperless-tax-mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,24 +1,30 @@
1
1
  # paperless-tax-mcp
2
2
 
3
- An **unofficial** MCP server for [paperless.tax](https://www.paperless.tax), the Israeli
4
- bookkeeping/expense platform.
3
+ An **unofficial** MCP server and CLI for [paperless.tax](https://www.paperless.tax), the
4
+ Israeli bookkeeping/expense platform.
5
5
 
6
6
  > ⚠️ Not affiliated with, endorsed by, or supported by Paperless. This talks to the app's
7
7
  > own private backend, not a published developer API, and can break without notice.
8
8
  > Use it against your own account.
9
9
 
10
- > 🚧 **Authentication is unresolved.** The self-service API key the UI issues is *not*
11
- > accepted as a bearer token on these endpoints — see
12
- > [`docs/api-surface.md`](docs/api-surface.md#authentication--unresolved). The server is
13
- > usable today only with a token taken from a logged-in browser session, which is
14
- > short-lived. Getting this to a durable credential is the project's main open problem;
15
- > help welcome.
16
-
17
10
  The value here is as much [`docs/api-surface.md`](docs/api-surface.md) as the code — no
18
- public documentation of this API exists anywhere, so the map is the point. The server
19
- grows as the surface gets documented.
11
+ public documentation of this API exists anywhere, so the map is the point.
12
+
13
+ ## Authentication
14
+
15
+ Use the **session JWT** the web app holds after login, as `Authorization: Bearer <jwt>`.
16
+
17
+ The self-service API key from the UI's API/webhooks modal is **not** a credential for these
18
+ endpoints — it returns 401 under every scheme, and paying for the API add-on does not change
19
+ that. Do not buy the add-on expecting it to help.
20
+
21
+ The good news, contrary to what you would assume of a "session" token: **its `exp` is about
22
+ ten months out**, so one interactive login covers scheduled work for most of a year. Login
23
+ is passwordless (email → entry code), so a fresh token needs a browser once; after that it
24
+ can be lifted from the browser profile.
25
+ [`docs/api-surface.md`](docs/api-surface.md#authentication--resolved) has both.
20
26
 
21
- ## Use
27
+ ## Use as an MCP server
22
28
 
23
29
  Register with any MCP client — Claude Code, Claude Desktop, or a gateway. No install step:
24
30
 
@@ -40,47 +46,91 @@ Claude Code, in one line:
40
46
  claude mcp add paperless-tax --env PAPERLESS_TOKEN=... -- npx -y paperless-tax-mcp
41
47
  ```
42
48
 
49
+ ## Use as a CLI
50
+
51
+ The same client, for exports and poking at the API:
52
+
53
+ ```bash
54
+ export PAPERLESS_TOKEN=...
55
+
56
+ npx paperless-tax whoami # verify the token, print the account id
57
+ npx paperless-tax probe # hit each read endpoint, report what answers
58
+ npx paperless-tax list --type 1 # expenses only
59
+ npx paperless-tax list --from 2025-01 --to 2025-12
60
+ npx paperless-tax export --out export/ # files plus metadata sidecars
61
+ npx paperless-tax raw POST documents/get/downloadurl --json '{"sDocID":"..."}'
62
+ ```
63
+
64
+ `export` writes `{docid}.pdf` plus a `{docid}.json` sidecar under
65
+ `export/expenses/{YYYY}/{YYYY-MM}/`, with a `manifest.jsonl` index. It is resumable —
66
+ documents already on disk are skipped — and the manifest is rewritten from scratch each run,
67
+ so it describes the whole export rather than only what that run fetched.
68
+
69
+ `--from`/`--to` accept `YYYY-MM-DD`, `YYYY-MM` or the API's native `YYMM`. Omit them to get
70
+ everything: the default range is the app's own unbounded `1001..2912`.
71
+
72
+ ### Use as a library
73
+
74
+ ```ts
75
+ import { PaperlessClient } from "paperless-tax-mcp/client";
76
+ import { Exporter } from "paperless-tax-mcp/export";
77
+
78
+ const client = new PaperlessClient({ token });
79
+ const docs = await client.byDate(); // whole history, one call
80
+ const pdfUrl = await client.downloadUrl(docs[0].id);
81
+ ```
82
+
43
83
  ### Configuration
44
84
 
45
85
  | Variable | Required | Meaning |
46
86
  |---|---|---|
47
- | `PAPERLESS_TOKEN` | yes | Bearer token — see the auth caveat above |
87
+ | `PAPERLESS_TOKEN` | yes | The session JWT — see above |
48
88
  | `PAPERLESS_USER_ID` | no | 10-char account id; `whoami` discovers it |
49
89
  | `PAPERLESS_BASE_URL` | no | Defaults to `https://api.paperless.tax/` |
50
90
 
51
91
  ### From source
52
92
 
53
93
  ```bash
54
- npm install && npm run build && node dist/index.js
94
+ npm install && npm run build && npm test
55
95
  ```
56
96
 
57
97
  ## Tools
58
98
 
59
99
  | Tool | Endpoint | Confidence |
60
100
  |---|---|---|
61
- | `whoami` | `GET users/get/params` | endpoint confirmed |
62
- | `pending_documents` | `GET documents/get/pending/{sUserID}` | endpoint confirmed |
63
- | `list_documents` | `POST bydate` | endpoint confirmed, body inferred |
64
- | `get_document` | `GET document/{id}` | inferred |
65
- | `get_download_url` | `POST downloadurl` | endpoint confirmed, response inferred |
101
+ | `whoami` | `GET users/get/params` | confirmed live |
102
+ | `pending_documents` | `GET documents/get/pending/{uid}` | confirmed live |
103
+ | `list_documents` | `POST documents/get/bydate` | confirmed live |
104
+ | `get_document` | `GET documents/get/document/{id}` | confirmed live |
105
+ | `get_download_url` | `POST documents/get/downloadurl` | confirmed live |
66
106
  | `raw_request` | anything | escape hatch for mapping new surface |
67
107
 
68
- ## Two things that will bite you
108
+ ## Four things that will bite you
109
+
110
+ All documented at length in `docs/api-surface.md`, and all of them cost real time to find:
69
111
 
70
- Both are documented at length in `docs/api-surface.md`, and both cost real time to find:
112
+ 1. **The controller prefix is not optional.** `POST /bydate` is a 404; the route is
113
+ `POST documents/get/bydate`. Same for `downloadurl`, `searchdp`, `document/{id}`.
114
+ 2. **A 401 does not mean the route exists.** Auth runs before routing, so unauthenticated
115
+ *every* path 401s — including nonsense ones. This is what produced the earlier, wrong
116
+ version of the endpoint map. Never infer existence from a 401 here.
117
+ 3. **`bydate` takes `YYMM` period strings, not ISO dates.** `sStart: "2506"` is June 2025.
118
+ Give it a date and you get a narrower result set rather than an error.
119
+ 4. **A 204 is not success.** Send no credentials at all and you get the same 204. Control-test
120
+ every auth scheme against a deliberately invalid credential.
71
121
 
72
- 1. **`bydate` returns an empty list for a bad parameter, not an error.** A zero-document
73
- response is indistinguishable from a genuinely empty range. Cross-check any count
74
- against the web UI before believing it.
75
- 2. **A 204 from this API is not success.** Send no credentials at all and you get the same
76
- 204. Control-test every auth scheme against a deliberately invalid credential.
122
+ Also worth knowing: `documents/get/bydate` returns an entire account history in one
123
+ unpaginated response, and its records are identical to what the per-document detail endpoint
124
+ returns so a per-document loop is pure waste. And `export/download` with
125
+ `iExportType: 8` (Backup) answers 503 `NotImplementedException`; there is no whole-account
126
+ backup on that route.
77
127
 
78
128
  ## Contributing
79
129
 
80
130
  Corrections to `docs/api-surface.md` are the most valuable contribution — especially
81
- anything that moves a row from *inferred* to *confirmed*, or that cracks authentication.
82
- Please mark what you verified versus what you assumed, and never include account
83
- identifiers, tokens, or ingest addresses in an issue or PR.
131
+ anything that moves a row from *inferred* to *confirmed*. Please mark what you verified
132
+ versus what you assumed, and never include account identifiers, tokens, or ingest addresses
133
+ in an issue or PR.
84
134
 
85
135
  ## Licence
86
136
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `paperless-tax` — command line access to the same client the MCP server uses.
4
+ *
5
+ * Deliberately dependency-free argument parsing: this package ships as an npx
6
+ * one-liner, and a CLI framework would be most of its install weight.
7
+ */
8
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `paperless-tax` — command line access to the same client the MCP server uses.
4
+ *
5
+ * Deliberately dependency-free argument parsing: this package ships as an npx
6
+ * one-liner, and a CLI framework would be most of its install weight.
7
+ */
8
+ import { PERIOD_MAX, PERIOD_MIN, PaperlessClient, PaperlessError, toPeriod } from "./client.js";
9
+ import { Exporter } from "./export.js";
10
+ const USAGE = `paperless-tax — unofficial CLI for api.paperless.tax
11
+
12
+ whoami Verify the token and print the account id
13
+ probe Hit each read endpoint once and report what answers
14
+ list [--from X] [--to Y] List documents without downloading
15
+ export [--out DIR] Export PDFs plus metadata sidecars
16
+ raw METHOD PATH [--json S] Call an arbitrary endpoint
17
+
18
+ Options
19
+ --from, --to YYYY-MM-DD, YYYY-MM or YYMM. Default is the app's unbounded
20
+ range (${PERIOD_MIN}..${PERIOD_MAX}), which returns everything.
21
+ --type N iDocType filter; 1 = expense
22
+ --out DIR Export destination (default: export)
23
+ --metadata-only Skip file downloads
24
+
25
+ Requires PAPERLESS_TOKEN — the SPA session JWT, not a self-service API key.
26
+ See docs/api-surface.md §Authentication.`;
27
+ function parseArgs(argv) {
28
+ const positional = [];
29
+ const flags = {};
30
+ for (let i = 0; i < argv.length; i++) {
31
+ const arg = argv[i];
32
+ if (arg.startsWith("--")) {
33
+ const key = arg.slice(2);
34
+ const next = argv[i + 1];
35
+ if (next && !next.startsWith("--")) {
36
+ flags[key] = next;
37
+ i++;
38
+ }
39
+ else {
40
+ flags[key] = true;
41
+ }
42
+ }
43
+ else {
44
+ positional.push(arg);
45
+ }
46
+ }
47
+ return { command: positional.shift(), positional, flags };
48
+ }
49
+ const period = (value, fallback) => typeof value === "string" ? toPeriod(value) : fallback;
50
+ async function main() {
51
+ const { command, positional, flags } = parseArgs(process.argv.slice(2));
52
+ if (!command || flags.help || command === "help") {
53
+ console.log(USAGE);
54
+ return command ? 0 : 2;
55
+ }
56
+ const client = new PaperlessClient();
57
+ switch (command) {
58
+ case "whoami": {
59
+ console.log(JSON.stringify(await client.getParams(), null, 2));
60
+ console.log(`\naccount id: ${await client.resolveUserId()}`);
61
+ return 0;
62
+ }
63
+ case "probe": {
64
+ const periodStart = period(flags.from, PERIOD_MIN);
65
+ const periodEnd = period(flags.to, PERIOD_MAX);
66
+ const checks = [
67
+ ["users/get/params", () => client.getParams()],
68
+ ["documents/get/pending", () => client.pending()],
69
+ ["documents/get/bydate", () => client.byDate({ periodStart, periodEnd })],
70
+ ];
71
+ for (const [name, call] of checks) {
72
+ try {
73
+ const result = await call();
74
+ const count = Array.isArray(result) ? `${result.length} record(s)` : "ok";
75
+ console.log(` ${name.padEnd(24)} ok — ${count}`);
76
+ }
77
+ catch (error) {
78
+ console.log(` ${name.padEnd(24)} ${error instanceof Error ? error.message : String(error)}`);
79
+ }
80
+ }
81
+ return 0;
82
+ }
83
+ case "list": {
84
+ let docs = await client.byDate({
85
+ periodStart: period(flags.from, PERIOD_MIN),
86
+ periodEnd: period(flags.to, PERIOD_MAX),
87
+ });
88
+ if (typeof flags.type === "string") {
89
+ docs = docs.filter((doc) => doc.iDocType === Number(flags.type));
90
+ }
91
+ for (const doc of docs) {
92
+ const amount = typeof doc.iAmount === "number" ? (doc.iAmount / 100).toFixed(2) : "";
93
+ console.log([
94
+ doc.id,
95
+ doc.sPeriod ?? "",
96
+ String(doc.iDocType ?? ""),
97
+ (doc.sSupplierName ?? "").slice(0, 28).padEnd(28),
98
+ amount.padStart(10),
99
+ ].join(" "));
100
+ }
101
+ console.log(`\n${docs.length} document(s)`);
102
+ return 0;
103
+ }
104
+ case "export": {
105
+ const out = typeof flags.out === "string" ? flags.out : "export";
106
+ console.log(`account: ${await client.resolveUserId()}`);
107
+ const exporter = new Exporter(client, out);
108
+ const stats = await exporter.exportRange({
109
+ periodStart: period(flags.from, PERIOD_MIN),
110
+ periodEnd: period(flags.to, PERIOD_MAX),
111
+ docType: typeof flags.type === "string" ? Number(flags.type) : undefined,
112
+ metadataOnly: Boolean(flags["metadata-only"]),
113
+ });
114
+ console.log(`\nlisted ${stats.listed} downloaded ${stats.downloaded} skipped ${stats.skipped} ` +
115
+ `no attachment ${stats.noAttachment} failed ${stats.failed} ` +
116
+ `(${(stats.bytes / 1e6).toFixed(1)} MB)`);
117
+ if (stats.failed)
118
+ console.error(`failures logged to ${exporter.errorsPath}`);
119
+ return stats.failed ? 1 : 0;
120
+ }
121
+ case "raw": {
122
+ const [method, path] = positional;
123
+ if (!method || !path) {
124
+ console.error("raw needs METHOD and PATH");
125
+ return 2;
126
+ }
127
+ let body;
128
+ if (typeof flags.json === "string") {
129
+ body = JSON.parse(flags.json);
130
+ if (body && typeof body === "object") {
131
+ const record = body;
132
+ if (record.sUserID === "auto")
133
+ record.sUserID = await client.resolveUserId();
134
+ }
135
+ }
136
+ console.log(JSON.stringify(await client.request(method, path, body), null, 2));
137
+ return 0;
138
+ }
139
+ default:
140
+ console.error(`unknown command: ${command}\n`);
141
+ console.error(USAGE);
142
+ return 2;
143
+ }
144
+ }
145
+ main()
146
+ .then((code) => process.exit(code))
147
+ .catch((error) => {
148
+ if (error instanceof PaperlessError) {
149
+ console.error(`error: ${error.message}`);
150
+ }
151
+ else {
152
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
153
+ }
154
+ process.exit(1);
155
+ });
156
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/client.d.ts CHANGED
@@ -1,17 +1,74 @@
1
1
  /**
2
- * Minimal HTTP client for api.paperless.tax.
2
+ * HTTP client for api.paperless.tax.
3
3
  *
4
- * Endpoint names were read out of the vendor's own Angular bundle; request and
5
- * response *shapes* are largely inferred, so nothing here assumes a field exists.
6
- * See docs/api-surface.md for what is confirmed versus guessed.
4
+ * Endpoint names and request shapes were read out of the vendor's own Angular
5
+ * bundle and then checked against the live API. See docs/api-surface.md for what
6
+ * is confirmed versus inferred, and for the traps that produced earlier wrong
7
+ * answers — chiefly that this API authenticates *before* it routes, so an
8
+ * unauthenticated 401 says nothing about whether a path exists.
7
9
  */
8
10
  export declare const DEFAULT_BASE_URL = "https://api.paperless.tax/";
11
+ /** Enum values read from the SPA bundle. */
12
+ export declare const DocType: {
13
+ readonly Undefined: 0;
14
+ readonly Expense: 1;
15
+ readonly Income: 2;
16
+ readonly Other: 3;
17
+ readonly IncomeExpense: 4;
18
+ };
19
+ export declare const DateMode: {
20
+ readonly Year: 1;
21
+ readonly Month: 2;
22
+ readonly Day: 3;
23
+ };
24
+ export declare const DateType: {
25
+ readonly ByPeriod: 0;
26
+ readonly ByTrans: 1;
27
+ };
28
+ export declare const ExportType: {
29
+ readonly BKMV: 1;
30
+ readonly Backup: 8;
31
+ readonly MOVEIN: 9;
32
+ readonly DocumentGroup: 12;
33
+ };
34
+ /** The app's own defaults for an unbounded range: YYMM, Jan 2010 → Dec 2029. */
35
+ export declare const PERIOD_MIN = "1001";
36
+ export declare const PERIOD_MAX = "2912";
37
+ /**
38
+ * Coerce YYYY-MM-DD / YYYY-MM / YYMM to the YYMM the API wants.
39
+ *
40
+ * `bydate` takes period strings, not dates. Given something else it returns a
41
+ * narrower set rather than complaining, so this throws instead of guessing.
42
+ */
43
+ export declare function toPeriod(value: string): string;
44
+ /** A document as returned by `documents/get/bydate`. Fields beyond `id` are optional. */
45
+ export interface PaperlessDocument {
46
+ id: string;
47
+ sID?: string;
48
+ sPeriod?: string;
49
+ dtTrans?: string;
50
+ dtCapture?: string;
51
+ iDocType?: number;
52
+ sDocNumber?: string;
53
+ sSupplierName?: string;
54
+ sCurrency?: string;
55
+ /** Integer agorot — iAmount 10341 is ₪103.41. */
56
+ iAmount?: number;
57
+ iVAT?: number;
58
+ iExpenseDeductible?: number;
59
+ aTags?: string[];
60
+ /** Page renders: _1000.jpg for page 1, then _1.jpg, _2.jpg … at 707×1000. */
61
+ aDocURLs?: string[];
62
+ sDocURLEdit?: string;
63
+ sDocTNURL?: string;
64
+ [key: string]: unknown;
65
+ }
9
66
  export declare class PaperlessError extends Error {
10
67
  status?: number;
11
68
  body?: string;
12
69
  constructor(message: string, status?: number, body?: string);
13
70
  }
14
- /** 401/403 see docs/api-surface.md §Authentication, which is unresolved. */
71
+ /** 401/403. A self-service API key is not a credential here; use the session JWT. */
15
72
  export declare class PaperlessAuthError extends PaperlessError {
16
73
  constructor(message: string, status?: number, body?: string);
17
74
  }
@@ -32,16 +89,82 @@ export declare class PaperlessClient {
32
89
  private maxRetries;
33
90
  private lastCall;
34
91
  constructor(options?: ClientOptions);
92
+ /**
93
+ * Join a path. Every route sits behind a controller prefix — the SPA builds
94
+ * URLs as base + controller + "/" + action + "/" + param, and the prefix is
95
+ * not optional: `POST /bydate` is a 404, `POST documents/get/bydate` is not.
96
+ */
35
97
  buildUrl(...parts: string[]): string;
36
98
  private throttle;
99
+ private headersFor;
100
+ /** Shared transport: throttle, retry on 5xx and network errors, classify failures. */
101
+ private send;
37
102
  request(method: string, path: string, body?: unknown): Promise<unknown>;
103
+ /** Fetch binary content — presigned S3 URLs and `export/download` blobs. */
104
+ requestBinary(method: string, path: string, body?: unknown): Promise<Uint8Array>;
38
105
  getParams(): Promise<unknown>;
106
+ /**
107
+ * The 10-character account id.
108
+ *
109
+ * `users/get/params` carries no `sUserID` field at all, despite that being the
110
+ * parameter name every other endpoint expects. The id is `sID` at the top
111
+ * level; the nested business record repeats it as "V_" + id.
112
+ */
39
113
  resolveUserId(): Promise<string>;
40
114
  pending(): Promise<unknown>;
41
- byDate(dateStart: string, dateEnd: string, docType?: number, extra?: Record<string, unknown>): Promise<unknown>;
115
+ /**
116
+ * `POST documents/get/bydate` — the listing call.
117
+ *
118
+ * Returns a bare JSON array with no envelope and no pagination; one call can
119
+ * carry an entire account history. The records are complete — `document()` on
120
+ * any of them returns the same field set, so there is no reason to follow up
121
+ * per document.
122
+ *
123
+ * Periods are YYMM strings, not ISO dates.
124
+ */
125
+ byDate(options?: {
126
+ periodStart?: string;
127
+ periodEnd?: string;
128
+ dateMode?: number;
129
+ dateType?: number;
130
+ extra?: Record<string, unknown>;
131
+ }): Promise<PaperlessDocument[]>;
132
+ /** `GET documents/get/document/{id}`. Same fields as the listing; for spot checks. */
42
133
  document(docId: string): Promise<unknown>;
43
- downloadUrl(docId: string, english?: boolean): Promise<unknown>;
134
+ /**
135
+ * `POST documents/get/downloadurl` — presigned URL for the document PDF.
136
+ *
137
+ * Answers with a bare JSON string. `english` asks for the `_eng.pdf` variant.
138
+ * Documents with nothing scanned against them have no PDF and this fails
139
+ * rather than returning an empty URL — check {@link hasAttachment} first.
140
+ */
141
+ downloadUrl(docId: string, english?: boolean): Promise<string>;
142
+ /** Fetch a presigned URL. No Authorization header — the signature does not cover one. */
143
+ fetchFile(url: string): Promise<Uint8Array>;
144
+ /**
145
+ * `POST export/download` with iExportType=12 — the document's pages as a zip
146
+ * of JPEGs. A lossy alternative to {@link downloadUrl}, kept because it is the
147
+ * app's own download path. Requires a doc id; without one the server answers
148
+ * 503 NotImplementedException rather than producing a bulk archive.
149
+ */
150
+ documentZip(docId: string): Promise<Uint8Array>;
44
151
  }
152
+ /**
153
+ * Whether a record has any file behind it.
154
+ *
155
+ * A sizeable minority are hand-keyed ledger entries with nothing scanned; they
156
+ * carry no render URLs, and `downloadurl` errors rather than returning nothing.
157
+ */
158
+ export declare function hasAttachment(doc: PaperlessDocument): boolean;
159
+ /** Pull the URL out of a `downloadurl` response — a bare string, or wrapped. */
160
+ export declare function resolveFileUrl(payload: unknown): string | undefined;
161
+ /**
162
+ * Locate the document list in a response.
163
+ *
164
+ * `bydate` returns a bare array, but other endpoints wrap their payload, so this
165
+ * finds the longest document-like list by structure rather than by a fixed path.
166
+ */
167
+ export declare function extractDocuments(payload: unknown): PaperlessDocument[];
45
168
  /**
46
169
  * Depth-first search for a key anywhere in a nested structure. Response
47
170
  * envelopes are inconsistent between endpoints, so locating a field by name is