@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 +138 -3
- package/build/api/PaperlessAPI.d.ts +8 -2
- package/build/api/PaperlessAPI.js +50 -1
- package/build/api/documentEnhancer.d.ts +21 -0
- package/build/api/documentEnhancer.js +88 -0
- package/build/api/types.d.ts +47 -1
- package/build/index.js +26 -4
- package/build/tools/correspondents.js +22 -7
- package/build/tools/customFields.d.ts +3 -0
- package/build/tools/customFields.js +142 -0
- package/build/tools/documentTypes.js +22 -7
- package/build/tools/documents.js +117 -71
- package/build/tools/tags.js +15 -7
- package/build/tools/utils/empty.d.ts +2 -0
- package/build/tools/utils/empty.js +7 -0
- package/build/tools/utils/middlewares.d.ts +1 -1
- package/build/tools/utils/middlewares.js +12 -13
- package/package.json +6 -3
- package/paperless-mcp.dxt +0 -0
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<!-- [](https://mseep.ai/app/nloui-paperless-mcp) -->
|
|
2
|
+
|
|
1
3
|
# Paperless-NGX MCP Server
|
|
2
4
|
|
|
3
5
|
[](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
|
+
[](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?:
|
|
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
|
-
|
|
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
|
+
}
|
package/build/api/types.d.ts
CHANGED
|
@@ -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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
|
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.
|
|
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,
|