@baruchiro/paperless-mcp 0.0.3 → 0.1.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
@@ -206,6 +206,15 @@ bulk_edit_documents({
206
206
  add_tags: [1, 2],
207
207
  remove_tags: [3, 4]
208
208
  })
209
+
210
+ // Modify custom fields
211
+ bulk_edit_documents({
212
+ documents: [12, 13],
213
+ method: "modify_custom_fields",
214
+ add_custom_fields: {
215
+ "2": "שנה"
216
+ }
217
+ })
209
218
  ```
210
219
 
211
220
  #### post_document
@@ -232,7 +241,8 @@ post_document({
232
241
  correspondent: 1,
233
242
  document_type: 2,
234
243
  tags: [1, 3],
235
- archive_serial_number: "2024-001"
244
+ archive_serial_number: "2024-001",
245
+ custom_fields: [1, 2]
236
246
  })
237
247
  ```
238
248
 
@@ -313,6 +323,85 @@ create_document_type({
313
323
  })
314
324
  ```
315
325
 
326
+ ### Custom Field Operations
327
+
328
+ #### list_custom_fields
329
+ Get all custom fields.
330
+
331
+ ```typescript
332
+ list_custom_fields()
333
+ ```
334
+
335
+ #### get_custom_field
336
+ Get a specific custom field by ID.
337
+
338
+ Parameters:
339
+ - id: Custom field ID
340
+
341
+ ```typescript
342
+ get_custom_field({
343
+ id: 1
344
+ })
345
+ ```
346
+
347
+ #### create_custom_field
348
+ Create a new custom field.
349
+
350
+ Parameters:
351
+ - name: Custom field name
352
+ - data_type: One of "string", "url", "date", "boolean", "integer", "float", "monetary", "documentlink", "select"
353
+ - extra_data (optional): Extra data for the custom field, such as select options
354
+
355
+ ```typescript
356
+ create_custom_field({
357
+ name: "Invoice Number",
358
+ data_type: "string"
359
+ })
360
+ ```
361
+
362
+ #### update_custom_field
363
+ Update an existing custom field.
364
+
365
+ Parameters:
366
+ - id: Custom field ID
367
+ - name (optional): New custom field name
368
+ - data_type (optional): New data type
369
+ - extra_data (optional): Extra data for the custom field
370
+
371
+ ```typescript
372
+ update_custom_field({
373
+ id: 1,
374
+ name: "Updated Invoice Number",
375
+ data_type: "string"
376
+ })
377
+ ```
378
+
379
+ #### delete_custom_field
380
+ Delete a custom field.
381
+
382
+ Parameters:
383
+ - id: Custom field ID
384
+
385
+ ```typescript
386
+ delete_custom_field({
387
+ id: 1
388
+ })
389
+ ```
390
+
391
+ #### bulk_edit_custom_fields
392
+ Perform bulk operations on multiple custom fields.
393
+
394
+ Parameters:
395
+ - custom_fields: Array of custom field IDs
396
+ - operation: One of "delete"
397
+
398
+ ```typescript
399
+ bulk_edit_custom_fields({
400
+ custom_fields: [1, 2, 3],
401
+ operation: "delete"
402
+ })
403
+ ```
404
+
316
405
  ## Error Handling
317
406
 
318
407
  The server will show clear error messages if:
@@ -372,3 +461,42 @@ npm run start -- <baseUrl> <token> --http --port 3000
372
461
  # Credits
373
462
 
374
463
  This project is a fork of [nloui/paperless-mcp](https://github.com/nloui/paperless-mcp). Many thanks to the original author for their work. Contributions and improvements may be returned upstream.
464
+
465
+ ## Debugging
466
+
467
+ To debug the MCP server in VS Code, use the following launch configuration:
468
+
469
+ ```json
470
+ {
471
+ "type": "node",
472
+ "request": "launch",
473
+ "name": "Debug Paperless MCP (HTTP, ts-node ESM)",
474
+ "program": "${workspaceFolder}/node_modules/ts-node/dist/bin.js",
475
+ "args": [
476
+ "--esm",
477
+ "src/index.ts",
478
+ "--http",
479
+ "--baseUrl",
480
+ "http://your-paperless-instance:8000",
481
+ "--token",
482
+ "your-api-token",
483
+ "--port",
484
+ "3002"
485
+ ],
486
+ "env": {
487
+ "NODE_OPTIONS": "--loader ts-node/esm",
488
+ },
489
+ "console": "integratedTerminal",
490
+ "skipFiles": [
491
+ "<node_internals>/**"
492
+ ]
493
+ }
494
+ ```
495
+
496
+ **Important:** Before debugging, uncomment the following line in `src/index.ts` (around line 175):
497
+
498
+ ```typescript
499
+ // await new Promise((resolve) => setTimeout(resolve, 1000000));
500
+ ```
501
+
502
+ This prevents the server from exiting immediately and allows you to set breakpoints and debug the code.
@@ -1,13 +1,14 @@
1
- import { BulkEditDocumentsResult, Correspondent, Document, DocumentsResponse, DocumentType, GetCorrespondentsResponse, GetDocumentTypesResponse, GetTagsResponse, Tag } from "./types";
1
+ import { BulkEditDocumentsResult, BulkEditParameters, Correspondent, CustomField, Document, DocumentsResponse, DocumentType, GetCorrespondentsResponse, GetCustomFieldsResponse, GetDocumentTypesResponse, GetTagsResponse, Tag } from "./types";
2
2
  export declare class PaperlessAPI {
3
3
  private readonly baseUrl;
4
4
  private readonly token;
5
5
  constructor(baseUrl: string, token: string);
6
6
  request<T = any>(path: string, options?: RequestInit): Promise<T>;
7
- bulkEditDocuments(documents: number[], method: string, parameters?: {}): Promise<BulkEditDocumentsResult>;
7
+ bulkEditDocuments(documents: number[], method: string, parameters?: BulkEditParameters): Promise<BulkEditDocumentsResult>;
8
8
  postDocument(file: File, metadata?: Record<string, string | string[] | number | number[]>): Promise<string>;
9
9
  getDocuments(query?: string): Promise<DocumentsResponse>;
10
10
  getDocument(id: number): Promise<Document>;
11
+ updateDocument(id: number, data: Partial<Document>): Promise<Document>;
11
12
  searchDocuments(query: string): Promise<DocumentsResponse>;
12
13
  downloadDocument(id: number, asOriginal?: boolean): Promise<import("axios").AxiosResponse<any, any>>;
13
14
  getTags(): Promise<GetTagsResponse>;
@@ -22,5 +23,10 @@ export declare class PaperlessAPI {
22
23
  createDocumentType(data: Partial<DocumentType>): Promise<DocumentType>;
23
24
  updateDocumentType(id: number, data: Partial<DocumentType>): Promise<DocumentType>;
24
25
  deleteDocumentType(id: number): Promise<void>;
26
+ getCustomFields(): Promise<GetCustomFieldsResponse>;
27
+ getCustomField(id: number): Promise<CustomField>;
28
+ createCustomField(data: Partial<CustomField>): Promise<CustomField>;
29
+ updateCustomField(id: number, data: Partial<CustomField>): Promise<CustomField>;
30
+ deleteCustomField(id: number): Promise<void>;
25
31
  bulkEditObjects(objects: any, objectType: any, operation: any, parameters?: {}): Promise<any>;
26
32
  }
@@ -25,6 +25,7 @@ class PaperlessAPI {
25
25
  }
26
26
  request(path_1) {
27
27
  return __awaiter(this, arguments, void 0, function* (path, options = {}) {
28
+ var _a, _b;
28
29
  const url = `${this.baseUrl}/api${path}`;
29
30
  const isJson = !options.body || typeof options.body === "string";
30
31
  const mergedHeaders = Object.assign(Object.assign({ Authorization: `Token ${this.token}`, Accept: "application/json; version=5", "Accept-Language": "en-US,en;q=0.9" }, (isJson ? { "Content-Type": "application/json" } : {})), (0, utils_1.headersToObject)(options.headers));
@@ -44,7 +45,11 @@ class PaperlessAPI {
44
45
  status: response.status,
45
46
  response: body,
46
47
  });
47
- throw new Error(`HTTP error! status: ${response.status}`);
48
+ const errorMessage = (body === null || body === void 0 ? void 0 : body.detail) ||
49
+ (body === null || body === void 0 ? void 0 : body.error) ||
50
+ (body === null || body === void 0 ? void 0 : body.message) ||
51
+ `HTTP error! status: ${response.status}`;
52
+ throw new Error(String(errorMessage));
48
53
  }
49
54
  return body;
50
55
  }
@@ -54,6 +59,8 @@ class PaperlessAPI {
54
59
  message: error instanceof Error ? error.message : String(error),
55
60
  url,
56
61
  options,
62
+ responseData: (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data,
63
+ status: (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status,
57
64
  });
58
65
  throw error;
59
66
  }
@@ -115,6 +122,14 @@ class PaperlessAPI {
115
122
  return this.request(`/documents/${id}/`);
116
123
  });
117
124
  }
125
+ updateDocument(id, data) {
126
+ return __awaiter(this, void 0, void 0, function* () {
127
+ return this.request(`/documents/${id}/`, {
128
+ method: "PATCH",
129
+ body: JSON.stringify(data),
130
+ });
131
+ });
132
+ }
118
133
  searchDocuments(query) {
119
134
  return __awaiter(this, void 0, void 0, function* () {
120
135
  const response = yield this.request(`/documents/?query=${encodeURIComponent(query)}`);
@@ -220,6 +235,40 @@ class PaperlessAPI {
220
235
  });
221
236
  });
222
237
  }
238
+ // Custom field operations
239
+ getCustomFields() {
240
+ return __awaiter(this, void 0, void 0, function* () {
241
+ return this.request("/custom_fields/");
242
+ });
243
+ }
244
+ getCustomField(id) {
245
+ return __awaiter(this, void 0, void 0, function* () {
246
+ return this.request(`/custom_fields/${id}/`);
247
+ });
248
+ }
249
+ createCustomField(data) {
250
+ return __awaiter(this, void 0, void 0, function* () {
251
+ return this.request("/custom_fields/", {
252
+ method: "POST",
253
+ body: JSON.stringify(data),
254
+ });
255
+ });
256
+ }
257
+ updateCustomField(id, data) {
258
+ return __awaiter(this, void 0, void 0, function* () {
259
+ return this.request(`/custom_fields/${id}/`, {
260
+ method: "PUT",
261
+ body: JSON.stringify(data),
262
+ });
263
+ });
264
+ }
265
+ deleteCustomField(id) {
266
+ return __awaiter(this, void 0, void 0, function* () {
267
+ return this.request(`/custom_fields/${id}/`, {
268
+ method: "DELETE",
269
+ });
270
+ });
271
+ }
223
272
  // Bulk object operations
224
273
  bulkEditObjects(objects_1, objectType_1, operation_1) {
225
274
  return __awaiter(this, arguments, void 0, function* (objects, objectType, operation, parameters = {}) {
@@ -0,0 +1,21 @@
1
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types";
2
+ import { PaperlessAPI } from "./PaperlessAPI";
3
+ import { Document, DocumentsResponse } from "./types";
4
+ interface NamedItem {
5
+ id: number;
6
+ name: string;
7
+ }
8
+ interface CustomField {
9
+ field: number;
10
+ name: string;
11
+ value: string | number | boolean | object | null;
12
+ }
13
+ export interface EnhancedDocument extends Omit<Document, "correspondent" | "document_type" | "tags" | "custom_fields"> {
14
+ correspondent: NamedItem | null;
15
+ document_type: NamedItem | null;
16
+ tags: NamedItem[];
17
+ custom_fields: CustomField[];
18
+ }
19
+ export declare function convertDocsWithNames(document: Document, api: PaperlessAPI): Promise<CallToolResult>;
20
+ export declare function convertDocsWithNames(documentsResponse: DocumentsResponse, api: PaperlessAPI): Promise<CallToolResult>;
21
+ export {};
@@ -0,0 +1,88 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.convertDocsWithNames = convertDocsWithNames;
13
+ function convertDocsWithNames(input, api) {
14
+ return __awaiter(this, void 0, void 0, function* () {
15
+ if ("results" in input) {
16
+ const enhancedResults = yield enhanceDocumentsArray(input.results || [], api);
17
+ return {
18
+ content: [
19
+ {
20
+ type: "text",
21
+ text: (enhancedResults === null || enhancedResults === void 0 ? void 0 : enhancedResults.length)
22
+ ? JSON.stringify(Object.assign(Object.assign({}, input), { results: enhancedResults }))
23
+ : "No documents found",
24
+ },
25
+ ],
26
+ };
27
+ }
28
+ if (!input) {
29
+ return {
30
+ content: [
31
+ {
32
+ type: "text",
33
+ text: "No document found",
34
+ },
35
+ ],
36
+ };
37
+ }
38
+ const [enhanced] = yield enhanceDocumentsArray([input], api);
39
+ return {
40
+ content: [
41
+ {
42
+ type: "text",
43
+ text: JSON.stringify(enhanced),
44
+ },
45
+ ],
46
+ };
47
+ });
48
+ }
49
+ function enhanceDocumentsArray(documents, api) {
50
+ return __awaiter(this, void 0, void 0, function* () {
51
+ if (!(documents === null || documents === void 0 ? void 0 : documents.length)) {
52
+ return [];
53
+ }
54
+ const [correspondents, documentTypes, tags, customFields] = yield Promise.all([
55
+ api.getCorrespondents(),
56
+ api.getDocumentTypes(),
57
+ api.getTags(),
58
+ api.getCustomFields(),
59
+ ]);
60
+ const correspondentMap = new Map((correspondents.results || []).map((c) => [c.id, c.name]));
61
+ const documentTypeMap = new Map((documentTypes.results || []).map((dt) => [dt.id, dt.name]));
62
+ const tagMap = new Map((tags.results || []).map((tag) => [tag.id, tag.name]));
63
+ const customFieldMap = new Map((customFields.results || []).map((cf) => [cf.id, cf.name]));
64
+ return documents.map((doc) => (Object.assign(Object.assign({}, doc), { correspondent: doc.correspondent
65
+ ? {
66
+ id: doc.correspondent,
67
+ name: correspondentMap.get(doc.correspondent) ||
68
+ String(doc.correspondent),
69
+ }
70
+ : null, document_type: doc.document_type
71
+ ? {
72
+ id: doc.document_type,
73
+ name: documentTypeMap.get(doc.document_type) || String(doc.document_type),
74
+ }
75
+ : null, tags: Array.isArray(doc.tags)
76
+ ? doc.tags.map((tagId) => ({
77
+ id: tagId,
78
+ name: tagMap.get(tagId) || String(tagId),
79
+ }))
80
+ : doc.tags, custom_fields: Array.isArray(doc.custom_fields)
81
+ ? doc.custom_fields.map((field) => ({
82
+ field: field.field,
83
+ name: customFieldMap.get(field.field) || String(field.field),
84
+ value: field.value,
85
+ }))
86
+ : doc.custom_fields })));
87
+ });
88
+ }
@@ -12,6 +12,21 @@ export interface Tag {
12
12
  owner: number | null;
13
13
  user_can_change: boolean;
14
14
  }
15
+ export interface CustomField {
16
+ id: number;
17
+ name: string;
18
+ data_type: string;
19
+ extra_data?: Record<string, unknown> | null;
20
+ document_count: number;
21
+ }
22
+ export interface CustomFieldInstance {
23
+ field: number;
24
+ value: string | number | boolean | object | null;
25
+ }
26
+ export interface CustomFieldInstanceRequest {
27
+ field: number;
28
+ value: string | number | boolean | object | null;
29
+ }
15
30
  export interface PaginationResponse<T> {
16
31
  count: number;
17
32
  next: string | null;
@@ -21,6 +36,8 @@ export interface PaginationResponse<T> {
21
36
  }
22
37
  export interface GetTagsResponse extends PaginationResponse<Tag> {
23
38
  }
39
+ export interface GetCustomFieldsResponse extends PaginationResponse<CustomField> {
40
+ }
24
41
  export interface DocumentsResponse extends PaginationResponse<Document> {
25
42
  }
26
43
  export interface Document {
@@ -43,7 +60,7 @@ export interface Document {
43
60
  user_can_change: boolean;
44
61
  is_shared_by_requester: boolean;
45
62
  notes: any[];
46
- custom_fields: any[];
63
+ custom_fields: CustomFieldInstance[];
47
64
  page_count: number;
48
65
  mime_type: string;
49
66
  __search_hit__?: SearchHit;
@@ -87,3 +104,32 @@ export interface GetDocumentTypesResponse extends PaginationResponse<DocumentTyp
87
104
  export interface BulkEditDocumentsResult {
88
105
  result: string;
89
106
  }
107
+ export interface BulkEditParameters {
108
+ assign_custom_fields?: number[];
109
+ assign_custom_fields_values?: CustomFieldInstanceRequest[];
110
+ remove_custom_fields?: number[];
111
+ add_tags?: number[];
112
+ remove_tags?: number[];
113
+ degrees?: number;
114
+ pages?: string;
115
+ metadata_document_id?: number;
116
+ delete_originals?: boolean;
117
+ correspondent?: number;
118
+ document_type?: number;
119
+ storage_path?: number;
120
+ tag?: number;
121
+ permissions?: {
122
+ owner?: number | null;
123
+ set_permissions?: {
124
+ view: {
125
+ users: number[];
126
+ groups: number[];
127
+ };
128
+ change: {
129
+ users: number[];
130
+ groups: number[];
131
+ };
132
+ };
133
+ merge?: boolean;
134
+ };
135
+ }
package/build/index.js CHANGED
@@ -21,6 +21,7 @@ const express_1 = __importDefault(require("express"));
21
21
  const node_util_1 = require("node:util");
22
22
  const PaperlessAPI_1 = require("./api/PaperlessAPI");
23
23
  const correspondents_1 = require("./tools/correspondents");
24
+ const customFields_1 = require("./tools/customFields");
24
25
  const documents_1 = require("./tools/documents");
25
26
  const documentTypes_1 = require("./tools/documentTypes");
26
27
  const tags_1 = require("./tools/tags");
@@ -51,8 +52,15 @@ function main() {
51
52
  instructions: `
