@baruchiro/paperless-mcp 1.0.0 → 2.0.1

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.
@@ -1,11 +1,59 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
13
  };
5
14
  Object.defineProperty(exports, "__esModule", { value: true });
6
15
  const strict_1 = __importDefault(require("node:assert/strict"));
16
+ const node_fs_1 = require("node:fs");
17
+ const node_os_1 = require("node:os");
18
+ const node_path_1 = require("node:path");
7
19
  const node_test_1 = require("node:test");
20
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
21
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
8
22
  const documents_1 = require("./documents");
23
+ const documentQuery_1 = require("./utils/documentQuery");
24
+ function getQueryParams(queryString) {
25
+ return new URLSearchParams(queryString.replace(/^\?/, ""));
26
+ }
27
+ function getDocumentQueryParamsFromOpenApi() {
28
+ const openApiPath = (0, node_path_1.join)(process.cwd(), "Paperless_ngx_REST_API.yaml");
29
+ const text = (0, node_fs_1.readFileSync)(openApiPath, "utf8");
30
+ const start = text.indexOf(" /api/documents/:");
31
+ const end = text.indexOf(" /api/documents/{id}/:");
32
+ strict_1.default.ok(start >= 0, "OpenAPI docs marker '/api/documents/' not found");
33
+ strict_1.default.ok(end > start, "OpenAPI docs marker '/api/documents/{id}/' not found or out of order");
34
+ const section = text.slice(start, end);
35
+ return Array.from(section.matchAll(/^\s*-?\s*name:\s+(.+)$/gm), (match) => match[1]).sort();
36
+ }
37
+ // ALLOWED_UPLOAD_PATHS is read from the environment at module load, so the
38
+ // allowlist can only be exercised by re-importing the module with the env set.
39
+ function validateFilePathWithAllowlist(allowedPaths) {
40
+ const modulePath = require.resolve("./documents");
41
+ const previous = process.env.PAPERLESS_MCP_UPLOAD_PATHS;
42
+ process.env.PAPERLESS_MCP_UPLOAD_PATHS = allowedPaths;
43
+ delete require.cache[modulePath];
44
+ try {
45
+ return require("./documents").validateFilePath;
46
+ }
47
+ finally {
48
+ delete require.cache[modulePath];
49
+ if (previous === undefined) {
50
+ delete process.env.PAPERLESS_MCP_UPLOAD_PATHS;
51
+ }
52
+ else {
53
+ process.env.PAPERLESS_MCP_UPLOAD_PATHS = previous;
54
+ }
55
+ }
56
+ }
9
57
  (0, node_test_1.test)("buildBulkEditParameters sends Paperless bulk custom fields as id:value map", () => {
10
58
  const parameters = (0, documents_1.buildBulkEditParameters)({ remove_custom_fields: [] }, [
11
59
  { field: 9, value: "" },
@@ -76,3 +124,359 @@ const documents_1 = require("./documents");
76
124
  "5": [123, 456],
77
125
  });
78
126
  });
