@baruchiro/paperless-mcp 2.0.1 → 2.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
@@ -252,7 +252,9 @@ Parameters:
252
252
  - tag: ID for add_tag/remove_tag
253
253
  - add_tags: Array of tag IDs for modify_tags
254
254
  - remove_tags: Array of tag IDs for modify_tags
255
- - permissions: Object for set_permissions with owner, permissions, merge flag
255
+ - set_permissions: Object for set_permissions with view/change users and groups (`{"view": {"users": [], "groups": []}, "change": {...}}`). Omitted actions/lists are left untouched
256
+ - owner: User ID (or null to remove) for set_permissions. Unless merge is true, omitting owner clears the current owner
257
+ - merge: Boolean for set_permissions — true adds to existing permissions and keeps the owner; false (default) replaces the listed users/groups
256
258
  - metadata_document_id: ID for merge to specify metadata source
257
259
  - delete_originals: Boolean for merge/split
258
260
  - pages: String for split "[1,2-3,4,5-7]" or delete_pages "[2,3,4]"
@@ -7,7 +7,7 @@ export declare class PaperlessAPI {
7
7
  constructor(baseUrl: string, token: string);
8
8
  request<T = any>(path: string, options?: RequestInit): Promise<T>;
9
9
  bulkEditDocuments(documents: number[], method: string, parameters?: BulkEditParameters): Promise<BulkEditDocumentsResult>;
10
- postDocument(document: Buffer, filename: string, metadata?: Record<string, string | string[] | number | number[]>): Promise<string>;
10
+ postDocument(document: Buffer, filename: string, metadata?: Record<string, string | string[] | number | number[] | undefined>): Promise<string>;
11
11
  getDocuments(query?: string): Promise<DocumentsResponse>;
12
12
  getDocument(id: number): Promise<Document>;
13
13
  updateDocument(id: number, data: Partial<Document>): Promise<Document>;
@@ -68,7 +68,7 @@ export interface Document {
68
68
  id: number;
69
69
  correspondent: number | null;
70
70
  document_type: number | null;
71
- storage_path: string | null;
71
+ storage_path: number | null;
72
72
  title: string;
73
73
  content: string | null;
74
74
  tags: number[];
@@ -77,7 +77,7 @@ export interface Document {
77
77
  modified: string;
78
78
  added: string;
79
79
  deleted_at: string | null;
80
- archive_serial_number: string | null;
80
+ archive_serial_number: number | null;
81
81
  original_file_name: string;
82
82
  archived_file_name: string;
83
83
  owner: number | null;
@@ -188,18 +188,16 @@ export interface BulkEditParameters {
188
188
  document_type?: number;
189
189
  storage_path?: number;
190
190
  tag?: number;
191
- permissions?: {
192
- owner?: number | null;
193
- set_permissions?: {
194
- view: {
195
- users: number[];
196
- groups: number[];
197
- };
198
- change: {
199
- users: number[];
200
- groups: number[];
201
- };
191
+ set_permissions?: {
192
+ view?: {
193
+ users?: number[];
194
+ groups?: number[];
195
+ };
196
+ change?: {
197
+ users?: number[];
198
+ groups?: number[];
202
199
  };
203
- merge?: boolean;
204
200
  };
201
+ owner?: number | null;
202
+ merge?: boolean;
205
203
  }
@@ -22,8 +22,8 @@ var __rest = (this && this.__rest) || function (s, e) {
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.registerCorrespondentTools = registerCorrespondentTools;
24
24
  const zod_1 = require("zod");
25
- const types_1 = require("../api/types");
26
25
  const utils_1 = require("../api/utils");
26
+ const matchingAlgorithm_1 = require("./utils/matchingAlgorithm");
27
27
  const middlewares_1 = require("./utils/middlewares");
28
28
  const queryString_1 = require("./utils/queryString");
29
29
  function registerCorrespondentTools(server, api) {
@@ -64,13 +64,7 @@ function registerCorrespondentTools(server, api) {
64
64
  server.tool("create_correspondent", "Create a new correspondent with optional matching pattern and algorithm for automatic document assignment.", {
65
65
  name: zod_1.z.string(),
66
66
  match: zod_1.z.string().optional(),
67
- matching_algorithm: zod_1.z
68
- .number()
69
- .int()
70
- .min(0)
71
- .max(6)
72
- .optional()
73
- .describe(types_1.MATCHING_ALGORITHM_DESCRIPTION),
67
+ matching_algorithm: matchingAlgorithm_1.matchingAlgorithmSchema.optional(),
74
68
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
75
69
  if (!api)
76
70
  throw new Error("Please configure API connection first");
@@ -86,13 +80,7 @@ function registerCorrespondentTools(server, api) {
86
80
  id: zod_1.z.number(),
87
81
  name: zod_1.z.string(),
88
82
  match: zod_1.z.string().optional(),
89
- matching_algorithm: zod_1.z
90
- .number()
91
- .int()
92
- .min(0)
93
- .max(6)
94
- .optional()
95
- .describe(types_1.MATCHING_ALGORITHM_DESCRIPTION),
83
+ matching_algorithm: matchingAlgorithm_1.matchingAlgorithmSchema.optional(),
96
84
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
97
85
  if (!api)
98
86
  throw new Error("Please configure API connection first");
@@ -33,7 +33,7 @@ function registerCustomFieldTools(server, api) {
33
33
  name__iexact: zod_1.z.string().optional(),
34
34
  name__istartswith: zod_1.z.string().optional(),
35
35
  ordering: zod_1.z.string().optional(),
36
- }, (0, middlewares_1.withErrorHandling)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) {
36
+ }, (0, middlewares_1.withErrorHandling)((args) => __awaiter(this, void 0, void 0, function* () {
37
37
  if (!api)
38
38
  throw new Error("Please configure API connection first");
39
39
  const queryString = (0, queryString_1.buildQueryString)(args);
@@ -68,7 +68,7 @@ function registerCustomFieldTools(server, api) {
68
68
  "documentlink",
69
69
  "select",
70
70
  ]),
71
- extra_data: zod_1.z.record(zod_1.z.unknown()).nullable().optional(),
71
+ extra_data: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).nullable().optional(),
72
72
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
73
73
  if (!api)
74
74
  throw new Error("Please configure API connection first");
@@ -93,7 +93,7 @@ function registerCustomFieldTools(server, api) {
93
93
  "select",
94
94
  ])
95
95
  .optional(),
96
- extra_data: zod_1.z.record(zod_1.z.unknown()).nullable().optional(),
96
+ extra_data: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).nullable().optional(),
97
97
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
98
98
  if (!api)
99
99
  throw new Error("Please configure API connection first");
@@ -22,8 +22,8 @@ var __rest = (this && this.__rest) || function (s, e) {
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.registerDocumentTypeTools = registerDocumentTypeTools;
24
24
  const zod_1 = require("zod");
25
- const types_1 = require("../api/types");
26
25
  const utils_1 = require("../api/utils");
26
+ const matchingAlgorithm_1 = require("./utils/matchingAlgorithm");
27
27
  const middlewares_1 = require("./utils/middlewares");
28
28
  const queryString_1 = require("./utils/queryString");
29
29
  function registerDocumentTypeTools(server, api) {
@@ -35,7 +35,7 @@ function registerDocumentTypeTools(server, api) {
35
35
  name__iexact: zod_1.z.string().optional(),
36
36
  name__istartswith: zod_1.z.string().optional(),
37
37
  ordering: zod_1.z.string().optional(),
38
- }, (0, middlewares_1.withErrorHandling)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}, extra) {
38
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
39
39
  if (!api)
40
40
  throw new Error("Please configure API connection first");
41
41
  const queryString = (0, queryString_1.buildQueryString)(args);
@@ -62,13 +62,7 @@ function registerDocumentTypeTools(server, api) {
62
62
  server.tool("create_document_type", "Create a new document type with optional matching pattern and algorithm for automatic document classification.", {
63
63
  name: zod_1.z.string(),
64
64
  match: zod_1.z.string().optional(),
65
- matching_algorithm: zod_1.z
66
- .number()
67
- .int()
68
- .min(0)
69
- .max(6)
70
- .optional()
71
- .describe(types_1.MATCHING_ALGORITHM_DESCRIPTION),
65
+ matching_algorithm: matchingAlgorithm_1.matchingAlgorithmSchema.optional(),
72
66
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
73
67
  if (!api)
74
68
  throw new Error("Please configure API connection first");
@@ -82,13 +76,7 @@ function registerDocumentTypeTools(server, api) {
82
76
  id: zod_1.z.number(),
83
77
  name: zod_1.z.string(),
84
78
  match: zod_1.z.string().optional(),
85
- matching_algorithm: zod_1.z
86
- .number()
87
- .int()
88
- .min(0)
89
- .max(6)
90
- .optional()
91
- .describe(types_1.MATCHING_ALGORITHM_DESCRIPTION),
79
+ matching_algorithm: matchingAlgorithm_1.matchingAlgorithmSchema.optional(),
92
80
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
93
81
  if (!api)
94
82
  throw new Error("Please configure API connection first");
@@ -164,25 +164,38 @@ function registerDocumentTools(server, api) {
164
164
  .array(zod_1.z.number())
165
165
  .optional()
166
166
  .transform(empty_1.arrayNotEmpty),
167
- permissions: zod_1.z
167
+ set_permissions: zod_1.z
168
168
  .object({
169
- owner: zod_1.z.number().nullable().optional(),
170
- set_permissions: zod_1.z
169
+ view: zod_1.z
171
170
  .object({
172
- view: zod_1.z.object({
173
- users: zod_1.z.array(zod_1.z.number()),
174
- groups: zod_1.z.array(zod_1.z.number()),
175
- }),
176
- change: zod_1.z.object({
177
- users: zod_1.z.array(zod_1.z.number()),
178
- groups: zod_1.z.array(zod_1.z.number()),
179
- }),
171
+ users: zod_1.z.array(zod_1.z.number()).optional(),
172
+ groups: zod_1.z.array(zod_1.z.number()).optional(),
180
173
  })
174
+ .strict()
175
+ .optional(),
176
+ change: zod_1.z
177
+ .object({
178
+ users: zod_1.z.array(zod_1.z.number()).optional(),
179
+ groups: zod_1.z.array(zod_1.z.number()).optional(),
180
+ })
181
+ .strict()
181
182
  .optional(),
182
- merge: zod_1.z.boolean().optional(),
183
183
  })
184
+ // strict: a misspelled action ("read") must not silently collapse
185
+ // into {} and clear permissions/ownership.
186
+ .strict()
187
+ .optional()
188
+ .describe("For set_permissions: view/change permissions to apply. Omitted actions (view/change) and omitted users/groups lists are left untouched; an empty list [] removes all (unless merge is true). Omit entirely for owner-only changes."),
189
+ owner: zod_1.z
190
+ .number()
191
+ .int()
192
+ .nullable()
193
+ .optional()
194
+ .describe("For set_permissions: new owner user ID, or null to remove the owner. Unless merge is true, omitting owner also clears the current owner."),
195
+ merge: zod_1.z
196
+ .boolean()
184
197
  .optional()
185
- .transform(empty_1.objectNotEmpty),
198
+ .describe("For set_permissions: true adds to existing permissions and keeps the current owner; false (default) replaces the listed users/groups"),
186
199
  metadata_document_id: zod_1.z.number().optional(),
187
200
  delete_originals: zod_1.z.boolean().optional(),
188
201
  pages: zod_1.z.string().optional(),
@@ -192,12 +205,23 @@ function registerDocumentTools(server, api) {
192
205
  .optional()
193
206
  .describe("Must be true when method is 'delete' to confirm destructive operation"),
194
207
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
208
+ var _a;
195
209
  if (!api)
196
210
  throw new Error("Please configure API connection first");
197
211
  if (args.method === "delete" && !args.confirm) {
198
212
  throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
199
213
  }
214
+ if (args.method === "set_permissions" &&
215
+ !args.set_permissions &&
216
+ args.owner === undefined) {
217
+ throw new Error("Method 'set_permissions' requires set_permissions and/or owner.");
218
+ }
200
219
  const { documents, method, add_custom_fields, confirm } = args, parameters = __rest(args, ["documents", "method", "add_custom_fields", "confirm"]);
220
+ if (method === "set_permissions") {
221
+ // Paperless rejects (Paperless <= 3.0.5: crashes with a 500 on) a missing
222
+ // set_permissions key even for owner-only changes.
223
+ (_a = parameters.set_permissions) !== null && _a !== void 0 ? _a : (parameters.set_permissions = {});
224
+ }
201
225
  (0, monetary_1.validateCustomFields)(add_custom_fields);
202
226
  const resolvedCustomFields = yield (0, selectFields_1.resolveSelectCustomFieldValues)(api, add_custom_fields, "stored");
203
227
  const response = yield api.bulkEditDocuments(documents, method, method === "delete"
@@ -272,7 +296,7 @@ function registerDocumentTools(server, api) {
272
296
  throw new Error("Please configure API connection first");
273
297
  const validationResult = postDocumentSchema.safeParse(args);
274
298
  if (!validationResult.success) {
275
- throw new Error(validationResult.error.errors.map(e => e.message).join("; "));
299
+ throw new Error(validationResult.error.issues.map(e => e.message).join("; "));
276
300
  }
277
301
  let document;
278
302
  let filename;
@@ -378,6 +402,9 @@ function registerDocumentTools(server, api) {
378
402
  });
379
403
  return {
380
404
  content: [
405
+ // Legacy clients surface only content[].text and drop resource blocks
406
+ // entirely, so the URI is repeated here to stay reachable (issue #134).
407
+ { type: "text", text: uri },
381
408
  {
382
409
  type: "resource",
383
410
  resource: {
@@ -397,12 +424,16 @@ function registerDocumentTools(server, api) {
397
424
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
398
425
  if (!api)
399
426
  throw new Error("Please configure API connection first");
427
+ const uri = (0, resourceUri_1.buildThumbnailResourceUri)(args.id);
400
428
  return {
401
429
  content: [
430
+ // See download_document above: the URI is repeated as text for legacy
431
+ // clients that drop resource blocks.
432
+ { type: "text", text: uri },
402
433
  {
403
434
  type: "resource",
404
435
  resource: {
405
- uri: (0, resourceUri_1.buildThumbnailResourceUri)(args.id),
436
+ uri,
406
437
  // See download_document above: the binary thumbnail is fetched
407
438
  // lazily through resources/read instead of embedded here.
408
439
  text: "",
@@ -421,16 +452,19 @@ function registerDocumentTools(server, api) {
421
452
  .describe("The new title for the document (max 128 characters)"),
422
453
  correspondent: zod_1.z
423
454
  .number()
455
+ .int()
424
456
  .nullable()
425
457
  .optional()
426
458
  .describe("The ID of the correspondent to assign"),
427
459
  document_type: zod_1.z
428
460
  .number()
461
+ .int()
429
462
  .nullable()
430
463
  .optional()
431
464
  .describe("The ID of the document type to assign"),
432
465
  storage_path: zod_1.z
433
466
  .number()
467
+ .int()
434
468
  .nullable()
435
469
  .optional()
436
470
  .describe("The ID of the storage path to assign"),
@@ -452,6 +486,7 @@ function registerDocumentTools(server, api) {
452
486
  .describe("The archive serial number (0-4294967295)"),
453
487
  owner: zod_1.z
454
488
  .number()
489
+ .int()
455
490
  .nullable()
456
491
  .optional()
457
492
  .describe("The ID of the user who owns the document"),
@@ -21,6 +21,7 @@ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
21
21
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
22
22
  const documents_1 = require("./documents");
23
23
  const documentQuery_1 = require("./utils/documentQuery");
24
+ const resourceUri_1 = require("./utils/resourceUri");
24
25
  function getQueryParams(queryString) {
25
26
  return new URLSearchParams(queryString.replace(/^\?/, ""));
26
27
  }
@@ -480,3 +481,178 @@ const OBJECT_SELECT_FIELD = {
480
481
  strict_1.default.equal(calls.updateDocument.length, 0, "no document update should be sent when the option is invalid");
481
482
  }));
482
483
  });
484
+ (0, node_test_1.describe)("document resource reference tools", () => {
485
+ // Expected URIs come from the builders rather than literals: their exact
486
+ // format is already pinned by utils/resourceUri.test.ts, and what matters
487
+ // here is that the handler surfaces that URI in both content blocks.
488
+ const cases = [
489
+ {
490
+ tool: "download_document",
491
+ args: { id: 4 },
492
+ uri: (0, resourceUri_1.buildDocumentResourceUri)(4),
493
+ mimeType: "application/octet-stream",
494
+ },
495
+ {
496
+ tool: "download_document",
497
+ args: { id: 4, original: true },
498
+ uri: (0, resourceUri_1.buildDocumentResourceUri)(4, { original: true }),
499
+ mimeType: "application/octet-stream",
500
+ },
501
+ {
502
+ tool: "get_document_thumbnail",
503
+ args: { id: 123 },
504
+ uri: (0, resourceUri_1.buildThumbnailResourceUri)(123),
505
+ mimeType: "image/webp",
506
+ },
507
+ ];
508
+ for (const { tool, args, uri, mimeType } of cases) {
509
+ (0, node_test_1.test)(`${tool} ${JSON.stringify(args)} returns the URI as text beside the resource`, () => __awaiter(void 0, void 0, void 0, function* () {
510
+ const { api } = createDocumentApi([]);
511
+ let result;
512
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
513
+ result = (yield client.callTool({
514
+ name: tool,
515
+ arguments: args,
516
+ }));
517
+ }));
518
+ strict_1.default.ok(result && !result.isError, `${tool} failed`);
519
+ const [text, embedded] = result.content;
520
+ // Legacy clients read only content[].text (issue #134).
521
+ strict_1.default.equal(text.type, "text");
522
+ strict_1.default.equal(text.text, uri);
523
+ strict_1.default.equal(embedded.type, "resource");
524
+ const { resource } = embedded;
525
+ strict_1.default.equal(resource.uri, uri, "both blocks must reference the same URI");
526
+ strict_1.default.equal(resource.mimeType, mimeType);
527
+ }));
528
+ }
529
+ });
530
+ (0, node_test_1.describe)("bulk_edit_documents set_permissions", () => {
531
+ (0, node_test_1.test)("sends set_permissions, owner and merge at the top level of parameters", () => __awaiter(void 0, void 0, void 0, function* () {
532
+ const { api, calls } = createDocumentApi([]);
533
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
534
+ var _a;
535
+ const result = (yield client.callTool({
536
+ name: "bulk_edit_documents",
537
+ arguments: {
538
+ documents: [4103],
539
+ method: "set_permissions",
540
+ set_permissions: {
541
+ view: { users: [], groups: [10] },
542
+ change: { users: [], groups: [10] },
543
+ },
544
+ owner: 3,
545
+ merge: true,
546
+ },
547
+ }));
548
+ strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
549
+ }));
550
+ strict_1.default.equal(calls.bulkEditDocuments.length, 1);
551
+ const [documents, method, parameters] = calls.bulkEditDocuments[0];
552
+ strict_1.default.deepEqual(documents, [4103]);
553
+ strict_1.default.equal(method, "set_permissions");
554
+ // Paperless reads parameters["set_permissions"] directly; nesting it under
555
+ // another key makes the server crash with a KeyError (HTTP 500).
556
+ strict_1.default.deepEqual(parameters, {
557
+ set_permissions: {
558
+ view: { users: [], groups: [10] },
559
+ change: { users: [], groups: [10] },
560
+ },
561
+ owner: 3,
562
+ merge: true,
563
+ });
564
+ }));
565
+ (0, node_test_1.test)("forwards partial permissions without filling in omitted actions or lists", () => __awaiter(void 0, void 0, void 0, function* () {
566
+ const { api, calls } = createDocumentApi([]);
567
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
568
+ var _a;
569
+ const result = (yield client.callTool({
570
+ name: "bulk_edit_documents",
571
+ arguments: {
572
+ documents: [1],
573
+ method: "set_permissions",
574
+ set_permissions: { view: { groups: [10] } },
575
+ },
576
+ }));
577
+ strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
578
+ }));
579
+ // Paperless only touches the actions/lists that are present, and clears
580
+ // the owner when it is omitted without merge — so nothing may be added.
581
+ const [, , parameters] = calls.bulkEditDocuments[0];
582
+ strict_1.default.deepEqual(parameters, { set_permissions: { view: { groups: [10] } } });
583
+ }));
584
+ (0, node_test_1.test)("owner-only changes send an empty set_permissions object", () => __awaiter(void 0, void 0, void 0, function* () {
585
+ const { api, calls } = createDocumentApi([]);
586
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
587
+ var _a;
588
+ for (const owner of [3, null]) {
589
+ const result = (yield client.callTool({
590
+ name: "bulk_edit_documents",
591
+ arguments: { documents: [1], method: "set_permissions", owner },
592
+ }));
593
+ strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
594
+ }
595
+ }));
596
+ strict_1.default.deepEqual(calls.bulkEditDocuments.map(([, , parameters]) => parameters), [
597
+ { set_permissions: {}, owner: 3 },
598
+ { set_permissions: {}, owner: null },
599
+ ]);
600
+ }));
601
+ (0, node_test_1.test)("rejects method set_permissions with neither set_permissions nor owner", () => __awaiter(void 0, void 0, void 0, function* () {
602
+ const { api, calls } = createDocumentApi([]);
603
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
604
+ var _a, _b;
605
+ const result = (yield client.callTool({
606
+ name: "bulk_edit_documents",
607
+ arguments: { documents: [1], method: "set_permissions", merge: true },
608
+ }));
609
+ strict_1.default.ok(result.isError, "expected an error when both are missing");
610
+ strict_1.default.match((_b = (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error) !== null && _b !== void 0 ? _b : "", /set_permissions and\/or owner/);
611
+ }));
612
+ strict_1.default.equal(calls.bulkEditDocuments.length, 0);
613
+ }));
614
+ (0, node_test_1.test)("rejects malformed set_permissions input before calling Paperless", () => __awaiter(void 0, void 0, void 0, function* () {
615
+ const { api, calls } = createDocumentApi([]);
616
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
617
+ for (const set_permissions of [
618
+ { view: { groups: ["ai-agents"] } },
619
+ { read: { groups: [10] } },
620
+ { view: [10] },
621
+ ]) {
622
+ // Depending on the installed MCP SDK version, an input-schema (zod)
623
+ // violation either rejects with a protocol error (-32602) or resolves
624
+ // with a CallToolResult carrying isError: true.
625
+ let rejected = false;
626
+ let result;
627
+ try {
628
+ result = (yield client.callTool({
629
+ name: "bulk_edit_documents",
630
+ arguments: { documents: [1], method: "set_permissions", set_permissions },
631
+ }));
632
+ }
633
+ catch (_a) {
634
+ rejected = true;
635
+ }
636
+ strict_1.default.ok(rejected || (result === null || result === void 0 ? void 0 : result.isError), `expected ${JSON.stringify(set_permissions)} to be rejected`);
637
+ }
638
+ }));
639
+ strict_1.default.equal(calls.bulkEditDocuments.length, 0);
640
+ }));
641
+ });
642
+ (0, node_test_1.describe)("nullable foreign keys can still be cleared (#138)", () => {
643
+ (0, node_test_1.test)("update_document forwards an explicit null so a foreign key can be cleared", () => __awaiter(void 0, void 0, void 0, function* () {
644
+ const { api, calls } = createDocumentApi([]);
645
+ yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
646
+ var _a;
647
+ const result = (yield client.callTool({
648
+ name: "update_document",
649
+ arguments: { id: 42, correspondent: null, owner: null },
650
+ }));
651
+ strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
652
+ }));
653
+ strict_1.default.equal(calls.updateDocument.length, 1);
654
+ const [, data] = calls.updateDocument[0];
655
+ strict_1.default.equal(data.correspondent, null);
656
+ strict_1.default.equal(data.owner, null);
657
+ }));
658
+ });
@@ -35,10 +35,10 @@ const mailRuleFields = {
35
35
  account: zod_1.z.number().int().optional(),
36
36
  enabled: zod_1.z.boolean().optional(),
37
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(),
38
+ filter_from: zod_1.z.string().max(256).nullable().optional(),
39
+ filter_to: zod_1.z.string().max(256).nullable().optional(),
40
+ filter_subject: zod_1.z.string().max(256).nullable().optional(),
41
+ filter_body: zod_1.z.string().max(256).nullable().optional(),
42
42
  filter_attachment_filename_include: zod_1.z.string().nullable().optional(),
43
43
  filter_attachment_filename_exclude: zod_1.z.string().nullable().optional(),
44
44
  maximum_age: zod_1.z.number().int().min(0).optional(),
@@ -96,7 +96,7 @@ function registerMailTools(server, api) {
96
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
97
  page: zod_1.z.number().optional(),
98
98
  page_size: zod_1.z.number().optional(),
99
- }, (0, middlewares_1.withErrorHandling)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) {
99
+ }, (0, middlewares_1.withErrorHandling)((args) => __awaiter(this, void 0, void 0, function* () {
100
100
  if (!api)
101
101
  throw new Error("Please configure API connection first");
102
102
  const queryString = (0, queryString_1.buildQueryString)(args);
@@ -132,7 +132,7 @@ function registerMailTools(server, api) {
132
132
  server.tool("list_mail_rules", "List Paperless mail rules with optional pagination.", {
133
133
  page: zod_1.z.number().optional(),
134
134
  page_size: zod_1.z.number().optional(),
135
- }, (0, middlewares_1.withErrorHandling)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) {
135
+ }, (0, middlewares_1.withErrorHandling)((args) => __awaiter(this, void 0, void 0, function* () {
136
136
  if (!api)
137
137
  throw new Error("Please configure API connection first");
138
138
  const queryString = (0, queryString_1.buildQueryString)(args);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
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 __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const strict_1 = __importDefault(require("node:assert/strict"));
16
+ const node_test_1 = require("node:test");
17
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
18
+ const inMemory_js_1 = require("@modelcontextprotocol/sdk/inMemory.js");
19
+ const server_1 = require("../server");
20
+ // Strict MCP clients and gateways reject an array-valued `type` and silently drop
21
+ // the whole tool (#138). A JSON Schema converter reaches that form by collapsing a
22
+ // union of unchecked primitives: `zod-to-json-schema` (the zod v3 path) always does
23
+ // it, and zod's own converter did it again from 4.5.0 on. The tools are only free
24
+ // of it because the server is on the zod v4 path with zod pinned to 4.4.x, so this
25
+ // walks every advertised schema rather than a field list: bumping that pin, moving
26
+ // off zod v4, or adding a construct that reintroduces the form fails here.
27
+ function arrayFormTypes(node, path = "") {
28
+ if (Array.isArray(node)) {
29
+ return node.flatMap((item, i) => arrayFormTypes(item, `${path}[${i}]`));
30
+ }
31
+ if (!node || typeof node !== "object") {
32
+ return [];
33
+ }
34
+ return Object.entries(node).flatMap(([key, value]) => key === "type" && Array.isArray(value)
35
+ ? [`${path}.type = ${JSON.stringify(value)}`]
36
+ : arrayFormTypes(value, `${path}.${key}`));
37
+ }
38
+ (0, node_test_1.test)("no tool advertises an array-form `type` (#138)", () => __awaiter(void 0, void 0, void 0, function* () {
39
+ const server = (0, server_1.createMcpServer)({
40
+ baseUrl: "http://paperless.test",
41
+ token: "test-token",
42
+ version: "0.0.0-test",
43
+ publicUrl: "http://paperless.test",
44
+ });
45
+ const [clientTransport, serverTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
46
+ const client = new index_js_1.Client({ name: "schema-compat-test", version: "1.0.0" });
47
+ yield Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
48
+ try {
49
+ const { tools } = yield client.listTools();
50
+ const found = tools
51
+ .flatMap((tool) => arrayFormTypes(tool.inputSchema).map((hit) => `${tool.name} ${hit}`))
52
+ .sort();
53
+ strict_1.default.deepEqual(found, []);
54
+ }
55
+ finally {
56
+ yield client.close();
57
+ yield server.close();
58
+ }
59
+ }));
@@ -11,8 +11,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.registerTagTools = registerTagTools;
13
13
  const zod_1 = require("zod");
14
- const types_1 = require("../api/types");
15
14
  const utils_1 = require("../api/utils");
15
+ const matchingAlgorithm_1 = require("./utils/matchingAlgorithm");
16
16
  const middlewares_1 = require("./utils/middlewares");
17
17
  const queryString_1 = require("./utils/queryString");
18
18
  function registerTagTools(server, api) {
@@ -24,7 +24,7 @@ function registerTagTools(server, api) {
24
24
  name__iexact: zod_1.z.string().optional(),
25
25
  name__istartswith: zod_1.z.string().optional(),
26
26
  ordering: zod_1.z.string().optional(),
27
- }, (0, middlewares_1.withErrorHandling)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) {
27
+ }, (0, middlewares_1.withErrorHandling)((args) => __awaiter(this, void 0, void 0, function* () {
28
28
  if (!api)
29
29
  throw new Error("Please configure API connection first");
30
30
  const queryString = (0, queryString_1.buildQueryString)(args);
@@ -46,13 +46,7 @@ function registerTagTools(server, api) {
46
46
  .regex(/^#[0-9A-Fa-f]{6}$/)
47
47
  .optional(),
48
48
  match: zod_1.z.string().optional(),
49
- matching_algorithm: zod_1.z
50
- .number()
51
- .int()
52
- .min(0)
53
- .max(6)
54
- .optional()
55
- .describe(types_1.MATCHING_ALGORITHM_DESCRIPTION),
49
+ matching_algorithm: matchingAlgorithm_1.matchingAlgorithmSchema.optional(),
56
50
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
57
51
  if (!api)
58
52
  throw new Error("Please configure API connection first");
@@ -75,13 +69,7 @@ function registerTagTools(server, api) {
75
69
  .regex(/^#[0-9A-Fa-f]{6}$/)
76
70
  .optional(),
77
71
  match: zod_1.z.string().optional(),
78
- matching_algorithm: zod_1.z
79
- .number()
80
- .int()
81
- .min(0)
82
- .max(6)
83
- .optional()
84
- .describe(types_1.MATCHING_ALGORITHM_DESCRIPTION),
72
+ matching_algorithm: matchingAlgorithm_1.matchingAlgorithmSchema.optional(),
85
73
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
86
74
  if (!api)
87
75
  throw new Error("Please configure API connection first");
@@ -6,8 +6,8 @@ export type CustomFieldQuery = [fieldNameOrId: string | number, operator: string
6
6
  clauses: CustomFieldQuery[]
7
7
  ];
8
8
  export declare const customFieldQuerySchema: z.ZodType<CustomFieldQuery>;
9
- export declare const paperlessFilterValueSchema: z.ZodUnion<[z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>, "many">]>;
10
- export declare const paperlessFiltersSchema: z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>, "many">]>>;
9
+ export declare const paperlessFilterValueSchema: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>;
10
+ export declare const paperlessFiltersSchema: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>>;
11
11
  export declare const LIST_DOCUMENTS_ARGS_SHAPE: {
12
12
  custom_field_query: z.ZodOptional<z.ZodString>;
13
13
  page: z.ZodOptional<z.ZodNumber>;
@@ -27,8 +27,8 @@ export declare const LIST_DOCUMENTS_ARGS_SHAPE: {
27
27
  export declare const QUERY_DOCUMENTS_ARGS_SHAPE: {
28
28
  query: z.ZodOptional<z.ZodString>;
29
29
  more_like_id: z.ZodOptional<z.ZodNumber>;
30
- custom_field_query: z.ZodOptional<z.ZodType<CustomFieldQuery, z.ZodTypeDef, CustomFieldQuery>>;
31
- paperless_filters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>, "many">]>>>;
30
+ custom_field_query: z.ZodOptional<z.ZodType<CustomFieldQuery, unknown, z.core.$ZodTypeInternals<CustomFieldQuery, unknown>>>;
31
+ paperless_filters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>>>;
32
32
  page: z.ZodOptional<z.ZodNumber>;
33
33
  page_size: z.ZodOptional<z.ZodNumber>;
34
34
  search: z.ZodOptional<z.ZodString>;
@@ -68,7 +68,7 @@ exports.paperlessFilterValueSchema = zod_1.z.union([
68
68
  paperlessFilterScalarSchema,
69
69
  zod_1.z.array(paperlessFilterScalarSchema),
70
70
  ]);
71
- exports.paperlessFiltersSchema = zod_1.z.record(exports.paperlessFilterValueSchema);
71
+ exports.paperlessFiltersSchema = zod_1.z.record(zod_1.z.string(), exports.paperlessFilterValueSchema);
72
72
  const DOCUMENT_QUERY_BASE_ARGS_SHAPE = {
73
73
  page: zod_1.z.number().optional(),
74
74
  page_size: zod_1.z.number().optional(),
@@ -0,0 +1,2 @@
1
+ import { z } from "zod";
2
+ export declare const matchingAlgorithmSchema: z.ZodPipe<z.ZodNumber, z.ZodTransform<0 | 1 | 2 | 3 | 4 | 5 | 6, number>>;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchingAlgorithmSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const types_1 = require("../../api/types");
6
+ // The 0-6 range the API declares is exactly the set `MatchingAlgorithm`
7
+ // enumerates, so narrowing to it after the check lets the PaperlessAPI request
8
+ // types take the parsed value without a cast at every call site.
9
+ exports.matchingAlgorithmSchema = zod_1.z
10
+ .number()
11
+ .int()
12
+ .min(0)
13
+ .max(6)
14
+ .transform((value) => value)
15
+ .describe(types_1.MATCHING_ALGORITHM_DESCRIPTION);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baruchiro/paperless-mcp",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "Model Context Protocol (MCP) server for interacting with Paperless-NGX document management system. Enables AI assistants to manage documents, tags, correspondents, and document types through the Paperless-NGX API.",
5
5
  "main": "build/index.js",
6
6
  "bin": {
@@ -44,12 +44,12 @@
44
44
  "url": "https://github.com/baruchiro/paperless-mcp/issues"
45
45
  },
46
46
  "dependencies": {
47
- "@modelcontextprotocol/sdk": "^1.11.1",
47
+ "@modelcontextprotocol/sdk": "^1.30.0",
48
48
  "axios": "^1.9.0",
49
49
  "express": "^5.1.0",
50
50
  "form-data": "^4.0.2",
51
51
  "typescript": "^5.8.3",
52
- "zod": "^3.24.1"
52
+ "zod": "~4.4.3"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@anthropic-ai/dxt": "^0.2.6",
package/paperless-mcp.dxt CHANGED
Binary file