52
53
  Paperless-NGX MCP Server Instructions
53
54
 
55
+ ⚠️ CRITICAL: Always differentiate between operations on specific documents vs operations on the entire system:
56
+
57
+ - REMOVE operations (e.g., remove_tag in bulk_edit_documents): Affect only the specified documents, items remain in the system
58
+ - DELETE operations (e.g., delete_tag, delete_correspondent): Permanently delete items from the entire system, affecting ALL documents that use them
59
+
60
+ When a user asks to "remove" something, prefer operations that affect specific documents. Only use DELETE operations when explicitly asked to delete from the system.
61
+
54
62
  To view documents in your Paperless-NGX web interface, construct URLs using this pattern:
55
- ${resolvedBaseUrl}/documents/{document_id}/
63
+ ${resolvedPublicUrl}/documents/{document_id}/
56
64
 
57
65
  Example: If your base URL is "http://localhost:8000", the web interface URL would be "http://localhost:8000/documents/123/" for document ID 123.
58
66
 
@@ -63,6 +71,7 @@ The document tools return JSON data with document IDs that you can use to constr
63
71
  (0, tags_1.registerTagTools)(server, api);
64
72
  (0, correspondents_1.registerCorrespondentTools)(server, api);
65
73
  (0, documentTypes_1.registerDocumentTypeTools)(server, api);
