@baruchiro/paperless-mcp 0.0.2 → 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
@@ -1,3 +1,5 @@
1
+ <!-- [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/nloui-paperless-mcp-badge.png)](https://mseep.ai/app/nloui-paperless-mcp) -->
2
+
1
3
  # Paperless-NGX MCP Server
2
4
 
3
5
  [![smithery badge](https://smithery.ai/badge/@baruchiro/paperless-mcp)](https://smithery.ai/server/@baruchiro/paperless-mcp)
@@ -6,6 +8,8 @@ An MCP (Model Context Protocol) server for interacting with a Paperless-NGX API
6
8
 
7
9
  ## Quick Start
8
10
 
11
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](https://cursor.com/install-mcp?name=paperless&config=eyJjb21tYW5kIjoibnB4IC15IEBiYXJ1Y2hpcm8vcGFwZXJsZXNzLW1jcEBsYXRlc3QiLCJlbnYiOnsiUEFQRVJMRVNTX1VSTCI6Imh0dHA6Ly95b3VyLXBhcGVybGVzcy1pbnN0YW5jZTo4MDAwIiwiUEFQRVJMRVNTX0FQSV9LRVkiOiJ5b3VyLWFwaS10b2tlbiJ9fQ%3D%3D)
12
+
9
13
  ### Installing via Smithery
10
14
 
11
15
  To install Paperless NGX MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@baruchiro/paperless-mcp):
@@ -28,7 +32,8 @@ Add these to your MCP config file:
28
32
  ],
29
33
  "env": {
30
34
  "PAPERLESS_URL": "http://your-paperless-instance:8000",
31
- "PAPERLESS_API_KEY": "your-api-token"
35
+ "PAPERLESS_API_KEY": "your-api-token",
36
+ "PAPERLESS_PUBLIC_URL": "https://your-public-domain.com"
32
37
  }
33
38
  }
34
39
  ```
@@ -45,7 +50,8 @@ Add these to your MCP config file:
45
50
  ],
46
51
  "env": {
47
52
  "PAPERLESS_URL": "http://your-paperless-instance:8000",
48
- "PAPERLESS_API_KEY": "your-api-token"
53
+ "PAPERLESS_API_KEY": "your-api-token",
54
+ "PAPERLESS_PUBLIC_URL": "https://your-public-domain.com"
49
55
  }
50
56
  }
51
57
  ```
@@ -59,6 +65,7 @@ Add these to your MCP config file:
59
65
  4. Replace the placeholders in your MCP config:
60
66
  - `http://your-paperless-instance:8000` with your Paperless-NGX URL
61
67
  - `your-api-token` with the token you just generated
68
+ - `https://your-public-domain.com` with your public Paperless-NGX URL (optional, falls back to PAPERLESS_URL)
62
69
 
63
70
  That's it! Now you can ask Claude to help you manage your Paperless-NGX documents.
64
71
 
@@ -199,6 +206,15 @@ bulk_edit_documents({
199
206
  add_tags: [1, 2],
200
207
  remove_tags: [3, 4]
201
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
+ })
202
218
  ```
203
219
 
204
220
  #### post_document
@@ -225,7 +241,8 @@ post_document({
225
241
  correspondent: 1,
226
242
  document_type: 2,
227
243
  tags: [1, 3],
228
- archive_serial_number: "2024-001"
244
+ archive_serial_number: "2024-001",
245
+ custom_fields: [1, 2]
229
246
  })
230
247
  ```
231
248
 
@@ -306,6 +323,85 @@ create_document_type({
306
323
  })
307
324
  ```
308
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
+
309
405
  ## Error Handling
310
406
 
311
407
  The server will show clear error messages if:
@@ -365,3 +461,42 @@ npm run start -- <baseUrl> <token> --http --port 3000
365
461
  # Credits
366
462
 
367
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,23 +21,26 @@ 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");
27
- const { values: { baseUrl, token, http: useHttp, port }, } = (0, node_util_1.parseArgs)({
28
+ const { values: { baseUrl, token, http: useHttp, port, publicUrl }, } = (0, node_util_1.parseArgs)({
28
29
  options: {
29
30
  baseUrl: { type: "string" },
30
31
  token: { type: "string" },
31
32
  http: { type: "boolean", default: false },
32
33
  port: { type: "string" },
34
+ publicUrl: { type: "string", default: "" },
33
35
  },
34
36
  allowPositionals: true,
35
37
  });
36
38
  const resolvedBaseUrl = baseUrl || process.env.PAPERLESS_URL;
37
39
  const resolvedToken = token || process.env.PAPERLESS_API_KEY;
40
+ const resolvedPublicUrl = publicUrl || process.env.PAPERLESS_PUBLIC_URL || resolvedBaseUrl;
38
41
  const resolvedPort = port ? parseInt(port, 10) : 3000;
39
42
  if (!resolvedBaseUrl || !resolvedToken) {
40
- console.error("Usage: paperless-mcp --baseUrl <url> --token <token> [--http] [--port <port>]");
43
+ console.error("Usage: paperless-mcp --baseUrl <url> --token <token> [--http] [--port <port>] [--publicUrl <url>]");
41
44
  console.error("Or set PAPERLESS_URL and PAPERLESS_API_KEY environment variables.");
42
45
  process.exit(1);
43
46
  }
@@ -45,11 +48,30 @@ function main() {
45
48
  return __awaiter(this, void 0, void 0, function* () {
46
49
  // Initialize API client and server once
47
50
  const api = new PaperlessAPI_1.PaperlessAPI(resolvedBaseUrl, resolvedToken);
48
- const server = new mcp_js_1.McpServer({ name: "paperless-ngx", version: "1.0.0" });
51
+ const server = new mcp_js_1.McpServer({ name: "paperless-ngx", version: "1.0.0" }, {
52
+ instructions: `
53
+ Paperless-NGX MCP Server Instructions
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
+
62
+ To view documents in your Paperless-NGX web interface, construct URLs using this pattern:
63
+ ${resolvedPublicUrl}/documents/{document_id}/
64
+
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.
66
+
67
+ The document tools return JSON data with document IDs that you can use to construct these URLs.
68
+ `,
69
+ });
49
70
  (0, documents_1.registerDocumentTools)(server, api);
50
71
  (0, tags_1.registerTagTools)(server, api);
51
72
  (0, correspondents_1.registerCorrespondentTools)(server, api);
52
73
  (0, documentTypes_1.registerDocumentTypeTools)(server, api);
74
+ (0, customFields_1.registerCustomFieldTools)(server, api);
53
75
  if (useHttp) {
54
76
  const app = (0, express_1.default)();
55
77
  app.use(express_1.default.json());
@@ -138,11 +160,11 @@ function main() {
138
160
  app.listen(resolvedPort, () => {
139
161
  console.log(`MCP Stateless Streamable HTTP Server listening on port ${resolvedPort}`);
140
162
  });
163
+ // await new Promise((resolve) => setTimeout(resolve, 1000000));
141
164
  }
142
165
  else {
143
166
  const transport = new stdio_js_1.StdioServerTransport();
144
167
  yield server.connect(transport);
145
- console.log("MCP server running with stdio transport");
146
168
  }
147
169
  });
148
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;