@baruchiro/paperless-mcp 0.2.2 → 0.4.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 +64 -10
- package/build/api/PaperlessAPI.d.ts +3 -1
- package/build/api/PaperlessAPI.js +11 -0
- package/build/api/documentEnhancer.js +17 -1
- package/build/tools/documents.js +50 -41
- package/package.json +5 -2
- package/paperless-mcp.dxt +0 -0
package/README.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# Paperless-NGX MCP Server
|
|
4
4
|
|
|
5
|
-
[](https://smithery.ai/server/@baruchiro/paperless-mcp)
|
|
6
5
|

|
|
7
6
|
|
|
8
7
|
An MCP (Model Context Protocol) server for interacting with a Paperless-NGX API server. This server provides tools for managing documents, tags, correspondents, and document types in your Paperless-NGX instance.
|
|
@@ -11,15 +10,7 @@ An MCP (Model Context Protocol) server for interacting with a Paperless-NGX API
|
|
|
11
10
|
|
|
12
11
|
[](https://cursor.com/install-mcp?name=paperless&config=eyJjb21tYW5kIjoibnB4IC15IEBiYXJ1Y2hpcm8vcGFwZXJsZXNzLW1jcEBsYXRlc3QiLCJlbnYiOnsiUEFQRVJMRVNTX1VSTCI6Imh0dHA6Ly95b3VyLXBhcGVybGVzcy1pbnN0YW5jZTo4MDAwIiwiUEFQRVJMRVNTX0FQSV9LRVkiOiJ5b3VyLWFwaS10b2tlbiJ9fQ%3D%3D)
|
|
13
12
|
|
|
14
|
-
###
|
|
15
|
-
|
|
16
|
-
To install Paperless NGX MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@baruchiro/paperless-mcp):
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npx -y @smithery/cli install @baruchiro/paperless-mcp --client claude
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### Manual Installation
|
|
13
|
+
### Installation
|
|
23
14
|
|
|
24
15
|
Add these to your MCP config file:
|
|
25
16
|
|
|
@@ -137,6 +128,18 @@ download_document({
|
|
|
137
128
|
})
|
|
138
129
|
```
|
|
139
130
|
|
|
131
|
+
#### get_document_thumbnail
|
|
132
|
+
Get a document thumbnail (image preview) by ID. Returns the thumbnail as a base64-encoded WebP image resource.
|
|
133
|
+
|
|
134
|
+
Parameters:
|
|
135
|
+
- id: Document ID
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
get_document_thumbnail({
|
|
139
|
+
id: 123
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
140
143
|
#### bulk_edit_documents
|
|
141
144
|
Perform bulk operations on multiple documents.
|
|
142
145
|
|
|
@@ -480,6 +483,57 @@ npm run start -- <baseUrl> <token> --http --port 3000
|
|
|
480
483
|
- Each request is handled statelessly, following the [StreamableHTTPServerTransport](https://github.com/modelcontextprotocol/typescript-sdk) pattern.
|
|
481
484
|
- GET and DELETE requests to `/mcp` will return 405 Method Not Allowed.
|
|
482
485
|
|
|
486
|
+
<details>
|
|
487
|
+
<summary>Docker Deployment</summary>
|
|
488
|
+
|
|
489
|
+
The MCP server can be deployed using Docker and Docker Compose. The Docker image automatically runs in HTTP mode with SSE (Server-Sent Events) support on port 3000.
|
|
490
|
+
|
|
491
|
+
### Docker Compose Configuration
|
|
492
|
+
|
|
493
|
+
Create a `docker-compose.yml` file:
|
|
494
|
+
|
|
495
|
+
```yaml
|
|
496
|
+
services:
|
|
497
|
+
paperless-mcp:
|
|
498
|
+
container_name: paperless-mcp
|
|
499
|
+
image: ghcr.io/baruchiro/paperless-mcp:latest
|
|
500
|
+
environment:
|
|
501
|
+
- PAPERLESS_URL=http://your-paperless-ngx-server:8000
|
|
502
|
+
- PAPERLESS_API_KEY=your-paperless-api-key
|
|
503
|
+
- PAPERLESS_PUBLIC_URL=https://paperless-ngx.yourpublicurl.com
|
|
504
|
+
ports:
|
|
505
|
+
- "3000:3000"
|
|
506
|
+
restart: unless-stopped
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Then run:
|
|
510
|
+
```bash
|
|
511
|
+
docker-compose up -d
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Using with Continue VS Code Extension
|
|
515
|
+
|
|
516
|
+
If you're using the [Continue VS Code extension](https://continue.dev/), you can configure it to use the Dockerized MCP server via SSE.
|
|
517
|
+
|
|
518
|
+
Create or edit `.continue/mcpServers/paperless-mcp.yaml` at your workspace root:
|
|
519
|
+
|
|
520
|
+
```yaml
|
|
521
|
+
name: Paperless
|
|
522
|
+
version: 0.0.1
|
|
523
|
+
schema: v1
|
|
524
|
+
mcpServers:
|
|
525
|
+
- name: Paperless
|
|
526
|
+
type: sse
|
|
527
|
+
url: http://localhost:3000/sse
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
**Notes:**
|
|
531
|
+
- Replace `localhost` with your Docker host's IP address or hostname if running on a remote server
|
|
532
|
+
- The Docker container handles authentication via environment variables, so no credentials are needed in the Continue config
|
|
533
|
+
- The SSE endpoint is available at `/sse` on the configured port (default: 3000)
|
|
534
|
+
|
|
535
|
+
</details>
|
|
536
|
+
|
|
483
537
|
# Credits
|
|
484
538
|
|
|
485
539
|
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.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AxiosResponse } from "axios";
|
|
1
2
|
import { BulkEditDocumentsResult, BulkEditParameters, Correspondent, CustomField, Document, DocumentsResponse, DocumentType, GetCorrespondentsResponse, GetCustomFieldsResponse, GetDocumentTypesResponse, GetTagsResponse, Tag } from "./types";
|
|
2
3
|
export declare class PaperlessAPI {
|
|
3
4
|
private readonly baseUrl;
|
|
@@ -10,7 +11,8 @@ export declare class PaperlessAPI {
|
|
|
10
11
|
getDocument(id: number): Promise<Document>;
|
|
11
12
|
updateDocument(id: number, data: Partial<Document>): Promise<Document>;
|
|
12
13
|
searchDocuments(query: string): Promise<DocumentsResponse>;
|
|
13
|
-
downloadDocument(id: number, asOriginal?: boolean): Promise<
|
|
14
|
+
downloadDocument(id: number, asOriginal?: boolean): Promise<AxiosResponse<ArrayBuffer>>;
|
|
15
|
+
getThumbnail(id: number): Promise<AxiosResponse<ArrayBuffer>>;
|
|
14
16
|
getTags(): Promise<GetTagsResponse>;
|
|
15
17
|
createTag(data: Partial<Tag>): Promise<Tag>;
|
|
16
18
|
updateTag(id: number, data: Partial<Tag>): Promise<Tag>;
|
|
@@ -148,6 +148,17 @@ class PaperlessAPI {
|
|
|
148
148
|
return response;
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
|
+
getThumbnail(id) {
|
|
152
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
153
|
+
const response = yield axios_1.default.get(`${this.baseUrl}/api/documents/${id}/thumb/`, {
|
|
154
|
+
headers: {
|
|
155
|
+
Authorization: `Token ${this.token}`,
|
|
156
|
+
},
|
|
157
|
+
responseType: "arraybuffer",
|
|
158
|
+
});
|
|
159
|
+
return response;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
151
162
|
// Tag operations
|
|
152
163
|
getTags() {
|
|
153
164
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -8,6 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
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
|
+
};
|
|
11
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
23
|
exports.convertDocsWithNames = convertDocsWithNames;
|
|
13
24
|
function convertDocsWithNames(input, api) {
|
|
@@ -61,7 +72,12 @@ function enhanceDocumentsArray(documents, api) {
|
|
|
61
72
|
const documentTypeMap = new Map((documentTypes.results || []).map((dt) => [dt.id, dt.name]));
|
|
62
73
|
const tagMap = new Map((tags.results || []).map((tag) => [tag.id, tag.name]));
|
|
63
74
|
const customFieldMap = new Map((customFields.results || []).map((cf) => [cf.id, cf.name]));
|
|
64
|
-
return documents
|
|
75
|
+
return documents
|
|
76
|
+
.map((doc) => {
|
|
77
|
+
const { content } = doc, docWithoutContent = __rest(doc, ["content"]);
|
|
78
|
+
return docWithoutContent;
|
|
79
|
+
})
|
|
80
|
+
.map((doc) => (Object.assign(Object.assign({}, doc), { correspondent: doc.correspondent
|
|
65
81
|
? {
|
|
66
82
|
id: doc.correspondent,
|
|
67
83
|
name: correspondentMap.get(doc.correspondent) ||
|
package/build/tools/documents.js
CHANGED
|
@@ -53,7 +53,13 @@ function registerDocumentTools(server, api) {
|
|
|
53
53
|
add_custom_fields: zod_1.z
|
|
54
54
|
.array(zod_1.z.object({
|
|
55
55
|
field: zod_1.z.number(),
|
|
56
|
-
value: zod_1.z.union([
|
|
56
|
+
value: zod_1.z.union([
|
|
57
|
+
zod_1.z.string(),
|
|
58
|
+
zod_1.z.number(),
|
|
59
|
+
zod_1.z.boolean(),
|
|
60
|
+
zod_1.z.array(zod_1.z.number()),
|
|
61
|
+
zod_1.z.null(),
|
|
62
|
+
]),
|
|
57
63
|
}))
|
|
58
64
|
.optional()
|
|
59
65
|
.transform(empty_1.arrayNotEmpty),
|
|
@@ -146,7 +152,7 @@ function registerDocumentTools(server, api) {
|
|
|
146
152
|
],
|
|
147
153
|
};
|
|
148
154
|
})));
|
|
149
|
-
server.tool("list_documents", "List and filter documents by fields such as title, correspondent, document type, tag, storage path, creation date, and more. IMPORTANT: For queries like 'the last 3 contributions' or when searching by tag, correspondent, document type, or storage path, you should FIRST use the relevant tool (e.g., 'list_tags', 'list_correspondents', 'list_document_types', 'list_storage_paths') to find the correct ID, and then use that ID as a filter here. Only use the 'search' argument for free-text search when no specific field applies. Using the correct ID filter will yield much more accurate results.", {
|
|
155
|
+
server.tool("list_documents", "List and filter documents by fields such as title, correspondent, document type, tag, storage path, creation date, and more. IMPORTANT: For queries like 'the last 3 contributions' or when searching by tag, correspondent, document type, or storage path, you should FIRST use the relevant tool (e.g., 'list_tags', 'list_correspondents', 'list_document_types', 'list_storage_paths') to find the correct ID, and then use that ID as a filter here. Only use the 'search' argument for free-text search when no specific field applies. Using the correct ID filter will yield much more accurate results. Note: Document content is excluded from results by default. Use 'get_document_content' to retrieve content when needed.", {
|
|
150
156
|
page: zod_1.z.number().optional(),
|
|
151
157
|
page_size: zod_1.z.number().optional(),
|
|
152
158
|
search: zod_1.z.string().optional(),
|
|
@@ -184,56 +190,34 @@ function registerDocumentTools(server, api) {
|
|
|
184
190
|
const docsResponse = yield api.getDocuments(query.toString() ? `?${query.toString()}` : "");
|
|
185
191
|
return (0, documentEnhancer_1.convertDocsWithNames)(docsResponse, api);
|
|
186
192
|
})));
|
|
187
|
-
server.tool("get_document", "Get a specific document by ID with full details including correspondent, document type, tags, and custom fields.", {
|
|
193
|
+
server.tool("get_document", "Get a specific document by ID with full details including correspondent, document type, tags, and custom fields. Note: Document content is excluded from results by default. Use 'get_document_content' to retrieve content when needed.", {
|
|
194
|
+
id: zod_1.z.number(),
|
|
195
|
+
}, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
|
|
196
|
+
if (!api)
|
|
197
|
+
throw new Error("Please configure API connection first");
|
|
198
|
+
const doc = yield api.getDocument(args.id);
|
|
199
|
+
return (0, documentEnhancer_1.convertDocsWithNames)(doc, api);
|
|
200
|
+
})));
|
|
201
|
+
server.tool("get_document_content", "Get the text content of a specific document by ID. Use this when you need to read or analyze the actual document text.", {
|
|
188
202
|
id: zod_1.z.number(),
|
|
189
203
|
}, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
|
|
190
204
|
if (!api)
|
|
191
205
|
throw new Error("Please configure API connection first");
|
|
192
206
|
const doc = yield api.getDocument(args.id);
|
|
193
|
-
const [correspondents, documentTypes, tags, customFields] = yield Promise.all([
|
|
194
|
-
api.getCorrespondents(),
|
|
195
|
-
api.getDocumentTypes(),
|
|
196
|
-
api.getTags(),
|
|
197
|
-
api.getCustomFields(),
|
|
198
|
-
]);
|
|
199
|
-
const correspondentMap = new Map((correspondents.results || []).map((c) => [c.id, c.name]));
|
|
200
|
-
const documentTypeMap = new Map((documentTypes.results || []).map((dt) => [dt.id, dt.name]));
|
|
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]));
|
|
203
|
-
const docWithNames = Object.assign(Object.assign({}, doc), { correspondent: doc.correspondent
|
|
204
|
-
? {
|
|
205
|
-
id: doc.correspondent,
|
|
206
|
-
name: correspondentMap.get(doc.correspondent) ||
|
|
207
|
-
String(doc.correspondent),
|
|
208
|
-
}
|
|
209
|
-
: null, document_type: doc.document_type
|
|
210
|
-
? {
|
|
211
|
-
id: doc.document_type,
|
|
212
|
-
name: documentTypeMap.get(doc.document_type) ||
|
|
213
|
-
String(doc.document_type),
|
|
214
|
-
}
|
|
215
|
-
: null, tags: Array.isArray(doc.tags)
|
|
216
|
-
? doc.tags.map((tagId) => ({
|
|
217
|
-
id: tagId,
|
|
218
|
-
name: tagMap.get(tagId) || String(tagId),
|
|
219
|
-
}))
|
|
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 });
|
|
227
207
|
return {
|
|
228
208
|
content: [
|
|
229
209
|
{
|
|
230
210
|
type: "text",
|
|
231
|
-
text: JSON.stringify(
|
|
211
|
+
text: JSON.stringify({
|
|
212
|
+
id: doc.id,
|
|
213
|
+
title: doc.title,
|
|
214
|
+
content: doc.content,
|
|
215
|
+
}),
|
|
232
216
|
},
|
|
233
217
|
],
|
|
234
218
|
};
|
|
235
219
|
})));
|
|
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.", {
|
|
220
|
+
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. Note: Document content is excluded from results by default. Use 'get_document_content' to retrieve content when needed.", {
|
|
237
221
|
query: zod_1.z.string(),
|
|
238
222
|
}, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
|
|
239
223
|
if (!api)
|
|
@@ -265,6 +249,25 @@ function registerDocumentTools(server, api) {
|
|
|
265
249
|
],
|
|
266
250
|
};
|
|
267
251
|
})));
|
|
252
|
+
server.tool("get_document_thumbnail", "Get a document thumbnail (image preview) by ID. Returns the thumbnail as a base64-encoded WebP image resource.", {
|
|
253
|
+
id: zod_1.z.number(),
|
|
254
|
+
}, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
|
|
255
|
+
if (!api)
|
|
256
|
+
throw new Error("Please configure API connection first");
|
|
257
|
+
const response = yield api.getThumbnail(args.id);
|
|
258
|
+
return {
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: "resource",
|
|
262
|
+
resource: {
|
|
263
|
+
uri: `document-${args.id}-thumb.webp`,
|
|
264
|
+
blob: Buffer.from(response.data).toString("base64"),
|
|
265
|
+
mimeType: "image/webp",
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
};
|
|
270
|
+
})));
|
|
268
271
|
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
272
|
id: zod_1.z.number().describe("The ID of the document to update"),
|
|
270
273
|
title: zod_1.z
|
|
@@ -312,8 +315,14 @@ function registerDocumentTools(server, api) {
|
|
|
312
315
|
.array(zod_1.z.object({
|
|
313
316
|
field: zod_1.z.number().describe("The custom field ID"),
|
|
314
317
|
value: zod_1.z
|
|
315
|
-
.union([
|
|
316
|
-
.
|
|
318
|
+
.union([
|
|
319
|
+
zod_1.z.string(),
|
|
320
|
+
zod_1.z.number(),
|
|
321
|
+
zod_1.z.boolean(),
|
|
322
|
+
zod_1.z.array(zod_1.z.number()),
|
|
323
|
+
zod_1.z.null(),
|
|
324
|
+
])
|
|
325
|
+
.describe("The value for the custom field. For documentlink fields, use a single document ID (e.g., 123) or an array of document IDs (e.g., [123, 456])."),
|
|
317
326
|
}))
|
|
318
327
|
.optional()
|
|
319
328
|
.describe("Array of custom field values to assign"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baruchiro/paperless-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
],
|
|
26
26
|
"author": "Baruch Odem",
|
|
27
27
|
"license": "ISC",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=24.0.0"
|
|
30
|
+
},
|
|
28
31
|
"repository": {
|
|
29
32
|
"type": "git",
|
|
30
33
|
"url": "git+https://github.com/baruchiro/paperless-mcp.git"
|
|
@@ -51,7 +54,7 @@
|
|
|
51
54
|
"@anthropic-ai/dxt": "^0.2.6",
|
|
52
55
|
"@changesets/cli": "^2.29.4",
|
|
53
56
|
"@types/express": "^5.0.2",
|
|
54
|
-
"@types/node": "^
|
|
57
|
+
"@types/node": "^24.0.0",
|
|
55
58
|
"ts-node": "^10.9.2"
|
|
56
59
|
}
|
|
57
60
|
}
|
package/paperless-mcp.dxt
CHANGED
|
Binary file
|