74
+ (0, customFields_1.registerCustomFieldTools)(server, api);
66
75
  if (useHttp) {
67
76
  const app = (0, express_1.default)();
68
77
  app.use(express_1.default.json());
@@ -151,11 +160,11 @@ The document tools return JSON data with document IDs that you can use to constr
151
160
  app.listen(resolvedPort, () => {
152
161
  console.log(`MCP Stateless Streamable HTTP Server listening on port ${resolvedPort}`);
153
162
  });
163
+ // await new Promise((resolve) => setTimeout(resolve, 1000000));
154
164
  }
155
165
  else {
156
166
  const transport = new stdio_js_1.StdioServerTransport();
157
167
  yield server.connect(transport);
158
- console.log("MCP server running with stdio transport");
159
168
  }
160
169
  });
161
170
  }
@@ -22,7 +22,7 @@ function registerCorrespondentTools(server, api) {
22
22
  name__iexact: zod_1.z.string().optional(),
23
23
  name__istartswith: zod_1.z.string().optional(),
24
24
  ordering: zod_1.z.string().optional(),
25
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
25
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
26
26
  if (!api)
27
27
  throw new Error("Please configure API connection first");
28
28
  const queryString = (0, queryString_1.buildQueryString)(args);
@@ -36,7 +36,7 @@ function registerCorrespondentTools(server, api) {
36
36
  ],
37
37
  };
