@baruchiro/paperless-mcp 0.4.5 → 0.5.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 +39 -3
- package/build/api/PaperlessAPI.d.ts +1 -0
- package/build/api/PaperlessAPI.js +29 -14
- package/build/api/types.d.ts +4 -4
- package/build/index.js +28 -34
- package/build/server.d.ts +11 -0
- package/build/server.js +54 -0
- package/build/tools/documents.d.ts +33 -0
- package/build/tools/documents.js +42 -7
- package/build/tools/documents.test.d.ts +1 -0
- package/build/tools/documents.test.js +78 -0
- package/package.json +1 -1
- package/paperless-mcp.dxt +0 -0
package/README.md
CHANGED
|
@@ -59,6 +59,15 @@ Add these to your MCP config file:
|
|
|
59
59
|
- `your-api-token` with the token you just generated
|
|
60
60
|
- `https://your-public-domain.com` with your public Paperless-NGX URL (optional, falls back to PAPERLESS_URL)
|
|
61
61
|
|
|
62
|
+
### Environment Variables
|
|
63
|
+
|
|
64
|
+
| Variable | Required | Default | Description |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `PAPERLESS_URL` | Yes | — | Base URL of your Paperless-NGX instance |
|
|
67
|
+
| `PAPERLESS_API_KEY` | Yes | — | API token from your Paperless-NGX profile |
|
|
68
|
+
| `PAPERLESS_PUBLIC_URL` | No | `PAPERLESS_URL` | Public-facing URL for document links |
|
|
69
|
+
| `PAPERLESS_API_VERSION` | No | `5` | Paperless-ngx REST API version. Use `10` for Paperless-ngx v3+. If you see HTTP 406 errors, set this to `10`. |
|
|
70
|
+
|
|
62
71
|
That's it! Now you can ask Claude to help you manage your Paperless-NGX documents.
|
|
63
72
|
|
|
64
73
|
### Example Usage
|
|
@@ -215,9 +224,20 @@ bulk_edit_documents({
|
|
|
215
224
|
bulk_edit_documents({
|
|
216
225
|
documents: [12, 13],
|
|
217
226
|
method: "modify_custom_fields",
|
|
218
|
-
add_custom_fields:
|
|
219
|
-
|
|
220
|
-
|
|
227
|
+
add_custom_fields: [
|
|
228
|
+
{ field: 2, value: "year" }
|
|
229
|
+
],
|
|
230
|
+
remove_custom_fields: []
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Set an empty custom field value, e.g. a date field used as a pending marker
|
|
234
|
+
bulk_edit_documents({
|
|
235
|
+
documents: [14],
|
|
236
|
+
method: "modify_custom_fields",
|
|
237
|
+
add_custom_fields: [
|
|
238
|
+
{ field: 9, value: "" }
|
|
239
|
+
],
|
|
240
|
+
remove_custom_fields: []
|
|
221
241
|
})
|
|
222
242
|
```
|
|
223
243
|
|
|
@@ -483,6 +503,22 @@ npm run start -- <baseUrl> <token> --http --port 3000
|
|
|
483
503
|
- Each request is handled statelessly, following the [StreamableHTTPServerTransport](https://github.com/modelcontextprotocol/typescript-sdk) pattern.
|
|
484
504
|
- GET and DELETE requests to `/mcp` will return 405 Method Not Allowed.
|
|
485
505
|
|
|
506
|
+
#### Per-request API token (HTTP/Docker mode)
|
|
507
|
+
|
|
508
|
+
In HTTP mode, clients can supply their own Paperless-NGX API token via the standard `Authorization` header instead of (or in addition to) the server-configured `PAPERLESS_API_KEY`. The client-supplied token takes precedence.
|
|
509
|
+
|
|
510
|
+
```
|
|
511
|
+
Authorization: Bearer <paperless-ngx-api-token>
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
| Scenario | Token used |
|
|
515
|
+
|---|---|
|
|
516
|
+
| Client sends `Authorization: Bearer <tok>` | `<tok>` (client-supplied, takes precedence) |
|
|
517
|
+
| No header, `PAPERLESS_API_KEY` env var set | `PAPERLESS_API_KEY` from env |
|
|
518
|
+
| No header, no env var | `401 Unauthorized` |
|
|
519
|
+
|
|
520
|
+
This allows a single server instance to serve multiple users, each authenticating with their own Paperless-NGX token. The same behaviour applies to both `/mcp` and `/sse` endpoints.
|
|
521
|
+
|
|
486
522
|
<details>
|
|
487
523
|
<summary>Docker Deployment</summary>
|
|
488
524
|
|
|
@@ -3,6 +3,7 @@ import { BulkEditDocumentsResult, BulkEditParameters, Correspondent, CustomField
|
|
|
3
3
|
export declare class PaperlessAPI {
|
|
4
4
|
private readonly baseUrl;
|
|
5
5
|
private readonly token;
|
|
6
|
+
private readonly apiVersion;
|
|
6
7
|
constructor(baseUrl: string, token: string);
|
|
7
8
|
request<T = any>(path: string, options?: RequestInit): Promise<T>;
|
|
8
9
|
bulkEditDocuments(documents: number[], method: string, parameters?: BulkEditParameters): Promise<BulkEditDocumentsResult>;
|
|
@@ -22,13 +22,14 @@ class PaperlessAPI {
|
|
|
22
22
|
this.token = token;
|
|
23
23
|
this.baseUrl = baseUrl;
|
|
24
24
|
this.token = token;
|
|
25
|
+
this.apiVersion = process.env.PAPERLESS_API_VERSION || "5";
|
|
25
26
|
}
|
|
26
27
|
request(path_1) {
|
|
27
28
|
return __awaiter(this, arguments, void 0, function* (path, options = {}) {
|
|
28
|
-
var _a, _b;
|
|
29
|
+
var _a, _b, _c;
|
|
29
30
|
const url = `${this.baseUrl}/api${path}`;
|
|
30
31
|
const isJson = !options.body || typeof options.body === "string";
|
|
31
|
-
const mergedHeaders = Object.assign(Object.assign({ Authorization: `Token ${this.token}`, Accept:
|
|
32
|
+
const mergedHeaders = Object.assign(Object.assign({ Authorization: `Token ${this.token}`, Accept: `application/json; version=${this.apiVersion}`, "Accept-Language": "en-US,en;q=0.9" }, (isJson ? { "Content-Type": "application/json" } : {})), (0, utils_1.headersToObject)(options.headers));
|
|
32
33
|
try {
|
|
33
34
|
const response = yield (0, axios_1.default)({
|
|
34
35
|
url,
|
|
@@ -54,13 +55,17 @@ class PaperlessAPI {
|
|
|
54
55
|
return body;
|
|
55
56
|
}
|
|
56
57
|
catch (error) {
|
|
58
|
+
if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 406) {
|
|
59
|
+
throw new Error(`HTTP 406: Paperless-ngx rejected API version ${this.apiVersion}. ` +
|
|
60
|
+
`Set the PAPERLESS_API_VERSION environment variable to match your server's API version (e.g., "10" for Paperless-ngx v3+).`);
|
|
61
|
+
}
|
|
57
62
|
console.error({
|
|
58
63
|
error: "Error executing request",
|
|
59
64
|
message: error instanceof Error ? error.message : String(error),
|
|
60
65
|
url,
|
|
61
66
|
options,
|
|
62
|
-
responseData: (
|
|
63
|
-
status: (
|
|
67
|
+
responseData: axios_1.default.isAxiosError(error) ? (_b = error.response) === null || _b === void 0 ? void 0 : _b.data : undefined,
|
|
68
|
+
status: axios_1.default.isAxiosError(error) ? (_c = error.response) === null || _c === void 0 ? void 0 : _c.status : undefined,
|
|
64
69
|
});
|
|
65
70
|
throw error;
|
|
66
71
|
}
|
|
@@ -81,6 +86,7 @@ class PaperlessAPI {
|
|
|
81
86
|
}
|
|
82
87
|
postDocument(document_1, filename_1) {
|
|
83
88
|
return __awaiter(this, arguments, void 0, function* (document, filename, metadata = {}) {
|
|
89
|
+
var _a;
|
|
84
90
|
const formData = new form_data_1.default();
|
|
85
91
|
formData.append("document", document, { filename });
|
|
86
92
|
// Add optional metadata fields
|
|
@@ -103,13 +109,22 @@ class PaperlessAPI {
|
|
|
103
109
|
if (metadata.custom_fields) {
|
|
104
110
|
metadata.custom_fields.forEach((field) => formData.append("custom_fields", String(field)));
|
|
105
111
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
try {
|
|
113
|
+
const response = yield axios_1.default.post(`${this.baseUrl}/api/documents/post_document/`, formData, {
|
|
114
|
+
headers: Object.assign({ Authorization: `Token ${this.token}`, Accept: `application/json; version=${this.apiVersion}` }, formData.getHeaders()),
|
|
115
|
+
});
|
|
116
|
+
if (response.status < 200 || response.status >= 300) {
|
|
117
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
118
|
+
}
|
|
119
|
+
return response.data;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 406) {
|
|
123
|
+
throw new Error(`HTTP 406: Paperless-ngx rejected API version ${this.apiVersion}. ` +
|
|
124
|
+
`Set the PAPERLESS_API_VERSION environment variable to match your server's API version (e.g., "10" for Paperless-ngx v3+).`);
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
111
127
|
}
|
|
112
|
-
return response.data;
|
|
113
128
|
});
|
|
114
129
|
}
|
|
115
130
|
getDocuments() {
|
|
@@ -176,7 +191,7 @@ class PaperlessAPI {
|
|
|
176
191
|
updateTag(id, data) {
|
|
177
192
|
return __awaiter(this, void 0, void 0, function* () {
|
|
178
193
|
return this.request(`/tags/${id}/`, {
|
|
179
|
-
method: "
|
|
194
|
+
method: "PATCH",
|
|
180
195
|
body: JSON.stringify(data),
|
|
181
196
|
});
|
|
182
197
|
});
|
|
@@ -213,7 +228,7 @@ class PaperlessAPI {
|
|
|
213
228
|
updateCorrespondent(id, data) {
|
|
214
229
|
return __awaiter(this, void 0, void 0, function* () {
|
|
215
230
|
return this.request(`/correspondents/${id}/`, {
|
|
216
|
-
method: "
|
|
231
|
+
method: "PATCH",
|
|
217
232
|
body: JSON.stringify(data),
|
|
218
233
|
});
|
|
219
234
|
});
|
|
@@ -242,7 +257,7 @@ class PaperlessAPI {
|
|
|
242
257
|
updateDocumentType(id, data) {
|
|
243
258
|
return __awaiter(this, void 0, void 0, function* () {
|
|
244
259
|
return this.request(`/document_types/${id}/`, {
|
|
245
|
-
method: "
|
|
260
|
+
method: "PATCH",
|
|
246
261
|
body: JSON.stringify(data),
|
|
247
262
|
});
|
|
248
263
|
});
|
|
@@ -276,7 +291,7 @@ class PaperlessAPI {
|
|
|
276
291
|
updateCustomField(id, data) {
|
|
277
292
|
return __awaiter(this, void 0, void 0, function* () {
|
|
278
293
|
return this.request(`/custom_fields/${id}/`, {
|
|
279
|
-
method: "
|
|
294
|
+
method: "PATCH",
|
|
280
295
|
body: JSON.stringify(data),
|
|
281
296
|
});
|
|
282
297
|
});
|
package/build/api/types.d.ts
CHANGED
|
@@ -30,13 +30,14 @@ export interface CustomField {
|
|
|
30
30
|
extra_data?: Record<string, unknown> | null;
|
|
31
31
|
document_count: number;
|
|
32
32
|
}
|
|
33
|
+
export type CustomFieldValue = string | number | boolean | number[] | null;
|
|
33
34
|
export interface CustomFieldInstance {
|
|
34
35
|
field: number;
|
|
35
|
-
value:
|
|
36
|
+
value: CustomFieldValue;
|
|
36
37
|
}
|
|
37
38
|
export interface CustomFieldInstanceRequest {
|
|
38
39
|
field: number;
|
|
39
|
-
value:
|
|
40
|
+
value: CustomFieldValue;
|
|
40
41
|
}
|
|
41
42
|
export interface PaginationResponse<T> {
|
|
42
43
|
count: number;
|
|
@@ -128,8 +129,7 @@ export interface BulkEditDocumentsResult {
|
|
|
128
129
|
result: string;
|
|
129
130
|
}
|
|
130
131
|
export interface BulkEditParameters {
|
|
131
|
-
|
|
132
|
-
assign_custom_fields_values?: CustomFieldInstanceRequest[];
|
|
132
|
+
add_custom_fields?: Record<string, CustomFieldInstanceRequest["value"]>;
|
|
133
133
|
remove_custom_fields?: number[];
|
|
134
134
|
add_tags?: number[];
|
|
135
135
|
remove_tags?: number[];
|
package/build/index.js
CHANGED
|
@@ -13,18 +13,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
14
|
};
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
17
16
|
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
18
17
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
19
18
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
20
19
|
const express_1 = __importDefault(require("express"));
|
|
21
20
|
const node_util_1 = require("node:util");
|
|
22
|
-
const
|
|
23
|
-
const correspondents_1 = require("./tools/correspondents");
|
|
24
|
-
const customFields_1 = require("./tools/customFields");
|
|
25
|
-
const documents_1 = require("./tools/documents");
|
|
26
|
-
const documentTypes_1 = require("./tools/documentTypes");
|
|
27
|
-
const tags_1 = require("./tools/tags");
|
|
21
|
+
const server_1 = require("./server");
|
|
28
22
|
const { version } = require("../package.json");
|
|
29
23
|
const { values: { baseUrl, token, http: useHttp, port, publicUrl }, } = (0, node_util_1.parseArgs)({
|
|
30
24
|
options: {
|
|
@@ -40,46 +34,39 @@ const resolvedBaseUrl = baseUrl || process.env.PAPERLESS_URL;
|
|
|
40
34
|
const resolvedToken = token || process.env.PAPERLESS_API_KEY;
|
|
41
35
|
const resolvedPublicUrl = publicUrl || process.env.PAPERLESS_PUBLIC_URL || resolvedBaseUrl;
|
|
42
36
|
const resolvedPort = port ? parseInt(port, 10) : 3000;
|
|
43
|
-
if (!resolvedBaseUrl
|
|
37
|
+
if (!resolvedBaseUrl) {
|
|
44
38
|
console.error("Usage: paperless-mcp --baseUrl <url> --token <token> [--http] [--port <port>] [--publicUrl <url>]");
|
|
45
39
|
console.error("Or set PAPERLESS_URL and PAPERLESS_API_KEY environment variables.");
|
|
46
40
|
process.exit(1);
|
|
47
41
|
}
|
|
42
|
+
if (!useHttp && !resolvedToken) {
|
|
43
|
+
console.error("Usage: paperless-mcp --baseUrl <url> --token <token> [--http] [--port <port>] [--publicUrl <url>]");
|
|
44
|
+
console.error("Or set PAPERLESS_URL and PAPERLESS_API_KEY environment variables.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
function buildServer(requestToken) {
|
|
48
|
+
return (0, server_1.createMcpServer)({
|
|
49
|
+
baseUrl: resolvedBaseUrl,
|
|
50
|
+
token: requestToken,
|
|
51
|
+
version,
|
|
52
|
+
publicUrl: resolvedPublicUrl,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
48
55
|
function main() {
|
|
49
56
|
return __awaiter(this, void 0, void 0, function* () {
|
|
50
|
-
// Initialize API client and server once
|
|
51
|
-
const api = new PaperlessAPI_1.PaperlessAPI(resolvedBaseUrl, resolvedToken);
|
|
52
|
-
const server = new mcp_js_1.McpServer({ name: "paperless-ngx", version }, {
|
|
53
|
-
instructions: `
|
|
54
|
-
Paperless-NGX MCP Server Instructions
|
|
55
|
-
|
|
56
|
-
⚠️ CRITICAL: Always differentiate between operations on specific documents vs operations on the entire system:
|
|
57
|
-
|
|
58
|
-
- REMOVE operations (e.g., remove_tag in bulk_edit_documents): Affect only the specified documents, items remain in the system
|
|
59
|
-
- DELETE operations (e.g., delete_tag, delete_correspondent): Permanently delete items from the entire system, affecting ALL documents that use them
|
|
60
|
-
|
|
61
|
-
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.
|
|
62
|
-
|
|
63
|
-
To view documents in your Paperless-NGX web interface, construct URLs using this pattern:
|
|
64
|
-
${resolvedPublicUrl}/documents/{document_id}/
|
|
65
|
-
|
|
66
|
-
Example: If your base URL is "http://localhost:8000", the web interface URL would be "http://localhost:8000/documents/123/" for document ID 123.
|
|
67
|
-
|
|
68
|
-
The document tools return JSON data with document IDs that you can use to construct these URLs.
|
|
69
|
-
`,
|
|
70
|
-
});
|
|
71
|
-
(0, documents_1.registerDocumentTools)(server, api);
|
|
72
|
-
(0, tags_1.registerTagTools)(server, api);
|
|
73
|
-
(0, correspondents_1.registerCorrespondentTools)(server, api);
|
|
74
|
-
(0, documentTypes_1.registerDocumentTypeTools)(server, api);
|
|
75
|
-
(0, customFields_1.registerCustomFieldTools)(server, api);
|
|
76
57
|
if (useHttp) {
|
|
77
58
|
const app = (0, express_1.default)();
|
|
78
59
|
app.use(express_1.default.json());
|
|
79
60
|
// Store transports for each session
|
|
80
61
|
const sseTransports = {};
|
|
81
62
|
app.post("/mcp", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
const requestToken = (0, server_1.getBearerToken)(req, resolvedToken);
|
|
64
|
+
if (!requestToken) {
|
|
65
|
+
(0, server_1.sendUnauthorized)(res);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
82
68
|
try {
|
|
69
|
+
const server = buildServer(requestToken);
|
|
83
70
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
84
71
|
sessionIdGenerator: undefined,
|
|
85
72
|
});
|
|
@@ -125,7 +112,13 @@ The document tools return JSON data with document IDs that you can use to constr
|
|
|
125
112
|
}));
|
|
126
113
|
app.get("/sse", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
127
114
|
console.log("SSE request received");
|
|
115
|
+
const requestToken = (0, server_1.getBearerToken)(req, resolvedToken);
|
|
116
|
+
if (!requestToken) {
|
|
117
|
+
(0, server_1.sendUnauthorized)(res);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
128
120
|
try {
|
|
121
|
+
const server = buildServer(requestToken);
|
|
129
122
|
const transport = new sse_js_1.SSEServerTransport("/messages", res);
|
|
130
123
|
sseTransports[transport.sessionId] = transport;
|
|
131
124
|
res.on("close", () => {
|
|
@@ -164,6 +157,7 @@ The document tools return JSON data with document IDs that you can use to constr
|
|
|
164
157
|
// await new Promise((resolve) => setTimeout(resolve, 1000000));
|
|
165
158
|
}
|
|
166
159
|
else {
|
|
160
|
+
const server = buildServer(resolvedToken);
|
|
167
161
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
168
162
|
yield server.connect(transport);
|
|
169
163
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type express from "express";
|
|
3
|
+
export interface CreateMcpServerOptions {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
token: string;
|
|
6
|
+
version: string;
|
|
7
|
+
publicUrl: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function createMcpServer({ baseUrl, token, version, publicUrl, }: CreateMcpServerOptions): McpServer;
|
|
10
|
+
export declare function getBearerToken(req: express.Request, fallbackToken?: string): string | undefined;
|
|
11
|
+
export declare function sendUnauthorized(res: express.Response): void;
|
package/build/server.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMcpServer = createMcpServer;
|
|
4
|
+
exports.getBearerToken = getBearerToken;
|
|
5
|
+
exports.sendUnauthorized = sendUnauthorized;
|
|
6
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7
|
+
const PaperlessAPI_1 = require("./api/PaperlessAPI");
|
|
8
|
+
const correspondents_1 = require("./tools/correspondents");
|
|
9
|
+
const customFields_1 = require("./tools/customFields");
|
|
10
|
+
const documents_1 = require("./tools/documents");
|
|
11
|
+
const documentTypes_1 = require("./tools/documentTypes");
|
|
12
|
+
const tags_1 = require("./tools/tags");
|
|
13
|
+
function createMcpServer({ baseUrl, token, version, publicUrl, }) {
|
|
14
|
+
const api = new PaperlessAPI_1.PaperlessAPI(baseUrl, token);
|
|
15
|
+
const server = new mcp_js_1.McpServer({ name: "paperless-ngx", version }, { instructions: buildInstructions(publicUrl) });
|
|
16
|
+
(0, documents_1.registerDocumentTools)(server, api);
|
|
17
|
+
(0, tags_1.registerTagTools)(server, api);
|
|
18
|
+
(0, correspondents_1.registerCorrespondentTools)(server, api);
|
|
19
|
+
(0, documentTypes_1.registerDocumentTypeTools)(server, api);
|
|
20
|
+
(0, customFields_1.registerCustomFieldTools)(server, api);
|
|
21
|
+
return server;
|
|
22
|
+
}
|
|
23
|
+
function getBearerToken(req, fallbackToken) {
|
|
24
|
+
const authHeader = req.headers["authorization"];
|
|
25
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
26
|
+
return authHeader.slice(7);
|
|
27
|
+
}
|
|
28
|
+
return fallbackToken || undefined;
|
|
29
|
+
}
|
|
30
|
+
function sendUnauthorized(res) {
|
|
31
|
+
res
|
|
32
|
+
.status(401)
|
|
33
|
+
.set("WWW-Authenticate", 'Bearer realm="paperless-mcp"')
|
|
34
|
+
.end();
|
|
35
|
+
}
|
|
36
|
+
function buildInstructions(publicUrl) {
|
|
37
|
+
return `
|
|
38
|
+
Paperless-NGX MCP Server Instructions
|
|
39
|
+
|
|
40
|
+
⚠️ CRITICAL: Always differentiate between operations on specific documents vs operations on the entire system:
|
|
41
|
+
|
|
42
|
+
- REMOVE operations (e.g., remove_tag in bulk_edit_documents): Affect only the specified documents, items remain in the system
|
|
43
|
+
- DELETE operations (e.g., delete_tag, delete_correspondent): Permanently delete items from the entire system, affecting ALL documents that use them
|
|
44
|
+
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
To view documents in your Paperless-NGX web interface, construct URLs using this pattern:
|
|
48
|
+
${publicUrl}/documents/{document_id}/
|
|
49
|
+
|
|
50
|
+
Example: If your base URL is "http://localhost:8000", the web interface URL would be "http://localhost:8000/documents/123/" for document ID 123.
|
|
51
|
+
|
|
52
|
+
The document tools return JSON data with document IDs that you can use to construct these URLs.
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
@@ -1,3 +1,36 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
|
|
2
2
|
import { PaperlessAPI } from "../api/PaperlessAPI";
|
|
3
|
+
export type BulkCustomFieldValue = string | number | boolean | number[] | null;
|
|
4
|
+
export type BulkCustomFieldUpdate = {
|
|
5
|
+
field: number;
|
|
6
|
+
value: BulkCustomFieldValue;
|
|
7
|
+
};
|
|
8
|
+
export type BulkCustomFieldParameters = {
|
|
9
|
+
add_custom_fields?: Record<string, BulkCustomFieldValue>;
|
|
10
|
+
remove_custom_fields?: number[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Builds Paperless-NGX bulk edit parameters from base parameters plus optional
|
|
14
|
+
* custom field updates.
|
|
15
|
+
*
|
|
16
|
+
* Paperless-NGX expects custom field bulk updates as an `add_custom_fields`
|
|
17
|
+
* record keyed by custom field id. `addCustomFields` is accepted as an array for
|
|
18
|
+
* the MCP tool schema and transformed into that id-to-value record while
|
|
19
|
+
* preserving supported value types, including `number[]` document links and
|
|
20
|
+
* `null` resets. Passing an empty `addCustomFields` array intentionally produces
|
|
21
|
+
* an empty `add_custom_fields` record.
|
|
22
|
+
*
|
|
23
|
+
* When `includeCustomFieldDefaults` is true, the function also initializes
|
|
24
|
+
* `add_custom_fields` and `remove_custom_fields` with empty defaults using
|
|
25
|
+
* nullish coalescing (`??=`). This keeps the `modify_custom_fields` method's
|
|
26
|
+
* payload shape acceptable to Paperless even when no field values are supplied.
|
|
27
|
+
*
|
|
28
|
+
* @param parameters - Base bulk edit parameters to include in the result.
|
|
29
|
+
* @param addCustomFields - Optional custom field updates to map by field id.
|
|
30
|
+
* @param includeCustomFieldDefaults - Whether to include empty custom field
|
|
31
|
+
* defaults required by `modify_custom_fields`.
|
|
32
|
+
* @returns The merged API parameters with custom field updates transformed into
|
|
33
|
+
* Paperless-NGX's `add_custom_fields` record shape.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildBulkEditParameters<T extends Record<string, unknown>>(parameters: T, addCustomFields?: BulkCustomFieldUpdate[], includeCustomFieldDefaults?: boolean): T & BulkCustomFieldParameters;
|
|
3
36
|
export declare function registerDocumentTools(server: McpServer, api: PaperlessAPI): void;
|
package/build/tools/documents.js
CHANGED
|
@@ -20,6 +20,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
20
20
|
return t;
|
|
21
21
|
};
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.buildBulkEditParameters = buildBulkEditParameters;
|
|
23
24
|
exports.registerDocumentTools = registerDocumentTools;
|
|
24
25
|
const zod_1 = require("zod");
|
|
25
26
|
const documentEnhancer_1 = require("../api/documentEnhancer");
|
|
@@ -27,6 +28,44 @@ const empty_1 = require("./utils/empty");
|
|
|
27
28
|
const middlewares_1 = require("./utils/middlewares");
|
|
28
29
|
const monetary_1 = require("./utils/monetary");
|
|
29
30
|
const descriptions_1 = require("./utils/descriptions");
|
|
31
|
+
/**
|
|
32
|
+
* Builds Paperless-NGX bulk edit parameters from base parameters plus optional
|
|
33
|
+
* custom field updates.
|
|
34
|
+
*
|
|
35
|
+
* Paperless-NGX expects custom field bulk updates as an `add_custom_fields`
|
|
36
|
+
* record keyed by custom field id. `addCustomFields` is accepted as an array for
|
|
37
|
+
* the MCP tool schema and transformed into that id-to-value record while
|
|
38
|
+
* preserving supported value types, including `number[]` document links and
|
|
39
|
+
* `null` resets. Passing an empty `addCustomFields` array intentionally produces
|
|
40
|
+
* an empty `add_custom_fields` record.
|
|
41
|
+
*
|
|
42
|
+
* When `includeCustomFieldDefaults` is true, the function also initializes
|
|
43
|
+
* `add_custom_fields` and `remove_custom_fields` with empty defaults using
|
|
44
|
+
* nullish coalescing (`??=`). This keeps the `modify_custom_fields` method's
|
|
45
|
+
* payload shape acceptable to Paperless even when no field values are supplied.
|
|
46
|
+
*
|
|
47
|
+
* @param parameters - Base bulk edit parameters to include in the result.
|
|
48
|
+
* @param addCustomFields - Optional custom field updates to map by field id.
|
|
49
|
+
* @param includeCustomFieldDefaults - Whether to include empty custom field
|
|
50
|
+
* defaults required by `modify_custom_fields`.
|
|
51
|
+
* @returns The merged API parameters with custom field updates transformed into
|
|
52
|
+
* Paperless-NGX's `add_custom_fields` record shape.
|
|
53
|
+
*/
|
|
54
|
+
function buildBulkEditParameters(parameters, addCustomFields, includeCustomFieldDefaults = false) {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
const apiParameters = Object.assign({}, parameters);
|
|
57
|
+
if (addCustomFields) {
|
|
58
|
+
apiParameters.add_custom_fields = Object.fromEntries(addCustomFields.map((customField) => [
|
|
59
|
+
String(customField.field),
|
|
60
|
+
customField.value,
|
|
61
|
+
]));
|
|
62
|
+
}
|
|
63
|
+
if (includeCustomFieldDefaults) {
|
|
64
|
+
(_a = apiParameters.add_custom_fields) !== null && _a !== void 0 ? _a : (apiParameters.add_custom_fields = {});
|
|
65
|
+
(_b = apiParameters.remove_custom_fields) !== null && _b !== void 0 ? _b : (apiParameters.remove_custom_fields = []);
|
|
66
|
+
}
|
|
67
|
+
return apiParameters;
|
|
68
|
+
}
|
|
30
69
|
function registerDocumentTools(server, api) {
|
|
31
70
|
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.", {
|
|
32
71
|
documents: zod_1.z.array(zod_1.z.number()),
|
|
@@ -104,13 +143,9 @@ function registerDocumentTools(server, api) {
|
|
|
104
143
|
}
|
|
105
144
|
const { documents, method, add_custom_fields, confirm } = args, parameters = __rest(args, ["documents", "method", "add_custom_fields", "confirm"]);
|
|
106
145
|
(0, monetary_1.validateCustomFields)(add_custom_fields);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
apiParameters.assign_custom_fields = add_custom_fields.map((cf) => cf.field);
|
|
111
|
-
apiParameters.assign_custom_fields_values = add_custom_fields;
|
|
112
|
-
}
|
|
113
|
-
const response = yield api.bulkEditDocuments(documents, method, apiParameters);
|
|
146
|
+
const response = yield api.bulkEditDocuments(documents, method, method === "delete"
|
|
147
|
+
? {}
|
|
148
|
+
: buildBulkEditParameters(parameters, add_custom_fields, method === "modify_custom_fields"));
|
|
114
149
|
return {
|
|
115
150
|
content: [
|
|
116
151
|
{
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = require("node:test");
|
|
8
|
+
const documents_1 = require("./documents");
|
|
9
|
+
(0, node_test_1.test)("buildBulkEditParameters sends Paperless bulk custom fields as id:value map", () => {
|
|
10
|
+
const parameters = (0, documents_1.buildBulkEditParameters)({ remove_custom_fields: [] }, [
|
|
11
|
+
{ field: 9, value: "" },
|
|
12
|
+
{ field: 10, value: "2026-05-14" },
|
|
13
|
+
]);
|
|
14
|
+
strict_1.default.deepEqual(parameters, {
|
|
15
|
+
remove_custom_fields: [],
|
|
16
|
+
add_custom_fields: {
|
|
17
|
+
"9": "",
|
|
18
|
+
"10": "2026-05-14",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
strict_1.default.ok(!("assign_custom_fields" in parameters));
|
|
22
|
+
strict_1.default.ok(!("assign_custom_fields_values" in parameters));
|
|
23
|
+
});
|
|
24
|
+
(0, node_test_1.test)("buildBulkEditParameters preserves null custom field values", () => {
|
|
25
|
+
const parameters = (0, documents_1.buildBulkEditParameters)({}, [{ field: 9, value: null }]);
|
|
26
|
+
strict_1.default.deepEqual(parameters, {
|
|
27
|
+
add_custom_fields: {
|
|
28
|
+
"9": null,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
(0, node_test_1.test)("buildBulkEditParameters includes Paperless-required empty custom field keys", () => {
|
|
33
|
+
const parameters = (0, documents_1.buildBulkEditParameters)({}, undefined, true);
|
|
34
|
+
strict_1.default.deepEqual(parameters, {
|
|
35
|
+
add_custom_fields: {},
|
|
36
|
+
remove_custom_fields: [],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
(0, node_test_1.test)("buildBulkEditParameters preserves an empty custom fields array", () => {
|
|
40
|
+
const parameters = (0, documents_1.buildBulkEditParameters)({}, []);
|
|
41
|
+
strict_1.default.deepEqual(parameters, {
|
|
42
|
+
add_custom_fields: {},
|
|
43
|
+
});
|
|
44
|
+
strict_1.default.ok(!("remove_custom_fields" in parameters));
|
|
45
|
+
});
|
|
46
|
+
(0, node_test_1.test)("buildBulkEditParameters preserves an empty custom fields array with defaults", () => {
|
|
47
|
+
const parameters = (0, documents_1.buildBulkEditParameters)({}, [], true);
|
|
48
|
+
strict_1.default.deepEqual(parameters, {
|
|
49
|
+
add_custom_fields: {},
|
|
50
|
+
remove_custom_fields: [],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
(0, node_test_1.test)("buildBulkEditParameters combines base parameters with custom fields", () => {
|
|
54
|
+
const parameters = (0, documents_1.buildBulkEditParameters)({ add_tags: [3], remove_tags: [1, 2] }, [{ field: 9, value: "pending" }]);
|
|
55
|
+
strict_1.default.deepEqual(parameters, {
|
|
56
|
+
add_tags: [3],
|
|
57
|
+
remove_tags: [1, 2],
|
|
58
|
+
add_custom_fields: {
|
|
59
|
+
"9": "pending",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
(0, node_test_1.test)("buildBulkEditParameters preserves supported custom field value types", () => {
|
|
64
|
+
const parameters = (0, documents_1.buildBulkEditParameters)({}, [
|
|
65
|
+
{ field: 1, value: 42 },
|
|
66
|
+
{ field: 2, value: true },
|
|
67
|
+
{ field: 3, value: "" },
|
|
68
|
+
{ field: 4, value: null },
|
|
69
|
+
{ field: 5, value: [123, 456] },
|
|
70
|
+
]);
|
|
71
|
+
strict_1.default.deepEqual(parameters.add_custom_fields, {
|
|
72
|
+
"1": 42,
|
|
73
|
+
"2": true,
|
|
74
|
+
"3": "",
|
|
75
|
+
"4": null,
|
|
76
|
+
"5": [123, 456],
|
|
77
|
+
});
|
|
78
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baruchiro/paperless-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|
package/paperless-mcp.dxt
CHANGED
|
Binary file
|