@baruchiro/paperless-mcp 0.0.0-test1

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 ADDED
@@ -0,0 +1,363 @@
1
+ # Paperless-NGX MCP Server
2
+
3
+ [![smithery badge](https://smithery.ai/badge/@baruchiro/paperless-mcp)](https://smithery.ai/server/@baruchiro/paperless-mcp)
4
+
5
+ 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.
6
+
7
+ ## Quick Start
8
+
9
+ ### Installing via Smithery
10
+
11
+ To install Paperless NGX MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@baruchiro/paperless-mcp):
12
+
13
+ ```bash
14
+ npx -y @smithery/cli install @baruchiro/paperless-mcp --client claude
15
+ ```
16
+
17
+ ### Manual Installation
18
+ 1. Install the MCP server:
19
+ ```bash
20
+ npm install -g paperless-mcp
21
+ ```
22
+
23
+ 2. Add it to your Claude's MCP configuration:
24
+
25
+ For VSCode extension, edit `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`:
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "paperless": {
30
+ "command": "npx",
31
+ "args": ["paperless-mcp", "http://your-paperless-instance:8000", "your-api-token"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ For Claude desktop app, edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "paperless": {
42
+ "command": "npx",
43
+ "args": ["paperless-mcp", "http://your-paperless-instance:8000", "your-api-token"]
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ 3. Get your API token:
50
+ 1. Log into your Paperless-NGX instance
51
+ 2. Click your username in the top right
52
+ 3. Select "My Profile"
53
+ 4. Click the circular arrow button to generate a new token
54
+
55
+ 4. Replace the placeholders in your MCP config:
56
+ - `http://your-paperless-instance:8000` with your Paperless-NGX URL
57
+ - `your-api-token` with the token you just generated
58
+
59
+ That's it! Now you can ask Claude to help you manage your Paperless-NGX documents.
60
+
61
+ ## Example Usage
62
+
63
+ Here are some things you can ask Claude to do:
64
+
65
+ - "Show me all documents tagged as 'Invoice'"
66
+ - "Search for documents containing 'tax return'"
67
+ - "Create a new tag called 'Receipts' with color #FF0000"
68
+ - "Download document #123"
69
+ - "List all correspondents"
70
+ - "Create a new document type called 'Bank Statement'"
71
+
72
+ ## Available Tools
73
+
74
+ ### Document Operations
75
+
76
+ #### list_documents
77
+ Get a paginated list of all documents.
78
+
79
+ Parameters:
80
+ - page (optional): Page number
81
+ - page_size (optional): Number of documents per page
82
+
83
+ ```typescript
84
+ list_documents({
85
+ page: 1,
86
+ page_size: 25
87
+ })
88
+ ```
89
+
90
+ #### get_document
91
+ Get a specific document by ID.
92
+
93
+ Parameters:
94
+ - id: Document ID
95
+
96
+ ```typescript
97
+ get_document({
98
+ id: 123
99
+ })
100
+ ```
101
+
102
+ #### search_documents
103
+ Full-text search across documents.
104
+
105
+ Parameters:
106
+ - query: Search query string
107
+
108
+ ```typescript
109
+ search_documents({
110
+ query: "invoice 2024"
111
+ })
112
+ ```
113
+
114
+ #### download_document
115
+ Download a document file by ID.
116
+
117
+ Parameters:
118
+ - id: Document ID
119
+ - original (optional): If true, downloads original file instead of archived version
120
+
121
+ ```typescript
122
+ download_document({
123
+ id: 123,
124
+ original: false
125
+ })
126
+ ```
127
+
128
+ #### bulk_edit_documents
129
+ Perform bulk operations on multiple documents.
130
+
131
+ Parameters:
132
+ - documents: Array of document IDs
133
+ - method: One of:
134
+ - set_correspondent: Set correspondent for documents
135
+ - set_document_type: Set document type for documents
136
+ - set_storage_path: Set storage path for documents
137
+ - add_tag: Add a tag to documents
138
+ - remove_tag: Remove a tag from documents
139
+ - modify_tags: Add and/or remove multiple tags
140
+ - delete: Delete documents
141
+ - reprocess: Reprocess documents
142
+ - set_permissions: Set document permissions
143
+ - merge: Merge multiple documents
144
+ - split: Split a document into multiple documents
145
+ - rotate: Rotate document pages
146
+ - delete_pages: Delete specific pages from a document
147
+ - Additional parameters based on method:
148
+ - correspondent: ID for set_correspondent
149
+ - document_type: ID for set_document_type
150
+ - storage_path: ID for set_storage_path
151
+ - tag: ID for add_tag/remove_tag
152
+ - add_tags: Array of tag IDs for modify_tags
153
+ - remove_tags: Array of tag IDs for modify_tags
154
+ - permissions: Object for set_permissions with owner, permissions, merge flag
155
+ - metadata_document_id: ID for merge to specify metadata source
156
+ - delete_originals: Boolean for merge/split
157
+ - pages: String for split "[1,2-3,4,5-7]" or delete_pages "[2,3,4]"
158
+ - degrees: Number for rotate (90, 180, or 270)
159
+
160
+ Examples:
161
+ ```typescript
162
+ // Add a tag to multiple documents
163
+ bulk_edit_documents({
164
+ documents: [1, 2, 3],
165
+ method: "add_tag",
166
+ tag: 5
167
+ })
168
+
169
+ // Set correspondent and document type
170
+ bulk_edit_documents({
171
+ documents: [4, 5],
172
+ method: "set_correspondent",
173
+ correspondent: 2
174
+ })
175
+
176
+ // Merge documents
177
+ bulk_edit_documents({
178
+ documents: [6, 7, 8],
179
+ method: "merge",
180
+ metadata_document_id: 6,
181
+ delete_originals: true
182
+ })
183
+
184
+ // Split document into parts
185
+ bulk_edit_documents({
186
+ documents: [9],
187
+ method: "split",
188
+ pages: "[1-2,3-4,5]"
189
+ })
190
+
191
+ // Modify multiple tags at once
192
+ bulk_edit_documents({
193
+ documents: [10, 11],
194
+ method: "modify_tags",
195
+ add_tags: [1, 2],
196
+ remove_tags: [3, 4]
197
+ })
198
+ ```
199
+
200
+ #### post_document
201
+ Upload a new document to Paperless-NGX.
202
+
203
+ Parameters:
204
+ - file: Base64 encoded file content
205
+ - filename: Name of the file
206
+ - title (optional): Title for the document
207
+ - created (optional): DateTime when the document was created (e.g. "2024-01-19" or "2024-01-19 06:15:00+02:00")
208
+ - correspondent (optional): ID of a correspondent
209
+ - document_type (optional): ID of a document type
210
+ - storage_path (optional): ID of a storage path
211
+ - tags (optional): Array of tag IDs
212
+ - archive_serial_number (optional): Archive serial number
213
+ - custom_fields (optional): Array of custom field IDs
214
+
215
+ ```typescript
216
+ post_document({
217
+ file: "base64_encoded_content",
218
+ filename: "invoice.pdf",
219
+ title: "January Invoice",
220
+ created: "2024-01-19",
221
+ correspondent: 1,
222
+ document_type: 2,
223
+ tags: [1, 3],
224
+ archive_serial_number: "2024-001"
225
+ })
226
+ ```
227
+
228
+ ### Tag Operations
229
+
230
+ #### list_tags
231
+ Get all tags.
232
+
233
+ ```typescript
234
+ list_tags()
235
+ ```
236
+
237
+ #### create_tag
238
+ Create a new tag.
239
+
240
+ Parameters:
241
+ - name: Tag name
242
+ - color (optional): Hex color code (e.g. "#ff0000")
243
+ - match (optional): Text pattern to match
244
+ - matching_algorithm (optional): One of "any", "all", "exact", "regular expression", "fuzzy"
245
+
246
+ ```typescript
247
+ create_tag({
248
+ name: "Invoice",
249
+ color: "#ff0000",
250
+ match: "invoice",
251
+ matching_algorithm: "fuzzy"
252
+ })
253
+ ```
254
+
255
+ ### Correspondent Operations
256
+
257
+ #### list_correspondents
258
+ Get all correspondents.
259
+
260
+ ```typescript
261
+ list_correspondents()
262
+ ```
263
+
264
+ #### create_correspondent
265
+ Create a new correspondent.
266
+
267
+ Parameters:
268
+ - name: Correspondent name
269
+ - match (optional): Text pattern to match
270
+ - matching_algorithm (optional): One of "any", "all", "exact", "regular expression", "fuzzy"
271
+
272
+ ```typescript
273
+ create_correspondent({
274
+ name: "ACME Corp",
275
+ match: "ACME",
276
+ matching_algorithm: "fuzzy"
277
+ })
278
+ ```
279
+
280
+ ### Document Type Operations
281
+
282
+ #### list_document_types
283
+ Get all document types.
284
+
285
+ ```typescript
286
+ list_document_types()
287
+ ```
288
+
289
+ #### create_document_type
290
+ Create a new document type.
291
+
292
+ Parameters:
293
+ - name: Document type name
294
+ - match (optional): Text pattern to match
295
+ - matching_algorithm (optional): One of "any", "all", "exact", "regular expression", "fuzzy"
296
+
297
+ ```typescript
298
+ create_document_type({
299
+ name: "Invoice",
300
+ match: "invoice total amount due",
301
+ matching_algorithm: "any"
302
+ })
303
+ ```
304
+
305
+ ## Error Handling
306
+
307
+ The server will show clear error messages if:
308
+ - The Paperless-NGX URL or API token is incorrect
309
+ - The Paperless-NGX server is unreachable
310
+ - The requested operation fails
311
+ - The provided parameters are invalid
312
+
313
+ ## Development
314
+
315
+ Want to contribute or modify the server? Here's what you need to know:
316
+
317
+ 1. Clone the repository
318
+ 2. Install dependencies:
319
+ ```bash
320
+ npm install
321
+ ```
322
+
323
+ 3. Make your changes to server.js
324
+ 4. Test locally:
325
+ ```bash
326
+ node server.js http://localhost:8000 your-test-token
327
+ ```
328
+
329
+ The server is built with:
330
+ - [litemcp](https://github.com/wong2/litemcp): A TypeScript framework for building MCP servers
331
+ - [zod](https://github.com/colinhacks/zod): TypeScript-first schema validation
332
+
333
+ ## API Documentation
334
+
335
+ This MCP server implements endpoints from the Paperless-NGX REST API. For more details about the underlying API, see the [official documentation](https://docs.paperless-ngx.com/api/).
336
+
337
+ ## Running the MCP Server
338
+
339
+ The MCP server can be run in two modes:
340
+
341
+ ### 1. stdio (default)
342
+
343
+ This is the default mode. The server communicates over stdio, suitable for CLI and direct integrations.
344
+
345
+ ```
346
+ npm run start -- <baseUrl> <token>
347
+ ```
348
+
349
+ ### 2. HTTP (Streamable HTTP Transport)
350
+
351
+ To run the server as an HTTP service, use the `--http` flag. You can also specify the port with `--port` (default: 3000). This mode requires [Express](https://expressjs.com/) to be installed (it is included as a dependency).
352
+
353
+ ```
354
+ npm run start -- <baseUrl> <token> --http --port 3000
355
+ ```
356
+
357
+ - The MCP API will be available at `POST /mcp` on the specified port.
358
+ - Each request is handled statelessly, following the [StreamableHTTPServerTransport](https://github.com/modelcontextprotocol/typescript-sdk) pattern.
359
+ - GET and DELETE requests to `/mcp` will return 405 Method Not Allowed.
360
+
361
+ # Credits
362
+
363
+ 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.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@baruchiro/paperless-mcp",
3
+ "version": "0.0.0-test1",
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
+ "main": "src/index.js",
6
+ "bin": {
7
+ "paperless-mcp": "src/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "start": "ts-node src/index.ts",
12
+ "build": "tsc",
13
+ "inspect": "npm run build && npx -y @modelcontextprotocol/inspector node build/index.js",
14
+ "prepare": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "paperless-ngx",
19
+ "document-management",
20
+ "ai",
21
+ "claude",
22
+ "model-context-protocol",
23
+ "paperless"
24
+ ],
25
+ "author": "Baruch Odem",
26
+ "license": "ISC",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/baruchiro/paperless-mcp"
30
+ },
31
+ "homepage": "https://github.com/baruchiro/paperless-mcp",
32
+ "bugs": {
33
+ "url": "https://github.com/baruchiro/paperless-mcp/issues"
34
+ },
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.11.1",
37
+ "axios": "^1.9.0",
38
+ "express": "^5.1.0",
39
+ "form-data": "^4.0.2",
40
+ "typescript": "^5.8.3",
41
+ "zod": "^3.24.1"
42
+ },
43
+ "devDependencies": {
44
+ "@changesets/cli": "^2.29.4",
45
+ "@types/node": "^22.15.17",
46
+ "ts-node": "^10.9.2"
47
+ }
48
+ }
@@ -0,0 +1,257 @@
1
+ import axios from "axios";
2
+ import FormData from "form-data";
3
+ import {
4
+ BulkEditDocumentsResult,
5
+ Correspondent,
6
+ Document,
7
+ DocumentsResponse,
8
+ DocumentType,
9
+ GetCorrespondentsResponse,
10
+ GetDocumentTypesResponse,
11
+ GetTagsResponse,
12
+ Tag,
13
+ } from "./types";
14
+ import { headersToObject } from "./utils";
15
+
16
+ export class PaperlessAPI {
17
+ constructor(
18
+ private readonly baseUrl: string,
19
+ private readonly token: string
20
+ ) {
21
+ this.baseUrl = baseUrl;
22
+ this.token = token;
23
+ }
24
+
25
+ async request<T = any>(path: string, options: RequestInit = {}) {
26
+ const url = `${this.baseUrl}/api${path}`;
27
+ const isJson = !options.body || typeof options.body === "string";
28
+
29
+ const mergedHeaders = {
30
+ Authorization: `Token ${this.token}`,
31
+ Accept: "application/json; version=5",
32
+ "Accept-Language": "en-US,en;q=0.9",
33
+ ...(isJson ? { "Content-Type": "application/json" } : {}),
34
+ ...headersToObject(options.headers),
35
+ };
36
+
37
+ try {
38
+ const response = await axios<T>({
39
+ url,
40
+ method: options.method || "GET",
41
+ headers: mergedHeaders,
42
+ data: options.body,
43
+ });
44
+
45
+ const body = response.data;
46
+ if (response.status < 200 || response.status >= 300) {
47
+ console.error({
48
+ error: "Error executing request",
49
+ url,
50
+ options,
51
+ status: response.status,
52
+ response: body,
53
+ });
54
+ throw new Error(`HTTP error! status: ${response.status}`);
55
+ }
56
+
57
+ return body;
58
+ } catch (error) {
59
+ console.error({
60
+ error: "Error executing request",
61
+ message: error instanceof Error ? error.message : String(error),
62
+ url,
63
+ options,
64
+ });
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ // Document operations
70
+ async bulkEditDocuments(
71
+ documents: number[],
72
+ method: string,
73
+ parameters = {}
74
+ ): Promise<BulkEditDocumentsResult> {
75
+ return this.request<BulkEditDocumentsResult>("/documents/bulk_edit/", {
76
+ method: "POST",
77
+ body: JSON.stringify({
78
+ documents,
79
+ method,
80
+ parameters,
81
+ }),
82
+ });
83
+ }
84
+
85
+ async postDocument(
86
+ file: File,
87
+ metadata: Record<string, string | string[] | number | number[]> = {}
88
+ ): Promise<string> {
89
+ const formData = new FormData();
90
+ formData.append("document", file);
91
+
92
+ // Add optional metadata fields
93
+ if (metadata.title) formData.append("title", metadata.title);
94
+ if (metadata.created) formData.append("created", metadata.created);
95
+ if (metadata.correspondent)
96
+ formData.append("correspondent", metadata.correspondent);
97
+ if (metadata.document_type)
98
+ formData.append("document_type", metadata.document_type);
99
+ if (metadata.storage_path)
100
+ formData.append("storage_path", metadata.storage_path);
101
+ if (metadata.tags) {
102
+ (metadata.tags as string[]).forEach((tag) =>
103
+ formData.append("tags", tag)
104
+ );
105
+ }
106
+ if (metadata.archive_serial_number) {
107
+ formData.append("archive_serial_number", metadata.archive_serial_number);
108
+ }
109
+ if (metadata.custom_fields) {
110
+ (metadata.custom_fields as string[]).forEach((field) =>
111
+ formData.append("custom_fields", field)
112
+ );
113
+ }
114
+
115
+ const response = await axios.post<string>(
116
+ `${this.baseUrl}/api/documents/post_document/`,
117
+ formData,
118
+ {
119
+ headers: {
120
+ Authorization: `Token ${this.token}`,
121
+ ...formData.getHeaders(),
122
+ },
123
+ }
124
+ );
125
+
126
+ if (response.status < 200 || response.status >= 300) {
127
+ throw new Error(`HTTP error! status: ${response.status}`);
128
+ }
129
+
130
+ return response.data;
131
+ }
132
+
133
+ async getDocuments(query = ""): Promise<DocumentsResponse> {
134
+ return this.request<DocumentsResponse>(`/documents/${query}`);
135
+ }
136
+
137
+ async getDocument(id: number): Promise<Document> {
138
+ return this.request<Document>(`/documents/${id}/`);
139
+ }
140
+
141
+ async searchDocuments(query: string): Promise<DocumentsResponse> {
142
+ const response = await this.request<DocumentsResponse>(
143
+ `/documents/?query=${encodeURIComponent(query)}`
144
+ );
145
+ return response;
146
+ }
147
+
148
+ async downloadDocument(id: number, asOriginal = false) {
149
+ const query = asOriginal ? "?original=true" : "";
150
+ const response = await axios.get(
151
+ `${this.baseUrl}/api/documents/${id}/download/${query}`,
152
+ {
153
+ headers: {
154
+ Authorization: `Token ${this.token}`,
155
+ },
156
+ responseType: "arraybuffer",
157
+ }
158
+ );
159
+ return response;
160
+ }
161
+
162
+ // Tag operations
163
+ async getTags(): Promise<GetTagsResponse> {
164
+ return this.request<GetTagsResponse>("/tags/");
165
+ }
166
+
167
+ async createTag(data: Partial<Tag>): Promise<Tag> {
168
+ return this.request<Tag>("/tags/", {
169
+ method: "POST",
170
+ body: JSON.stringify(data),
171
+ });
172
+ }
173
+
174
+ async updateTag(id: number, data: Partial<Tag>): Promise<Tag> {
175
+ return this.request<Tag>(`/tags/${id}/`, {
176
+ method: "PUT",
177
+ body: JSON.stringify(data),
178
+ });
179
+ }
180
+
181
+ async deleteTag(id: number): Promise<void> {
182
+ return this.request<void>(`/tags/${id}/`, {
183
+ method: "DELETE",
184
+ });
185
+ }
186
+
187
+ // Correspondent operations
188
+ async getCorrespondents(): Promise<GetCorrespondentsResponse> {
189
+ return this.request<GetCorrespondentsResponse>("/correspondents/");
190
+ }
191
+
192
+ async createCorrespondent(
193
+ data: Partial<Correspondent>
194
+ ): Promise<Correspondent> {
195
+ return this.request<Correspondent>("/correspondents/", {
196
+ method: "POST",
197
+ body: JSON.stringify(data),
198
+ });
199
+ }
200
+
201
+ async updateCorrespondent(
202
+ id: number,
203
+ data: Partial<Correspondent>
204
+ ): Promise<Correspondent> {
205
+ return this.request<Correspondent>(`/correspondents/${id}/`, {
206
+ method: "PUT",
207
+ body: JSON.stringify(data),
208
+ });
209
+ }
210
+
211
+ async deleteCorrespondent(id: number): Promise<void> {
212
+ return this.request<void>(`/correspondents/${id}/`, {
213
+ method: "DELETE",
214
+ });
215
+ }
216
+
217
+ // Document type operations
218
+ async getDocumentTypes(): Promise<GetDocumentTypesResponse> {
219
+ return this.request<GetDocumentTypesResponse>("/document_types/");
220
+ }
221
+
222
+ async createDocumentType(data: Partial<DocumentType>): Promise<DocumentType> {
223
+ return this.request<DocumentType>("/document_types/", {
224
+ method: "POST",
225
+ body: JSON.stringify(data),
226
+ });
227
+ }
228
+
229
+ async updateDocumentType(
230
+ id: number,
231
+ data: Partial<DocumentType>
232
+ ): Promise<DocumentType> {
233
+ return this.request<DocumentType>(`/document_types/${id}/`, {
234
+ method: "PUT",
235
+ body: JSON.stringify(data),
236
+ });
237
+ }
238
+
239
+ async deleteDocumentType(id: number): Promise<void> {
240
+ return this.request<void>(`/document_types/${id}/`, {
241
+ method: "DELETE",
242
+ });
243
+ }
244
+
245
+ // Bulk object operations
246
+ async bulkEditObjects(objects, objectType, operation, parameters = {}) {
247
+ return this.request("/bulk_edit_objects/", {
248
+ method: "POST",
249
+ body: JSON.stringify({
250
+ objects,
251
+ object_type: objectType,
252
+ operation,
253
+ ...parameters,
254
+ }),
255
+ });
256
+ }
257
+ }