38
38
  })));
39
- server.tool("get_correspondent", { id: zod_1.z.number() }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
39
+ server.tool("get_correspondent", { id: zod_1.z.number() }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
40
40
  if (!api)
41
41
  throw new Error("Please configure API connection first");
42
42
  const response = yield api.request(`/correspondents/${args.id}/`);
@@ -50,7 +50,7 @@ function registerCorrespondentTools(server, api) {
50
50
  matching_algorithm: zod_1.z
51
51
  .enum(["any", "all", "exact", "regular expression", "fuzzy"])
52
52
  .optional(),
53
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
53
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
54
54
  if (!api)
55
55
  throw new Error("Please configure API connection first");
56
56
  const response = yield api.createCorrespondent(args);
@@ -65,7 +65,7 @@ function registerCorrespondentTools(server, api) {
65
65
  matching_algorithm: zod_1.z
66
66
  .enum(["any", "all", "exact", "regular expression", "fuzzy"])
67
67
  .optional(),
68
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
68
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
69
69
  if (!api)
70
70
  throw new Error("Please configure API connection first");
71
71
  const response = yield api.request(`/correspondents/${args.id}/`, {
@@ -76,9 +76,17 @@ function registerCorrespondentTools(server, api) {
76
76
  content: [{ type: "text", text: JSON.stringify(response) }],
77
77
  };
78
78
  })));
79
- server.tool("delete_correspondent", { id: zod_1.z.number() }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
79
+ server.tool("delete_correspondent", "⚠️ DESTRUCTIVE: Permanently delete a correspondent from the entire system. This will affect ALL documents that use this correspondent.", {
80
+ id: zod_1.z.number(),
81
+ confirm: zod_1.z
82
+ .boolean()
83
+ .describe("Must be true to confirm this destructive operation"),
84
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
80
85
  if (!api)
81
86
  throw new Error("Please configure API connection first");
87
+ if (!args.confirm) {
88
+ throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
89
+ }
82
90
  yield api.request(`/correspondents/${args.id}/`, { method: "DELETE" });
83
91
  return {
84
92
  content: [
@@ -86,9 +94,13 @@ function registerCorrespondentTools(server, api) {
86
94
  ],
87
95
  };
88
96
  })));
89
- server.tool("bulk_edit_correspondents", {
97
+ server.tool("bulk_edit_correspondents", "Bulk edit correspondents. ⚠️ WARNING: 'delete' operation permanently removes correspondents from the entire system.", {
90
98
  correspondent_ids: zod_1.z.array(zod_1.z.number()),
91
99
  operation: zod_1.z.enum(["set_permissions", "delete"]),
100
+ confirm: zod_1.z
101
+ .boolean()
102
+ .optional()
103
+ .describe("Must be true when operation is 'delete' to confirm destructive operation"),
92
104
  owner: zod_1.z.number().optional(),
93
105
  permissions: zod_1.z
94
106
  .object({
@@ -103,9 +115,12 @@ function registerCorrespondentTools(server, api) {
103
115
  })
104
116
  .optional(),
105
117
  merge: zod_1.z.boolean().optional(),
106
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
118
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
107
119
  if (!api)
108
120
  throw new Error("Please configure API connection first");
121
+ if (args.operation === "delete" && !args.confirm) {
122
+ throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
123
+ }
109
124
  return api.bulkEditObjects(args.correspondent_ids, "correspondents", args.operation, args.operation === "set_permissions"
110
125
  ? {
111
126
  owner: args.owner,
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
2
+ import { PaperlessAPI } from "../api/PaperlessAPI";
3
+ export declare function registerCustomFieldTools(server: McpServer, api: PaperlessAPI): void;
@@ -0,0 +1,142 @@
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.registerCustomFieldTools = registerCustomFieldTools;
24
+ const zod_1 = require("zod");
25
+ const middlewares_1 = require("./utils/middlewares");
26
+ const queryString_1 = require("./utils/queryString");
27
+ function registerCustomFieldTools(server, api) {
28
+ server.tool("list_custom_fields", "List all custom fields. IMPORTANT: When a user query may refer to a custom field, you should fetch all custom fields up front (with a large enough page_size), cache them for the session, and search locally for matches by name before making further API calls. This reduces redundant requests and handles ambiguity efficiently.", {
29
+ page: zod_1.z.number().optional(),
30
+ page_size: zod_1.z.number().optional(),
31
+ name__icontains: zod_1.z.string().optional(),
32
+ name__iendswith: zod_1.z.string().optional(),
33
+ name__iexact: zod_1.z.string().optional(),
34
+ name__istartswith: zod_1.z.string().optional(),
35
+ ordering: zod_1.z.string().optional(),
36
+ }, (0, middlewares_1.withErrorHandling)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) {
37
+ if (!api)
38
+ throw new Error("Please configure API connection first");
39
+ const queryString = (0, queryString_1.buildQueryString)(args);
40
+ const response = yield api.request(`/custom_fields/${queryString ? `?${queryString}` : ""}`);
41
+ return {
42
+ content: [
43
+ {
44
+ type: "text",
45
+ text: JSON.stringify(response),
46
+ },
47
+ ],
48
+ };
49
+ })));
50
+ server.tool("get_custom_field", { id: zod_1.z.number() }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
51
+ if (!api)
52
+ throw new Error("Please configure API connection first");
53
+ const response = yield api.getCustomField(args.id);
54
+ return {
55
+ content: [{ type: "text", text: JSON.stringify(response) }],
56
+ };
57
+ })));
58
+ server.tool("create_custom_field", {
59
+ name: zod_1.z.string(),
60
+ data_type: zod_1.z.enum([
61
+ "string",
62
+ "url",
63
+ "date",
64
+ "boolean",
65
+ "integer",
66
+ "float",
67
+ "monetary",
68
+ "documentlink",
69
+ "select",
70
+ ]),
71
+ extra_data: zod_1.z.record(zod_1.z.unknown()).nullable().optional(),
72
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
73
+ if (!api)
74
+ throw new Error("Please configure API connection first");
75
+ const response = yield api.createCustomField(args);
76
+ return {
77
+ content: [{ type: "text", text: JSON.stringify(response) }],
78
+ };
79
+ })));
80
+ server.tool("update_custom_field", {
81
+ id: zod_1.z.number(),
82
+ name: zod_1.z.string().optional(),
83
+ data_type: zod_1.z
84
+ .enum([
85
+ "string",
86
+ "url",
87
+ "date",
88
+ "boolean",
89
+ "integer",
90
+ "float",
91
+ "monetary",
92
+ "documentlink",
93
+ "select",
94
+ ])
95
+ .optional(),
96
+ extra_data: zod_1.z.record(zod_1.z.unknown()).nullable().optional(),
97
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
98
+ if (!api)
99
+ throw new Error("Please configure API connection first");
100
+ const { id } = args, data = __rest(args, ["id"]);
101
+ const response = yield api.updateCustomField(id, data);
102
+ return {
103
+ content: [{ type: "text", text: JSON.stringify(response) }],
104
+ };
105
+ })));
106
+ server.tool("delete_custom_field", "⚠️ DESTRUCTIVE: Permanently delete a custom field from the entire system. This will remove the field from ALL documents that use it.", {
107
+ id: zod_1.z.number(),
108
+ confirm: zod_1.z.boolean().describe("Must be true to confirm this destructive operation"),
109
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
110
+ if (!api)
111
+ throw new Error("Please configure API connection first");
112
+ if (!args.confirm) {
113
+ throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
114
+ }
115
+ yield api.deleteCustomField(args.id);
116
+ return {
117
+ content: [
118
+ { type: "text", text: JSON.stringify({ status: "deleted" }) },
119
+ ],
120
+ };
121
+ })));
122
+ server.tool("bulk_edit_custom_fields", "Bulk edit custom fields. ⚠️ WARNING: 'delete' operation permanently removes custom fields from the entire system.", {
123
+ custom_fields: zod_1.z.array(zod_1.z.number()),
124
+ operation: zod_1.z.enum(["delete"]),
125
+ confirm: zod_1.z.boolean().optional().describe("Must be true when operation is 'delete' to confirm destructive operation"),
126
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
127
+ if (!api)
128
+ throw new Error("Please configure API connection first");
129
+ if (args.operation === "delete" && !args.confirm) {
130
+ throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
131
+ }
132
+ const response = yield api.bulkEditObjects(args.custom_fields, "custom_field", args.operation);
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: JSON.stringify(response),
138
+ },
139
+ ],
140
+ };
141
+ })));
142
+ }
@@ -22,7 +22,7 @@ function registerDocumentTypeTools(server, api) {
22
22
  name__iexact: zod_1.z.string().optional(),
23
23
  name__istartswith: zod_1.z.string().optional(),
24
24
  ordering: zod_1.z.string().optional(),
25
- }, (0, middlewares_1.errorMiddleware)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}, extra) {
25
+ }, (0, middlewares_1.withErrorHandling)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}, extra) {
26
26
  if (!api)
27
27
  throw new Error("Please configure API connection first");
28
28
  const queryString = (0, queryString_1.buildQueryString)(args);
@@ -36,7 +36,7 @@ function registerDocumentTypeTools(server, api) {
36
36
  ],
37
37
  };