127
+ (0, node_test_1.test)("paperless filter allowlist stays in sync with the document OpenAPI section", () => {
128
+ const documentedParams = getDocumentQueryParamsFromOpenApi();
129
+ const allowedParams = [...documentQuery_1.DOCUMENT_QUERY_PAPERLESS_FILTER_KEYS].sort();
130
+ strict_1.default.deepEqual(allowedParams, documentedParams);
131
+ });
132
+ (0, node_test_1.test)("serializes full-text query_documents arguments", () => {
133
+ const query = getQueryParams((0, documentQuery_1.buildDocumentQueryString)({
134
+ query: "invoice 2024",
135
+ search: "jan",
136
+ more_like_id: 42,
137
+ }));
138
+ strict_1.default.equal(query.get("query"), "invoice 2024");
139
+ strict_1.default.equal(query.get("search"), "jan");
140
+ strict_1.default.equal(query.get("more_like_id"), "42");
141
+ });
142
+ (0, node_test_1.test)("serializes first-class document filters using Paperless parameter names", () => {
143
+ const query = getQueryParams((0, documentQuery_1.buildDocumentQueryString)({
144
+ page: 2,
145
+ page_size: 50,
146
+ ordering: "-created",
147
+ correspondent: 3,
148
+ document_type: 4,
149
+ tag: 5,
150
+ storage_path: 6,
151
+ created__date__gte: "2024-01-01",
152
+ created__date__lte: "2024-12-31",
153
+ archive_serial_number: 99,
154
+ archive_serial_number__isnull: false,
155
+ custom_fields__icontains: "invoice",
156
+ }));
157
+ strict_1.default.equal(query.get("page"), "2");
158
+ strict_1.default.equal(query.get("page_size"), "50");
159
+ strict_1.default.equal(query.get("ordering"), "-created");
160
+ strict_1.default.equal(query.get("correspondent__id"), "3");
161
+ strict_1.default.equal(query.get("document_type__id"), "4");
162
+ strict_1.default.equal(query.get("tags__id"), "5");
163
+ strict_1.default.equal(query.get("storage_path__id"), "6");
164
+ strict_1.default.equal(query.get("created__date__gte"), "2024-01-01");
165
+ strict_1.default.equal(query.get("created__date__lte"), "2024-12-31");
166
+ strict_1.default.equal(query.get("archive_serial_number"), "99");
167
+ strict_1.default.equal(query.get("archive_serial_number__isnull"), "false");
168
+ strict_1.default.equal(query.get("custom_fields__icontains"), "invoice");
169
+ });
170
+ (0, node_test_1.test)("serializes paperless_filters arrays as comma-separated values", () => {
171
+ const query = getQueryParams((0, documentQuery_1.buildDocumentQueryString)({
172
+ paperless_filters: {
173
+ fields: ["title", "tags"],
174
+ id__in: [1, 2, 3],
175
+ },
176
+ }));
177
+ strict_1.default.equal(query.get("fields"), "title,tags");
178
+ strict_1.default.equal(query.get("id__in"), "1,2,3");
179
+ });
180
+ (0, node_test_1.test)("serializes raw list custom_field_query strings without JSON encoding", () => {
181
+ const rawCustomFieldQuery = '[7, "icontains", "value"]';
182
+ const query = getQueryParams((0, documentQuery_1.buildDocumentQueryString)({
183
+ custom_field_query: rawCustomFieldQuery,
184
+ }));
185
+ strict_1.default.equal(query.get("custom_field_query"), rawCustomFieldQuery);
186
+ });
187
+ (0, node_test_1.test)("serializes leaf custom_field_query values as JSON", () => {
188
+ const query = getQueryParams((0, documentQuery_1.buildDocumentQueryString)({
189
+ custom_field_query: ["Invoice Number", "exact", "12345"],
190
+ }));
191
+ strict_1.default.equal(query.get("custom_field_query"), JSON.stringify(["Invoice Number", "exact", "12345"]));
192
+ });
193
+ (0, node_test_1.test)("serializes numeric custom_field_query field IDs as JSON", () => {
194
+ const query = getQueryParams((0, documentQuery_1.buildDocumentQueryString)({
195
+ custom_field_query: [7, "exact", "12345"],
196
+ }));
197
+ strict_1.default.equal(query.get("custom_field_query"), JSON.stringify([7, "exact", "12345"]));
198
+ });
199
+ (0, node_test_1.test)("serializes grouped custom_field_query values as JSON", () => {
200
+ const groupedQuery = [
201
+ "OR",
202
+ [
203
+ ["Invoice Number", "isnull", true],
204
+ ["Invoice Number", "exact", ""],
205
+ ],
206
+ ];
207
+ const query = getQueryParams((0, documentQuery_1.buildDocumentQueryString)({
208
+ custom_field_query: groupedQuery,
209
+ }));
210
+ strict_1.default.equal(query.get("custom_field_query"), JSON.stringify(groupedQuery));
211
+ });
212
+ (0, node_test_1.test)("rejects unsupported paperless_filters keys", () => {
213
+ strict_1.default.throws(() => (0, documentQuery_1.buildDocumentQueryString)({
214
+ paperless_filters: {
215
+ not_a_real_filter: "value",
216
+ },
217
+ }), /Unsupported paperless_filters key/);
218
+ });
219
+ (0, node_test_1.test)("rejects duplicate first-class and paperless_filters definitions", () => {
220
+ strict_1.default.throws(() => (0, documentQuery_1.buildDocumentQueryString)({
221
+ correspondent: 7,
222
+ paperless_filters: {
223
+ correspondent__id: 7,
224
+ },
225
+ }), /Duplicate filter 'correspondent__id'/);
226
+ });
227
+ (0, node_test_1.test)("rejects invalid custom_field_query shapes", () => {
228
+ strict_1.default.equal(documentQuery_1.customFieldQuerySchema.safeParse(["field", "exact"]).success, false);
229
+ strict_1.default.equal(documentQuery_1.customFieldQuerySchema.safeParse(["AND", []]).success, false);
230
+ strict_1.default.equal(documentQuery_1.customFieldQuerySchema.safeParse(["AND", [["field"]]]).success, false);
231
+ strict_1.default.equal(documentQuery_1.customFieldQuerySchema.safeParse(["AND", "iexact", "foo"]).success, false);
232
+ });
233
+ (0, node_test_1.describe)("validateFilePath", () => {
234
+ let testDir;
235
+ let testFile;
236
+ let emptyFile;
237
+ let symlinkPath;
238
+ (0, node_test_1.before)(() => {
239
+ testDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "paperless-mcp-test-"));
240
+ testFile = (0, node_path_1.join)(testDir, "test.pdf");
241
+ (0, node_fs_1.writeFileSync)(testFile, "%PDF-1.4\ntest content");
242
+ emptyFile = (0, node_path_1.join)(testDir, "empty.pdf");
243
+ (0, node_fs_1.writeFileSync)(emptyFile, "");
244
+ symlinkPath = (0, node_path_1.join)(testDir, "link.pdf");
245
+ try {
246
+ (0, node_fs_1.symlinkSync)(testFile, symlinkPath);
247
+ }
248
+ catch (_a) {
249
+ // Skip if unsupported.
250
+ }
251
+ });
252
+ (0, node_test_1.after)(() => {
253
+ (0, node_fs_1.rmSync)(testDir, { recursive: true, force: true });
254
+ });
255
+ (0, node_test_1.test)("accepts a valid absolute file path", () => __awaiter(void 0, void 0, void 0, function* () {
256
+ yield strict_1.default.doesNotReject(() => (0, documents_1.validateFilePath)(testFile));
257
+ }));
258
+ (0, node_test_1.test)("rejects relative paths", () => __awaiter(void 0, void 0, void 0, function* () {
259
+ yield strict_1.default.rejects(() => (0, documents_1.validateFilePath)("relative/path.pdf"), {
260
+ message: "file_path must be an absolute path",
261
+ });
262
+ }));
263
+ (0, node_test_1.test)("rejects non-existent files", () => __awaiter(void 0, void 0, void 0, function* () {
264
+ yield strict_1.default.rejects(() => (0, documents_1.validateFilePath)((0, node_path_1.join)(testDir, "missing.pdf")), { message: "File not found" });
265
+ }));
266
+ (0, node_test_1.test)("rejects directories", () => __awaiter(void 0, void 0, void 0, function* () {
267
+ yield strict_1.default.rejects(() => (0, documents_1.validateFilePath)(testDir), {
268
+ message: "Path must point to a regular file",
269
+ });
270
+ }));
271
+ (0, node_test_1.test)("rejects empty files", () => __awaiter(void 0, void 0, void 0, function* () {
272
+ yield strict_1.default.rejects(() => (0, documents_1.validateFilePath)(emptyFile), {
273
+ message: "File is empty",
274
+ });
275
+ }));
276
+ (0, node_test_1.test)("resolves symlinks to the real file", () => __awaiter(void 0, void 0, void 0, function* () {
277
+ try {
278
+ yield strict_1.default.doesNotReject(() => (0, documents_1.validateFilePath)(symlinkPath));
279
+ }
280
+ catch (_a) {
281
+ // Skip on systems without symlink support.
282
+ }
283
+ }));
284
+ (0, node_test_1.test)("rejects files exceeding the maximum size", () => __awaiter(void 0, void 0, void 0, function* () {
285
+ const largeFile = (0, node_path_1.join)(testDir, "large.pdf");
286
+ (0, node_fs_1.writeFileSync)(largeFile, "%PDF-1.4\n");
287
+ (0, node_fs_1.truncateSync)(largeFile, 101 * 1024 * 1024);
288
+ yield strict_1.default.rejects(() => (0, documents_1.validateFilePath)(largeFile), {
289
+ message: /exceeds maximum allowed size/,
290
+ });
291
+ }));
292
+ (0, node_test_1.test)("accepts files within allowed upload paths", () => __awaiter(void 0, void 0, void 0, function* () {
293
+ const validate = validateFilePathWithAllowlist((0, node_fs_1.realpathSync)(testDir));
294
+ yield strict_1.default.doesNotReject(() => validate(testFile));
295
+ }));
296
+ (0, node_test_1.test)("rejects files outside allowed upload paths", () => __awaiter(void 0, void 0, void 0, function* () {
297
+ const validate = validateFilePathWithAllowlist("/some/other/path");
298
+ yield strict_1.default.rejects(() => validate(testFile), {
299
+ message: /outside allowed upload directories/,
300
+ });
301
+ }));
302
+ });
303
+ class TestTransport {
304
+ start() {
305
+ return __awaiter(this, void 0, void 0, function* () { });
306
+ }
307
+ send(message) {
308
+ return __awaiter(this, void 0, void 0, function* () {
309
+ queueMicrotask(() => { var _a, _b; return (_b = (_a = this.peer) === null || _a === void 0 ? void 0 : _a.onmessage) === null || _b === void 0 ? void 0 : _b.call(_a, message); });
310
+ });
311
+ }
312
+ close() {
313
+ return __awaiter(this, void 0, void 0, function* () {
314
+ var _a;
315
+ (_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
316
+ });
317
+ }
318
+ }
319
+ function createTransportPair() {
320
+ const clientTransport = new TestTransport();
321
+ const serverTransport = new TestTransport();
322
+ clientTransport.peer = serverTransport;
323
+ serverTransport.peer = clientTransport;
324
+ return { clientTransport, serverTransport };
325
+ }
326
+ function parseToolText(result) {
327
+ var _a;
328
+ const item = (_a = result.content) === null || _a === void 0 ? void 0 : _a[0];
329
+ if (!item || item.type !== "text") {
330
+ throw new Error("Expected text tool response");
331
+ }
332
+ return JSON.parse(item.text);
333
+ }
334
+ function withDocumentClient(api, run) {
335
+ return __awaiter(this, void 0, void 0, function* () {
336
+ const server = new mcp_js_1.McpServer({ name: "paperless-doc-test", version: "1.0.0" });
337
+ (0, documents_1.registerDocumentTools)(server, api);
338
+ const client = new index_js_1.Client({
339
+ name: "paperless-doc-test-client",
340
+ version: "1.0.0",
341
+ });
342
+ const { clientTransport, serverTransport } = createTransportPair();
343
+ yield server.connect(serverTransport);
344
+ yield client.connect(clientTransport);
345
+ try {
346
+ yield run(client);
347
+ }
348
+ finally {
349
+ yield client.close();
350
+ yield server.close();
351
+ }
352
+ });
353
+ }
354
+ function createDocumentApi(fields) {
355
+ const calls = {
356
+ updateDocument: [],
357
+ bulkEditDocuments: [],
358
+ getCustomField: [],
359
+ };
360
+ const fieldMap = new Map(fields.map((field) => [field.id, field]));
361
+ const api = {
362
+ getCustomField: (id) => __awaiter(this, void 0, void 0, function* () {
363
+ calls.getCustomField.push(id);
364
+ const field = fieldMap.get(id);
365
+ if (!field)
366
+ throw new Error(`custom field ${id} not found`);
367
+ return field;
368
+ }),
369
+ updateDocument: (id, data) => __awaiter(this, void 0, void 0, function* () {
370
+ var _a;
371
+ calls.updateDocument.push([id, data]);
372
+ return { id, custom_fields: (_a = data.custom_fields) !== null && _a !== void 0 ? _a : [] };
373
+ }),
374
+ bulkEditDocuments: (documents, method, parameters) => __awaiter(this, void 0, void 0, function* () {
375
+ calls.bulkEditDocuments.push([documents, method, parameters]);
376
+ return { result: "OK" };
377
+ }),
378
+ getCorrespondents: () => __awaiter(this, void 0, void 0, function* () { return ({ results: [] }); }),
379
+ getDocumentTypes: () => __awaiter(this, void 0, void 0, function* () { return ({ results: [] }); }),
380
+ getTags: () => __awaiter(this, void 0, void 0, function* () { return ({ results: [] }); }),
381
+ getCustomFields: () => __awaiter(this, void 0, void 0, function* () { return ({ results: [] }); }),
382
+ };
383
+ return { api, calls };
384
+ }
385
+ const LEGACY_SELECT_FIELD = {
386
+ id: 2,
387
+ name: "Retention period",
388
+ data_type: "select",
389
+ extra_data: { select_options: ["1 year", "7 years", "2 years"], default_currency: null },
390
+ document_count: 10,
391
+ };
392
+ const OBJECT_SELECT_FIELD = {
393
+ id: 3,
394
+ name: "Priority",
395
+ data_type: "select",
396
+ extra_data: {
397
+ select_options: [
398
+ { id: "abc123", label: "Low" },
399
+ { id: "def456", label: "High" },
400
+ ],
401
+ },
402
+ document_count: 5,
403
+ };
404
+ (0, node_test_1.describe)("select custom field value resolution in document handlers", () => {
405
+ (0, node_test_1.test)("update_document translates a select label to its zero-based index", () => __awaiter(void 0, void 0, void 0, function* () {
406
+ const { api, calls } = createDocumentApi([LEGACY_SELECT_FIELD]);
407
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
408
+ var _a;
409
+ const result = (yield client.callTool({
410
+ name: "update_document",
411
+ arguments: { id: 42, custom_fields: [{ field: 2, value: "1 year" }] },
412
+ }));
413
+ strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
414
+ }));
415
+ strict_1.default.equal(calls.updateDocument.length, 1);
416
+ const [, data] = calls.updateDocument[0];
417
+ strict_1.default.deepEqual(data.custom_fields, [{ field: 2, value: 0 }]);
418
+ }));
419
+ (0, node_test_1.test)("update_document sends the option id for 2.17+ select fields (stored form)", () => __awaiter(void 0, void 0, void 0, function* () {
420
+ const { api, calls } = createDocumentApi([OBJECT_SELECT_FIELD]);
421
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
422
+ var _a;
423
+ const result = (yield client.callTool({
424
+ name: "update_document",
425
+ arguments: { id: 7, custom_fields: [{ field: 3, value: "High" }] },
426
+ }));
427
+ strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
428
+ }));
429
+ const [, data] = calls.updateDocument[0];
430
+ strict_1.default.deepEqual(data.custom_fields, [{ field: 3, value: "def456" }]);
431
+ }));
432
+ (0, node_test_1.test)("bulk_edit_documents translates a select label in add_custom_fields", () => __awaiter(void 0, void 0, void 0, function* () {
433
+ const { api, calls } = createDocumentApi([LEGACY_SELECT_FIELD]);
434
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
435
+ var _a;
436
+ const result = (yield client.callTool({
437
+ name: "bulk_edit_documents",
438
+ arguments: {
439
+ documents: [1, 2],
440
+ method: "modify_custom_fields",
441
+ add_custom_fields: [{ field: 2, value: "7 years" }],
442
+ },
443
+ }));
444
+ strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
445
+ }));
446
+ strict_1.default.equal(calls.bulkEditDocuments.length, 1);
447
+ const [, , parameters] = calls.bulkEditDocuments[0];
448
+ strict_1.default.deepEqual(parameters.add_custom_fields, { "2": 1 });
449
+ }));
450
+ (0, node_test_1.test)("bulk_edit_documents sends the option id for 2.17+ select fields (stored form)", () => __awaiter(void 0, void 0, void 0, function* () {
451
+ const { api, calls } = createDocumentApi([OBJECT_SELECT_FIELD]);
452
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
453
+ var _a;
454
+ const result = (yield client.callTool({
455
+ name: "bulk_edit_documents",
456
+ arguments: {
457
+ documents: [1],
458
+ method: "modify_custom_fields",
459
+ add_custom_fields: [{ field: 3, value: "High" }],
460
+ },
461
+ }));
462
+ strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
463
+ }));
464
+ const [, , parameters] = calls.bulkEditDocuments[0];
465
+ strict_1.default.deepEqual(parameters.add_custom_fields, { "3": "def456" });
466
+ }));
467
+ (0, node_test_1.test)("update_document rejects an unknown select option with a helpful error", () => __awaiter(void 0, void 0, void 0, function* () {
468
+ const { api, calls } = createDocumentApi([LEGACY_SELECT_FIELD]);
469
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
470
+ var _a, _b;
471
+ const result = (yield client.callTool({
472
+ name: "update_document",
473
+ arguments: { id: 42, custom_fields: [{ field: 2, value: "forever" }] },
474
+ }));
475
+ strict_1.default.ok(result.isError, "expected an error for an unknown select option");
476
+ const message = (_b = (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error) !== null && _b !== void 0 ? _b : "";
477
+ strict_1.default.match(message, /forever/);
478
+ strict_1.default.match(message, /1 year/);
479
+ }));
480
+ strict_1.default.equal(calls.updateDocument.length, 0, "no document update should be sent when the option is invalid");
481
+ }));
482
+ });
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
2
+ import { PaperlessAPI } from "../api/PaperlessAPI";
3
+ export declare function registerMailTools(server: McpServer, api: PaperlessAPI): void;
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.registerMailTools = registerMailTools;
24
+ const zod_1 = require("zod");
25
+ const middlewares_1 = require("./utils/middlewares");
26
+ const queryString_1 = require("./utils/queryString");
27
+ const MAIL_RULE_ACTION_DESCRIPTION = "Mail rule action: 1=Delete, 2=Move to specified folder, 3=Mark as read/don't process read mails, 4=Flag/don't process flagged mails, 5=Tag/don't process tagged mails";
28
+ const ASSIGN_TITLE_FROM_DESCRIPTION = "Title assignment: 1=Use subject as title, 2=Use attachment filename as title, 3=Do not assign title from rule";
29
+ const ASSIGN_CORRESPONDENT_FROM_DESCRIPTION = "Correspondent assignment: 1=Do not assign, 2=Use mail address, 3=Use sender name or address, 4=Use assign_correspondent";
30
+ const ATTACHMENT_TYPE_DESCRIPTION = "Attachment type: 1=Only process attachments, 2=Process all files including inline attachments";
31
+ const CONSUMPTION_SCOPE_DESCRIPTION = "Consumption scope: 1=Only process attachments, 2=Process full mail as .eml, 3=Process full mail and attachments separately";
32
+ const PDF_LAYOUT_DESCRIPTION = "PDF layout for full-mail consumption: 0=System default, 1=Text then HTML, 2=HTML then text, 3=HTML only, 4=Text only";
33
+ const mailRuleFields = {
34
+ name: zod_1.z.string().optional(),
35
+ account: zod_1.z.number().int().optional(),
36
+ enabled: zod_1.z.boolean().optional(),
37
+ folder: zod_1.z.string().optional(),
38
+ filter_from: zod_1.z.string().nullable().optional(),
39
+ filter_to: zod_1.z.string().nullable().optional(),
40
+ filter_subject: zod_1.z.string().nullable().optional(),
41
+ filter_body: zod_1.z.string().nullable().optional(),
42
+ filter_attachment_filename_include: zod_1.z.string().nullable().optional(),
43
+ filter_attachment_filename_exclude: zod_1.z.string().nullable().optional(),
44
+ maximum_age: zod_1.z.number().int().min(0).optional(),
45
+ action: zod_1.z
46
+ .number()
47
+ .int()
48
+ .min(1)
49
+ .max(5)
50
+ .optional()
51
+ .describe(MAIL_RULE_ACTION_DESCRIPTION),
52
+ action_parameter: zod_1.z.string().nullable().optional(),
53
+ assign_title_from: zod_1.z
54
+ .number()
55
+ .int()
56
+ .min(1)
57
+ .max(3)
58
+ .optional()
59
+ .describe(ASSIGN_TITLE_FROM_DESCRIPTION),
60
+ assign_tags: zod_1.z.array(zod_1.z.number().int().nullable()).optional(),
61
+ assign_correspondent_from: zod_1.z
62
+ .number()
63
+ .int()
64
+ .min(1)
65
+ .max(4)
66
+ .optional()
67
+ .describe(ASSIGN_CORRESPONDENT_FROM_DESCRIPTION),
68
+ assign_correspondent: zod_1.z.number().int().nullable().optional(),
69
+ assign_document_type: zod_1.z.number().int().nullable().optional(),
70
+ assign_owner_from_rule: zod_1.z.boolean().optional(),
71
+ order: zod_1.z.number().int().optional(),
72
+ attachment_type: zod_1.z
73
+ .number()
74
+ .int()
75
+ .min(1)
76
+ .max(2)
77
+ .optional()
78
+ .describe(ATTACHMENT_TYPE_DESCRIPTION),
79
+ consumption_scope: zod_1.z
80
+ .number()
81
+ .int()
82
+ .min(1)
83
+ .max(3)
84
+ .optional()
85
+ .describe(CONSUMPTION_SCOPE_DESCRIPTION),
86
+ pdf_layout: zod_1.z
87
+ .number()
88
+ .int()
89
+ .min(0)
90
+ .max(4)
91
+ .optional()
92
+ .describe(PDF_LAYOUT_DESCRIPTION),
93
+ owner: zod_1.z.number().int().nullable().optional(),
94
+ };
95
+ function registerMailTools(server, api) {
96
+ server.tool("list_mail_accounts", "List Paperless mail accounts for selecting the account ID needed by mail rules. Does not expose account passwords.", {
97
+ page: zod_1.z.number().optional(),
98
+ page_size: zod_1.z.number().optional(),
99
+ }, (0, middlewares_1.withErrorHandling)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) {
100
+ if (!api)
101
+ throw new Error("Please configure API connection first");
102
+ const queryString = (0, queryString_1.buildQueryString)(args);
103
+ const response = yield api.getMailAccounts(queryString);
104
+ const sanitizedResults = (response.results || []).map((account) => (Object.assign(Object.assign({}, account), { password: undefined })));
105
+ return {
106
+ content: [
107
+ {
108
+ type: "text",
109
+ text: JSON.stringify(Object.assign(Object.assign({}, response), { results: sanitizedResults })),
110
+ },
111
+ ],
112
+ };
113
+ })));
114
+ server.tool("get_mail_account", "Get one Paperless mail account by ID. Password/token fields are redacted if the server returns them.", { id: zod_1.z.number().int() }, (0, middlewares_1.withErrorHandling)((args) => __awaiter(this, void 0, void 0, function* () {
115
+ if (!api)
116
+ throw new Error("Please configure API connection first");
117
+ const _a = yield api.getMailAccount(args.id), { password } = _a, account = __rest(_a, ["password"]);
118
+ return {
119
+ content: [{ type: "text", text: JSON.stringify(account) }],
120
+ };
121
+ })));
122
+ server.tool("process_mail_account", "Manually run Paperless mail processing for one account. This can consume matching mails according to enabled Paperless mail rules.", { id: zod_1.z.number().int() }, (0, middlewares_1.withErrorHandling)((args) => __awaiter(this, void 0, void 0, function* () {
123
+ if (!api)
124
+ throw new Error("Please configure API connection first");
125
+ yield api.processMailAccount(args.id);
126
+ return {
127
+ content: [
128
+ { type: "text", text: JSON.stringify({ status: "processed" }) },
129
+ ],
130
+ };
131
+ })));
132
+ server.tool("list_mail_rules", "List Paperless mail rules with optional pagination.", {
133
+ page: zod_1.z.number().optional(),
134
+ page_size: zod_1.z.number().optional(),
135
+ }, (0, middlewares_1.withErrorHandling)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) {
136
+ if (!api)
137
+ throw new Error("Please configure API connection first");
138
+ const queryString = (0, queryString_1.buildQueryString)(args);
139
+ const response = yield api.getMailRules(queryString);
140
+ return {
141
+ content: [{ type: "text", text: JSON.stringify(response) }],
142
+ };
143
+ })));
144
+ server.tool("get_mail_rule", "Get one Paperless mail rule by ID.", { id: zod_1.z.number().int() }, (0, middlewares_1.withErrorHandling)((args) => __awaiter(this, void 0, void 0, function* () {
145
+ if (!api)
146
+ throw new Error("Please configure API connection first");
147
+ const response = yield api.getMailRule(args.id);
148
+ return {
149
+ content: [{ type: "text", text: JSON.stringify(response) }],
150
+ };
151
+ })));
152
+ server.tool("create_mail_rule", "Create a Paperless mail rule. Use list_mail_accounts first to choose account. Prefer attachment-only rules for invoices unless the full mail must be archived.", Object.assign(Object.assign({}, mailRuleFields), { name: zod_1.z.string(), account: zod_1.z.number().int(), folder: zod_1.z.string() }), (0, middlewares_1.withErrorHandling)((args) => __awaiter(this, void 0, void 0, function* () {
153
+ if (!api)
154
+ throw new Error("Please configure API connection first");
155
+ const response = yield api.createMailRule(args);
156
+ return {
157
+ content: [{ type: "text", text: JSON.stringify(response) }],
158
+ };
159
+ })));
160
+ server.tool("update_mail_rule", "Patch an existing Paperless mail rule. Only supplied fields are changed.", Object.assign({ id: zod_1.z.number().int() }, mailRuleFields), (0, middlewares_1.withErrorHandling)((args) => __awaiter(this, void 0, void 0, function* () {
161
+ if (!api)
162
+ throw new Error("Please configure API connection first");
163
+ const { id } = args, data = __rest(args, ["id"]);
164
+ const response = yield api.updateMailRule(id, data);
165
+ return {
166
+ content: [{ type: "text", text: JSON.stringify(response) }],
167
+ };
168
+ })));
169
+ server.tool("delete_mail_rule", "Delete one Paperless mail rule. This changes future mail ingestion behavior but does not delete documents.", {
170
+ id: zod_1.z.number().int(),
171
+ confirm: zod_1.z
172
+ .boolean()
173
+ .describe("Must be true to confirm deleting the rule"),
174
+ }, (0, middlewares_1.withErrorHandling)((args) => __awaiter(this, void 0, void 0, function* () {
175
+ if (!api)
176
+ throw new Error("Please configure API connection first");
177
+ if (!args.confirm) {
178
+ throw new Error("Confirmation required. Set confirm: true to delete the mail rule.");
179
+ }
180
+ yield api.deleteMailRule(args.id);
181
+ return {
182
+ content: [
183
+ { type: "text", text: JSON.stringify({ status: "deleted" }) },
184
+ ],
185
+ };
186
+ })));
187
+ }
@@ -0,0 +1 @@
1
+ export {};