38
38
  })));
39
- server.tool("get_document_type", { id: zod_1.z.number() }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
39
+ server.tool("get_document_type", { id: zod_1.z.number() }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
40
40
  if (!api)
41
41
  throw new Error("Please configure API connection first");
42
42
  const response = yield api.request(`/document_types/${args.id}/`);
@@ -50,7 +50,7 @@ function registerDocumentTypeTools(server, api) {
50
50
  matching_algorithm: zod_1.z
51
51
  .enum(["any", "all", "exact", "regular expression", "fuzzy"])
52
52
  .optional(),
53
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
53
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
54
54
  if (!api)
55
55
  throw new Error("Please configure API connection first");
56
56
  const response = yield api.createDocumentType(args);
@@ -65,7 +65,7 @@ function registerDocumentTypeTools(server, api) {
65
65
  matching_algorithm: zod_1.z
66
66
  .enum(["any", "all", "exact", "regular expression", "fuzzy"])
67
67
  .optional(),
68
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
68
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
69
69
  if (!api)
70
70
  throw new Error("Please configure API connection first");
71
71
  const response = yield api.request(`/document_types/${args.id}/`, {
@@ -76,9 +76,17 @@ function registerDocumentTypeTools(server, api) {
76
76
  content: [{ type: "text", text: JSON.stringify(response) }],
77
77
  };
78
78
  })));
79
- server.tool("delete_document_type", { id: zod_1.z.number() }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
79
+ server.tool("delete_document_type", "⚠️ DESTRUCTIVE: Permanently delete a document type from the entire system. This will affect ALL documents that use this type.", {
80
+ id: zod_1.z.number(),
81
+ confirm: zod_1.z
82
+ .boolean()
83
+ .describe("Must be true to confirm this destructive operation"),
84
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
80
85
  if (!api)
81
86
  throw new Error("Please configure API connection first");
87
+ if (!args.confirm) {
88
+ throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
89
+ }
82
90
  yield api.request(`/document_types/${args.id}/`, { method: "DELETE" });
83
91
  return {
84
92
  content: [
@@ -86,9 +94,13 @@ function registerDocumentTypeTools(server, api) {
86
94
  ],
87
95
  };
88
96
  })));
89
- server.tool("bulk_edit_document_types", {
97
+ server.tool("bulk_edit_document_types", "Bulk edit document types. ⚠️ WARNING: 'delete' operation permanently removes document types from the entire system.", {
90
98
  document_type_ids: zod_1.z.array(zod_1.z.number()),
91
99
  operation: zod_1.z.enum(["set_permissions", "delete"]),
100
+ confirm: zod_1.z
101
+ .boolean()
102
+ .optional()
103
+ .describe("Must be true when operation is 'delete' to confirm destructive operation"),
92
104
  owner: zod_1.z.number().optional(),
93
105
  permissions: zod_1.z
94
106
  .object({
@@ -103,9 +115,12 @@ function registerDocumentTypeTools(server, api) {
103
115
  })
104
116
  .optional(),
105
117
  merge: zod_1.z.boolean().optional(),
106
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
118
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
107
119
  if (!api)
108
120
  throw new Error("Please configure API connection first");
121
+ if (args.operation === "delete" && !args.confirm) {
122
+ throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
123
+ }
109
124
  return api.bulkEditObjects(args.document_type_ids, "document_types", args.operation, args.operation === "set_permissions"
110
125
  ? {
111
126
  owner: args.owner,
@@ -22,9 +22,11 @@ var __rest = (this && this.__rest) || function (s, e) {
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.registerDocumentTools = registerDocumentTools;
24
24
  const zod_1 = require("zod");
25
+ const documentEnhancer_1 = require("../api/documentEnhancer");
26
+ const empty_1 = require("./utils/empty");
25
27
  const middlewares_1 = require("./utils/middlewares");
26
28
  function registerDocumentTools(server, api) {
27
- server.tool("bulk_edit_documents", {
29
+ server.tool("bulk_edit_documents", "Perform bulk operations on multiple documents. Note: 'remove_tag' removes a tag from specific documents (tag remains in system), while 'delete_tag' permanently deletes a tag from the entire system. ⚠️ WARNING: 'delete' method permanently deletes documents and requires confirmation.", {
28
30
  documents: zod_1.z.array(zod_1.z.number()),
29
31
  method: zod_1.z.enum([
30
32
  "set_correspondent",
@@ -33,6 +35,7 @@ function registerDocumentTools(server, api) {
33
35
  "add_tag",
34
36
  "remove_tag",
35
37
  "modify_tags",
38
+ "modify_custom_fields",
36
39
  "delete",
37
40
  "reprocess",
38
41
  "set_permissions",
@@ -45,8 +48,19 @@ function registerDocumentTools(server, api) {
45
48
  document_type: zod_1.z.number().optional(),
46
49
  storage_path: zod_1.z.number().optional(),
47
50
  tag: zod_1.z.number().optional(),
48
- add_tags: zod_1.z.array(zod_1.z.number()).optional(),
49
- remove_tags: zod_1.z.array(zod_1.z.number()).optional(),
51
+ add_tags: zod_1.z.array(zod_1.z.number()).optional().transform(empty_1.arrayNotEmpty),
52
+ remove_tags: zod_1.z.array(zod_1.z.number()).optional().transform(empty_1.arrayNotEmpty),
53
+ add_custom_fields: zod_1.z
54
+ .array(zod_1.z.object({
55
+ field: zod_1.z.number(),
56
+ value: zod_1.z.union([zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean(), zod_1.z.null()]),
57
+ }))
58
+ .optional()
59
+ .transform(empty_1.arrayNotEmpty),
60
+ remove_custom_fields: zod_1.z
61
+ .array(zod_1.z.number())
62
+ .optional()
63
+ .transform(empty_1.arrayNotEmpty),
50
64
  permissions: zod_1.z
51
65
  .object({
52
66
  owner: zod_1.z.number().nullable().optional(),
@@ -64,16 +78,30 @@ function registerDocumentTools(server, api) {
64
78
  .optional(),
65
79
  merge: zod_1.z.boolean().optional(),
66
80
  })
67
- .optional(),
81
+ .optional()
82
+ .transform(empty_1.objectNotEmpty),
68
83
  metadata_document_id: zod_1.z.number().optional(),
69
84
  delete_originals: zod_1.z.boolean().optional(),
70
85
  pages: zod_1.z.string().optional(),
71
86
  degrees: zod_1.z.number().optional(),
72
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
87
+ confirm: zod_1.z
88
+ .boolean()
89
+ .optional()
90
+ .describe("Must be true when method is 'delete' to confirm destructive operation"),
91
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
73
92
  if (!api)
74
93
  throw new Error("Please configure API connection first");
75
- const { documents, method } = args, parameters = __rest(args, ["documents", "method"]);
76
- const response = yield api.bulkEditDocuments(documents, method, parameters);
94
+ if (args.method === "delete" && !args.confirm) {
95
+ throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
96
+ }
97
+ const { documents, method, add_custom_fields } = args, parameters = __rest(args, ["documents", "method", "add_custom_fields"]);
98
+ // Transform add_custom_fields into the two separate API parameters
99
+ const apiParameters = Object.assign({}, parameters);
100
+ if (add_custom_fields && add_custom_fields.length > 0) {
101
+ apiParameters.assign_custom_fields = add_custom_fields.map((cf) => cf.field);
102
+ apiParameters.assign_custom_fields_values = add_custom_fields;
103
+ }
104
+ const response = yield api.bulkEditDocuments(documents, method, apiParameters);
77
105
  return {
78
106
  content: [
79
107
  {
@@ -94,7 +122,7 @@ function registerDocumentTools(server, api) {
94
122
  tags: zod_1.z.array(zod_1.z.number()).optional(),
95
123
  archive_serial_number: zod_1.z.string().optional(),
96
124
  custom_fields: zod_1.z.array(zod_1.z.number()).optional(),
97
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
125
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
98
126
  if (!api)
99
127
  throw new Error("Please configure API connection first");
100
128
  const binaryData = Buffer.from(args.file, "base64");
@@ -126,10 +154,10 @@ function registerDocumentTools(server, api) {
126
154
  document_type: zod_1.z.number().optional(),
127
155
  tag: zod_1.z.number().optional(),
128
156
  storage_path: zod_1.z.number().optional(),
129
- created__gte: zod_1.z.string().optional(),
130
- created__lte: zod_1.z.string().optional(),
157
+ created__date__gte: zod_1.z.string().optional(),
158
+ created__date__lte: zod_1.z.string().optional(),
131
159
  ordering: zod_1.z.string().optional(),
132
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
160
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
133
161
  if (!api)
134
162
  throw new Error("Please configure API connection first");
135
163
  const query = new URLSearchParams();
@@ -147,29 +175,31 @@ function registerDocumentTools(server, api) {
147
175
  query.set("tags__id", args.tag.toString());
148
176
  if (args.storage_path)
149
177
  query.set("storage_path__id", args.storage_path.toString());
150
- if (args.created__gte)
151
- query.set("created__gte", args.created__gte);
152
- if (args.created__lte)
153
- query.set("created__lte", args.created__lte);
178
+ if (args.created__date__gte)
179
+ query.set("created__date__gte", args.created__date__gte);
180
+ if (args.created__date__lte)
181
+ query.set("created__date__lte", args.created__date__lte);
154
182
  if (args.ordering)
155
183
  query.set("ordering", args.ordering);
156
184
  const docsResponse = yield api.getDocuments(query.toString() ? `?${query.toString()}` : "");
157
- return convertDocsWithNames(docsResponse, api);
185
+ return (0, documentEnhancer_1.convertDocsWithNames)(docsResponse, api);
158
186
  })));
159
187
  server.tool("get_document", {
160
188
  id: zod_1.z.number(),
161
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
189
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
162
190
  if (!api)
163
191
  throw new Error("Please configure API connection first");
164
192
  const doc = yield api.getDocument(args.id);
165
- const [correspondents, documentTypes, tags] = yield Promise.all([
193
+ const [correspondents, documentTypes, tags, customFields] = yield Promise.all([
166
194
  api.getCorrespondents(),
167
195
  api.getDocumentTypes(),
168
196
  api.getTags(),
197
+ api.getCustomFields(),
169
198
  ]);
170
199
  const correspondentMap = new Map((correspondents.results || []).map((c) => [c.id, c.name]));
171
200
  const documentTypeMap = new Map((documentTypes.results || []).map((dt) => [dt.id, dt.name]));
172
201
  const tagMap = new Map((tags.results || []).map((tag) => [tag.id, tag.name]));
202
+ const customFieldMap = new Map((customFields.results || []).map((cf) => [cf.id, cf.name]));
173
203
  const docWithNames = Object.assign(Object.assign({}, doc), { correspondent: doc.correspondent
174
204
  ? {
175
205
  id: doc.correspondent,
@@ -187,7 +217,13 @@ function registerDocumentTools(server, api) {
187
217
  id: tagId,
188
218
  name: tagMap.get(tagId) || String(tagId),
189
219
  }))
190
- : doc.tags });
220
+ : doc.tags, custom_fields: Array.isArray(doc.custom_fields)
221
+ ? doc.custom_fields.map((field) => ({
222
+ field: field.field,
223
+ name: customFieldMap.get(field.field) || String(field.field),
224
+ value: field.value,
225
+ }))
226
+ : doc.custom_fields });
191
227
  return {
192
228
  content: [
193
229
  {
@@ -199,16 +235,16 @@ function registerDocumentTools(server, api) {
199
235
  })));
200
236
  server.tool("search_documents", "Full text search for documents. This tool is for searching document content, title, and metadata using a full text query. For general document listing or filtering by fields, use 'list_documents' instead.", {
201
237
  query: zod_1.z.string(),
202
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
238
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
203
239
  if (!api)
204
240
  throw new Error("Please configure API connection first");
205
241
  const docsResponse = yield api.searchDocuments(args.query);
206
- return convertDocsWithNames(docsResponse, api);
242
+ return (0, documentEnhancer_1.convertDocsWithNames)(docsResponse, api);
207
243
  })));
208
244
  server.tool("download_document", {
209
245
  id: zod_1.z.number(),
210
246
  original: zod_1.z.boolean().optional(),
211
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
247
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
212
248
  var _a, _b;
213
249
  if (!api)
214
250
  throw new Error("Please configure API connection first");
@@ -229,53 +265,63 @@ function registerDocumentTools(server, api) {
229
265
  ],
230
266
  };
231
267
  })));
232
- }
233
- function convertDocsWithNames(docsResponse, api) {
234
- return __awaiter(this, void 0, void 0, function* () {
235
- var _a;
236
- if (!((_a = docsResponse.results) === null || _a === void 0 ? void 0 : _a.length)) {
237
- return {
238
- content: [
239
- {
240
- type: "text",
241
- text: "No documents found",
242
- },
243
- ],
244
- };
245
- }
246
- // Fetch all related entities for name mapping
247
- const [correspondents, documentTypes, tags] = yield Promise.all([
248
- api.getCorrespondents(),
249
- api.getDocumentTypes(),
250
- api.getTags(),
251
- ]);
252
- const correspondentMap = new Map((correspondents.results || []).map((c) => [c.id, c.name]));
253
- const documentTypeMap = new Map((documentTypes.results || []).map((dt) => [dt.id, dt.name]));
254
- const tagMap = new Map((tags.results || []).map((tag) => [tag.id, tag.name]));
255
- const docsWithNames = docsResponse.results.map((doc) => (Object.assign(Object.assign({}, doc), { correspondent: doc.correspondent
256
- ? {
257
- id: doc.correspondent,
258
- name: correspondentMap.get(doc.correspondent) ||
259
- String(doc.correspondent),
260
- }
261
- : null, document_type: doc.document_type
262
- ? {
263
- id: doc.document_type,
264
- name: documentTypeMap.get(doc.document_type) || String(doc.document_type),
265
- }
266
- : null, tags: Array.isArray(doc.tags)
267
- ? doc.tags.map((tagId) => ({
268
- id: tagId,
269
- name: tagMap.get(tagId) || String(tagId),
270
- }))
271
- : doc.tags })));
272
- return {
273
- content: [
274
- {
275
- type: "text",
276
- text: JSON.stringify(docsWithNames),
277
- },
278
- ],
279
- };
280
- });
268
+ server.tool("update_document", "Update a specific document with new values. This tool allows you to modify any document field including title, correspondent, document type, storage path, tags, custom fields, and more. Only the fields you specify will be updated.", {
269
+ id: zod_1.z.number().describe("The ID of the document to update"),
270
+ title: zod_1.z
271
+ .string()
272
+ .max(128)
273
+ .optional()
274
+ .describe("The new title for the document (max 128 characters)"),
275
+ correspondent: zod_1.z
276
+ .number()
277
+ .nullable()
278
+ .optional()
279
+ .describe("The ID of the correspondent to assign"),
280
+ document_type: zod_1.z
281
+ .number()
282
+ .nullable()
283
+ .optional()
284
+ .describe("The ID of the document type to assign"),
285
+ storage_path: zod_1.z
286
+ .number()
287
+ .nullable()
288
+ .optional()
289
+ .describe("The ID of the storage path to assign"),
290
+ tags: zod_1.z
291
+ .array(zod_1.z.number())
292
+ .optional()
293
+ .describe("Array of tag IDs to assign to the document"),
294
+ content: zod_1.z
295
+ .string()
296
+ .optional()
297
+ .describe("The raw text content of the document (used for searching)"),
298
+ created: zod_1.z
299
+ .string()
300
+ .optional()
301
+ .describe("The creation date in YYYY-MM-DD format"),
302
+ archive_serial_number: zod_1.z
303
+ .number()
304
+ .optional()
305
+ .describe("The archive serial number (0-4294967295)"),
306
+ owner: zod_1.z
307
+ .number()
308
+ .nullable()
309
+ .optional()
310
+ .describe("The ID of the user who owns the document"),
311
+ custom_fields: zod_1.z
312
+ .array(zod_1.z.object({
313
+ field: zod_1.z.number().describe("The custom field ID"),
314
+ value: zod_1.z
315
+ .union([zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean(), zod_1.z.null()])
316
+ .describe("The value for the custom field"),
317
+ }))
318
+ .optional()
319
+ .describe("Array of custom field values to assign"),
320
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
321
+ if (!api)
322
+ throw new Error("Please configure API connection first");
323
+ const { id } = args, updateData = __rest(args, ["id"]);
324
+ const response = yield api.updateDocument(id, updateData);
325
+ return (0, documentEnhancer_1.convertDocsWithNames)(response, api);
326
+ })));
281
327
  }
@@ -22,7 +22,7 @@ function registerTagTools(server, api) {
22
22
  name__iexact: zod_1.z.string().optional(),
23
23
  name__istartswith: zod_1.z.string().optional(),
24
24
  ordering: zod_1.z.string().optional(),
25
- }, (0, middlewares_1.errorMiddleware)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) {
25
+ }, (0, middlewares_1.withErrorHandling)((...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) {
26
26
  if (!api)
27
27
  throw new Error("Please configure API connection first");
28
28
  const queryString = (0, queryString_1.buildQueryString)(args);
@@ -44,7 +44,7 @@ function registerTagTools(server, api) {
44
44
  .optional(),
45
45
  match: zod_1.z.string().optional(),
46
46
  matching_algorithm: zod_1.z.number().int().min(0).max(4).optional(),
47
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
47
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
48
48
  if (!api)
49
49
  throw new Error("Please configure API connection first");
50
50
  const tag = yield api.createTag(args);
@@ -66,7 +66,7 @@ function registerTagTools(server, api) {
66
66
  .optional(),
67
67
  match: zod_1.z.string().optional(),
68
68
  matching_algorithm: zod_1.z.number().int().min(0).max(4).optional(),
69
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
69
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
70
70
  if (!api)
71
71
  throw new Error("Please configure API connection first");
72
72
  const tag = yield api.updateTag(args.id, args);
@@ -79,11 +79,15 @@ function registerTagTools(server, api) {
79
79
  ],
80
80
  };
81
81
  })));
82
- server.tool("delete_tag", {
82
+ server.tool("delete_tag", "⚠️ DESTRUCTIVE: Permanently delete a tag from the entire system. This will remove the tag from ALL documents that use it. Use with extreme caution.", {
83
83
  id: zod_1.z.number(),
84
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
84
+ confirm: zod_1.z.boolean().describe("Must be true to confirm this destructive operation"),
85
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
85
86
  if (!api)
86
87
  throw new Error("Please configure API connection first");
88
+ if (!args.confirm) {
89
+ throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
90
+ }
87
91
  yield api.deleteTag(args.id);
88
92
  return {
89
93
  content: [
@@ -94,9 +98,10 @@ function registerTagTools(server, api) {
94
98
  ],
95
99
  };
96
100
  })));
97
- server.tool("bulk_edit_tags", {
101
+ server.tool("bulk_edit_tags", "Bulk edit tags. ⚠️ WARNING: 'delete' operation permanently removes tags from the entire system. Use with caution.", {
98
102
  tag_ids: zod_1.z.array(zod_1.z.number()),
99
103
  operation: zod_1.z.enum(["set_permissions", "delete"]),
104
+ confirm: zod_1.z.boolean().optional().describe("Must be true when operation is 'delete' to confirm destructive operation"),
100
105
  owner: zod_1.z.number().optional(),
101
106
  permissions: zod_1.z
102
107
  .object({
@@ -111,9 +116,12 @@ function registerTagTools(server, api) {
111
116
  })
112
117
  .optional(),
113
118
  merge: zod_1.z.boolean().optional(),
114
- }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () {
119
+ }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
115
120
  if (!api)
116
121
  throw new Error("Please configure API connection first");
122
+ if (args.operation === "delete" && !args.confirm) {
123
+ throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
124
+ }
117
125
  return api.bulkEditObjects(args.tag_ids, "tags", args.operation, args.operation === "set_permissions"
118
126
  ? {
119
127
  owner: args.owner,
@@ -0,0 +1,2 @@
1
+ export declare const arrayNotEmpty: <T>(array: T[] | undefined) => T[] | undefined;
2
+ export declare const objectNotEmpty: <T>(object: T) => T | undefined;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.objectNotEmpty = exports.arrayNotEmpty = void 0;
4
+ const arrayNotEmpty = (array) => (array === null || array === void 0 ? void 0 : array.length) ? array : undefined;
5
+ exports.arrayNotEmpty = arrayNotEmpty;
6
+ const objectNotEmpty = (object) => object && Object.keys(object).length ? object : undefined;
7
+ exports.objectNotEmpty = objectNotEmpty;
@@ -1,3 +1,3 @@
1
1
  import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp";
2
2
  import { ZodRawShape } from "zod";
3
- export declare const errorMiddleware: <Args extends ZodRawShape>(cb: ToolCallback<Args>) => ToolCallback<Args>;
3
+ export declare const withErrorHandling: <Args extends ZodRawShape>(cb: ToolCallback<Args>) => ToolCallback<Args>;
@@ -9,24 +9,23 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.errorMiddleware = void 0;
13
- const errorMiddleware = (cb) => {
12
+ exports.withErrorHandling = void 0;
13
+ const withErrorHandling = (cb) => {
14
14
  return ((args, extra) => __awaiter(void 0, void 0, void 0, function* () {
15
+ var _a, _b;
15
16
  try {
16
17
  return yield cb(args, extra);
17
18
  }
18
19
  catch (err) {
19
- return {
20
- content: [
21
- {
22
- type: "text",
23
- text: JSON.stringify({
24
- error: err instanceof Error ? err.message : String(err),
25
- }),
26
- },
27
- ],
28
- };
20
+ const errorMessage = err instanceof Error ? err.message : String(err);
21
+ const responseData = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data;
22
+ const status = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status;
23
+ throw new Error(JSON.stringify({
24
+ error: errorMessage,
25
+ responseData,
26
+ status,
27
+ }));
29
28
  }
30
29
  }));
31
30
  };
32
- exports.errorMiddleware = errorMiddleware;
31
+ exports.withErrorHandling = withErrorHandling;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baruchiro/paperless-mcp",
3
- "version": "0.0.3",
3
+ "version": "0.1.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": {
@@ -10,8 +10,9 @@
10
10
  "test": "echo \"Error: no test specified\" && exit 1",
11
11
  "start": "ts-node src/index.ts",
12
12
  "build": "tsc",
13
+ "dxt-pack": "dxt pack",
13
14
  "inspect": "npm run build && npx -y @modelcontextprotocol/inspector node build/index.js",
14
- "prepublishOnly": "npm run build"
15
+ "prepack": "npm run build && npm run dxt-pack"
15
16
  },
16
17
  "keywords": [
17
18
  "mcp",
@@ -32,7 +33,8 @@
32
33
  "files": [
33
34
  "build",
34
35
  "README.md",
35
- "LICENSE"
36
+ "LICENSE",
37
+ "paperless-mcp.dxt"
36
38
  ],
37
39
  "bugs": {
38
40
  "url": "https://github.com/baruchiro/paperless-mcp/issues"
@@ -46,6 +48,7 @@
46
48
  "zod": "^3.24.1"
47
49
  },
48
50
  "devDependencies": {
51
+ "@anthropic-ai/dxt": "^0.2.6",
49
52
  "@changesets/cli": "^2.29.4",
50
53
  "@types/express": "^5.0.2",
51
54
  "@types/node": "^22.15.17",
Binary file