#
tokens: 17208/50000 16/16 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .cursor
│   └── rules
│       ├── http-transport.mdc
│       └── typescript-mcp-migration.mdc
├── .dockerignore
├── .github
│   ├── FUNDING.yml
│   └── workflows
│       ├── docker-build.yml
│       └── docker-publish.yml
├── .gitignore
├── Dockerfile
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── api
│   │   └── PaperlessAPI.ts
│   ├── index.ts
│   └── tools
│       ├── correspondents.ts
│       ├── documents.ts
│       ├── documentTypes.ts
│       └── tags.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
1 | node_modules
2 | build 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | 
 4 | # Environment variables
 5 | .env
 6 | .env.local
 7 | .env.*.local
 8 | 
 9 | # Build output
10 | dist/
11 | build/
12 | coverage/
13 | 
14 | # Logs
15 | logs/
16 | *.log
17 | npm-debug.log*
18 | yarn-debug.log*
19 | yarn-error.log*
20 | 
21 | # OS
22 | .DS_Store
23 | Thumbs.db
24 | 
25 | # Editor directories and files
26 | .idea/
27 | .vscode/
28 | *.suo
29 | *.ntvs*
30 | *.njsproj
31 | *.sln
32 | *.sw?
33 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

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

--------------------------------------------------------------------------------
/.github/workflows/docker-build.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Docker Build (PR)
 2 | on:
 3 |   pull_request:
 4 |     branches:
 5 |       - '**'
 6 | jobs:
 7 |   build:
 8 |     runs-on: ubuntu-latest
 9 |     steps:
10 |       - uses: actions/checkout@v4
11 |       - name: Set up Docker Buildx
12 |         uses: docker/setup-buildx-action@v3
13 |       - name: Build Docker image
14 |         uses: docker/build-push-action@v6
15 |         with:
16 |           context: .
17 |           file: ./Dockerfile
18 |           push: false
19 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Builder stage
 2 | FROM node:20-slim AS builder
 3 | WORKDIR /app
 4 | COPY package.json package-lock.json ./
 5 | RUN npm ci
 6 | COPY . .
 7 | RUN npm run build
 8 | 
 9 | # Production stage
10 | FROM node:20-slim AS production
11 | 
12 | WORKDIR /app
13 | COPY --from=builder /app/build .
14 | COPY --from=builder /app/node_modules ./node_modules
15 | COPY --from=builder /app/package.json ./package.json
16 | COPY --from=builder /app/package-lock.json ./package-lock.json
17 | 
18 | EXPOSE 3000
19 | ENTRYPOINT [ "node", "index.js", "--http", "--port", "3000" ]
20 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - baseUrl
10 |       - token
11 |     properties:
12 |       baseUrl:
13 |         type: string
14 |         description: The base URL of your Paperless-NGX instance.
15 |       token:
16 |         type: string
17 |         description: The API token for accessing your Paperless-NGX instance.
18 |   commandFunction:
19 |     # A function that produces the CLI command to start the MCP on stdio.
20 |     |-
21 |     (config) => ({command: 'node', args: ['src/index.js', config.baseUrl, config.token]})
```

--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # These are supported funding model platforms
 2 | 
 3 | github: [nloui] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
 4 | patreon: # Replace with a single Patreon username
 5 | open_collective: # Replace with a single Open Collective username
 6 | ko_fi: # Replace with a single Ko-fi username
 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
 9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 | 
```

--------------------------------------------------------------------------------
/.github/workflows/docker-publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Docker Publish
 2 | on:
 3 |   push:
 4 |     branches:
 5 |       - master
 6 |       - main
 7 | 
 8 | jobs:
 9 |   build-and-push:
10 |     runs-on: ubuntu-latest
11 |     permissions:
12 |       contents: read
13 |       packages: write
14 |     steps:
15 |       - uses: actions/checkout@v4
16 |       - name: Set up Docker Buildx
17 |         uses: docker/setup-buildx-action@v3
18 |       - name: Log in to GitHub Container Registry
19 |         uses: docker/login-action@v3
20 |         with:
21 |           registry: ghcr.io
22 |           username: ${{ github.actor }}
23 |           password: ${{ secrets.GITHUB_TOKEN }}
24 |       - name: Extract Docker metadata
25 |         id: meta
26 |         uses: docker/metadata-action@v5
27 |         with:
28 |           images: ghcr.io/${{ github.actor }}/${{ github.repository }}
29 |           flavor: |
30 |             latest=true
31 |       - name: Build and push Docker image
32 |         uses: docker/build-push-action@v6
33 |         with:
34 |           context: .
35 |           file: ./Dockerfile
36 |           push: true
37 |           tags: ${{ steps.meta.outputs.tags }}
38 |           labels: ${{ steps.meta.outputs.labels }} 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@nloui/paperless-mcp",
 3 |   "version": "1.0.0",
 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 |   },
15 |   "keywords": [
16 |     "mcp",
17 |     "paperless-ngx",
18 |     "document-management",
19 |     "ai",
20 |     "claude",
21 |     "model-context-protocol",
22 |     "paperless"
23 |   ],
24 |   "author": "Nick Loui",
25 |   "license": "ISC",
26 |   "repository": {
27 |     "type": "git",
28 |     "url": "git+https://github.com/nloui/paperless-mcp.git"
29 |   },
30 |   "dependencies": {
31 |     "@modelcontextprotocol/sdk": "^1.11.1",
32 |     "express": "^5.1.0",
33 |     "typescript": "^5.8.3",
34 |     "zod": "^3.24.1"
35 |   },
36 |   "devDependencies": {
37 |     "@types/node": "^22.15.17",
38 |     "ts-node": "^10.9.2"
39 |   }
40 | }
41 | 
```

--------------------------------------------------------------------------------
/src/tools/correspondents.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
 2 | import { z } from "zod";
 3 | 
 4 | export function registerCorrespondentTools(server: McpServer, api) {
 5 |   server.tool(
 6 |     "list_correspondents",
 7 |     "Retrieve all available correspondents (people, companies, organizations that send/receive documents). Returns names and automatic matching patterns for document assignment.",
 8 |     { }, async (args, extra) => {
 9 |     if (!api) throw new Error("Please configure API connection first");
10 |     return api.getCorrespondents();
11 |   });
12 | 
13 |   server.tool(
14 |     "create_correspondent",
15 |     "Create a new correspondent (person, company, or organization) for tracking document senders and receivers. Can include automatic matching patterns for smart assignment to incoming documents.",
16 |     {
17 |       name: z.string().describe("Name of the correspondent (person, company, or organization that sends/receives documents). Examples: 'Bank of America', 'John Smith', 'Electric Company'."),
18 |       match: z.string().optional().describe("Text pattern to automatically assign this correspondent to matching documents. Use names, email addresses, or keywords that appear in documents from this correspondent."),
19 |       matching_algorithm: z
20 |         .enum(["any", "all", "exact", "regular expression", "fuzzy"])
21 |         .optional().describe("How to match text patterns: 'any'=any word matches, 'all'=all words must match, 'exact'=exact phrase match, 'regular expression'=use regex patterns, 'fuzzy'=approximate matching with typos. Default is 'any'."),
22 |     },
23 |     async (args, extra) => {
24 |       if (!api) throw new Error("Please configure API connection first");
25 |       return api.createCorrespondent(args);
26 |     }
27 |   );
28 | 
29 |   server.tool(
30 |     "bulk_edit_correspondents",
31 |     "Perform bulk operations on multiple correspondents: set permissions to control who can assign them to documents, or permanently delete multiple correspondents. Use with caution as deletion affects all associated documents.",
32 |     {
33 |       correspondent_ids: z.array(z.number()).describe("Array of correspondent IDs to perform bulk operations on. Use list_correspondents to get valid correspondent IDs."),
34 |       operation: z.enum(["set_permissions", "delete"]).describe("Bulk operation: 'set_permissions' to control who can assign these correspondents to documents, 'delete' to permanently remove correspondents from the system. Warning: Deleting correspondents will remove them from all associated documents."),
35 |       owner: z.number().optional().describe("User ID to set as owner when operation is 'set_permissions'. The owner has full control over these correspondents."),
36 |       permissions: z
37 |         .object({
38 |           view: z.object({
39 |             users: z.array(z.number()).optional().describe("User IDs who can see and assign these correspondents to documents"),
40 |             groups: z.array(z.number()).optional().describe("Group IDs who can see and assign these correspondents to documents"),
41 |           }).describe("Users and groups with permission to view and use these correspondents"),
42 |           change: z.object({
43 |             users: z.array(z.number()).optional().describe("User IDs who can modify correspondent details (name, matching rules)"),
44 |             groups: z.array(z.number()).optional().describe("Group IDs who can modify correspondent details"),
45 |           }).describe("Users and groups with permission to edit these correspondent settings"),
46 |         })
47 |         .optional().describe("Permission settings when operation is 'set_permissions'. Defines who can view/assign and modify these correspondents."),
48 |       merge: z.boolean().optional().describe("Whether to merge with existing permissions (true) or replace them entirely (false). Default is false."),
49 |     },
50 |     async (args, extra) => {
51 |       if (!api) throw new Error("Please configure API connection first");
52 |       return api.bulkEditObjects(
53 |         args.correspondent_ids,
54 |         "correspondents",
55 |         args.operation,
56 |         args.operation === "set_permissions"
57 |           ? {
58 |               owner: args.owner,
59 |               permissions: args.permissions,
60 |               merge: args.merge,
61 |             }
62 |           : {}
63 |       );
64 |     }
65 |   );
66 | }
67 | 
```

--------------------------------------------------------------------------------
/src/tools/documentTypes.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | 
 3 | export function registerDocumentTypeTools(server, api) {
 4 |   server.tool(
 5 |     "list_document_types",
 6 |     "Retrieve all available document types for categorizing documents by purpose or format (Invoice, Receipt, Contract, etc.). Returns names and automatic matching rules.",
 7 |     {
 8 |     // No parameters - returns all available document types
 9 |   }, async (args, extra) => {
10 |     if (!api) throw new Error("Please configure API connection first");
11 |     return api.getDocumentTypes();
12 |   });
13 | 
14 |   server.tool(
15 |     "create_document_type",
16 |     "Create a new document type for categorizing documents by their purpose or format (e.g., Invoice, Receipt, Contract). Can include automatic matching rules for smart classification.",
17 |     {
18 |       name: z.string().describe("Name of the document type for categorizing documents by their purpose or format. Examples: 'Invoice', 'Receipt', 'Contract', 'Letter', 'Bank Statement', 'Tax Document'."),
19 |       match: z.string().optional().describe("Text pattern to automatically assign this document type to matching documents. Use keywords that commonly appear in this type of document (e.g., 'invoice', 'receipt', 'contract terms')."),
20 |       matching_algorithm: z
21 |         .enum(["any", "all", "exact", "regular expression", "fuzzy"])
22 |         .optional().describe("How to match text patterns: 'any'=any word matches, 'all'=all words must match, 'exact'=exact phrase match, 'regular expression'=use regex patterns, 'fuzzy'=approximate matching with typos. Default is 'any'."),
23 |     },
24 |     async (args, extra) => {
25 |       if (!api) throw new Error("Please configure API connection first");
26 |       return api.createDocumentType(args);
27 |     }
28 |   );
29 | 
30 |   server.tool(
31 |     "bulk_edit_document_types",
32 |     "Perform bulk operations on multiple document types: set permissions to control who can assign them to documents, or permanently delete multiple types. Use with caution as deletion affects all associated documents.",
33 |     {
34 |       document_type_ids: z.array(z.number()).describe("Array of document type IDs to perform bulk operations on. Use list_document_types to get valid document type IDs."),
35 |       operation: z.enum(["set_permissions", "delete"]).describe("Bulk operation: 'set_permissions' to control who can assign these document types to documents, 'delete' to permanently remove document types from the system. Warning: Deleting document types will remove the classification from all associated documents."),
36 |       owner: z.number().optional().describe("User ID to set as owner when operation is 'set_permissions'. The owner has full control over these document types."),
37 |       permissions: z
38 |         .object({
39 |           view: z.object({
40 |             users: z.array(z.number()).optional().describe("User IDs who can see and assign these document types to documents"),
41 |             groups: z.array(z.number()).optional().describe("Group IDs who can see and assign these document types to documents"),
42 |           }).describe("Users and groups with permission to view and use these document types for categorization"),
43 |           change: z.object({
44 |             users: z.array(z.number()).optional().describe("User IDs who can modify document type details (name, matching rules)"),
45 |             groups: z.array(z.number()).optional().describe("Group IDs who can modify document type details"),
46 |           }).describe("Users and groups with permission to edit these document type settings"),
47 |         })
48 |         .optional().describe("Permission settings when operation is 'set_permissions'. Defines who can view/assign and modify these document types."),
49 |       merge: z.boolean().optional().describe("Whether to merge with existing permissions (true) or replace them entirely (false). Default is false."),
50 |     },
51 |     async (args, extra) => {
52 |       if (!api) throw new Error("Please configure API connection first");
53 |       return api.bulkEditObjects(
54 |         args.document_type_ids,
55 |         "document_types",
56 |         args.operation,
57 |         args.operation === "set_permissions"
58 |           ? {
59 |               owner: args.owner,
60 |               permissions: args.permissions,
61 |               merge: args.merge,
62 |             }
63 |           : {}
64 |       );
65 |     }
66 |   );
67 | }
68 | 
```

--------------------------------------------------------------------------------
/src/api/PaperlessAPI.ts:
--------------------------------------------------------------------------------

```typescript
  1 | export class PaperlessAPI {
  2 |   constructor(
  3 |     private readonly baseUrl: string,
  4 |     private readonly token: string
  5 |   ) {
  6 |     this.baseUrl = baseUrl;
  7 |     this.token = token;
  8 |   }
  9 | 
 10 |   async request(path: string, options: RequestInit = {}) {
 11 |     const url = `${this.baseUrl}/api${path}`;
 12 |     const headers = {
 13 |       Authorization: `Token ${this.token}`,
 14 |       Accept: "application/json; version=5",
 15 |       "Content-Type": "application/json",
 16 |       "Accept-Language": "en-US,en;q=0.9",
 17 |     };
 18 | 
 19 |     const response = await fetch(url, {
 20 |       ...options,
 21 |       headers: {
 22 |         ...headers,
 23 |         ...options.headers,
 24 |       },
 25 |     });
 26 | 
 27 |     if (!response.ok) {
 28 |       console.error({
 29 |         error: "Error executing request",
 30 |         url,
 31 |         options,
 32 |         status: response.status,
 33 |         response: await response.json(),
 34 |       });
 35 |       throw new Error(`HTTP error! status: ${response.status}`);
 36 |     }
 37 | 
 38 |     return response.json();
 39 |   }
 40 | 
 41 |   // Document operations
 42 |   async bulkEditDocuments(documents, method, parameters = {}) {
 43 |     return this.request("/documents/bulk_edit/", {
 44 |       method: "POST",
 45 |       body: JSON.stringify({
 46 |         documents,
 47 |         method,
 48 |         parameters,
 49 |       }),
 50 |     });
 51 |   }
 52 | 
 53 |   async postDocument(
 54 |     file: File,
 55 |     metadata: Record<string, string | string[]> = {}
 56 |   ) {
 57 |     const formData = new FormData();
 58 |     formData.append("document", file);
 59 | 
 60 |     // Add optional metadata fields
 61 |     if (metadata.title) formData.append("title", metadata.title);
 62 |     if (metadata.created) formData.append("created", metadata.created);
 63 |     if (metadata.correspondent)
 64 |       formData.append("correspondent", metadata.correspondent);
 65 |     if (metadata.document_type)
 66 |       formData.append("document_type", metadata.document_type);
 67 |     if (metadata.storage_path)
 68 |       formData.append("storage_path", metadata.storage_path);
 69 |     if (metadata.tags) {
 70 |       (metadata.tags as string[]).forEach((tag) =>
 71 |         formData.append("tags", tag)
 72 |       );
 73 |     }
 74 |     if (metadata.archive_serial_number) {
 75 |       formData.append("archive_serial_number", metadata.archive_serial_number);
 76 |     }
 77 |     if (metadata.custom_fields) {
 78 |       (metadata.custom_fields as string[]).forEach((field) =>
 79 |         formData.append("custom_fields", field)
 80 |       );
 81 |     }
 82 | 
 83 |     const response = await fetch(
 84 |       `${this.baseUrl}/api/documents/post_document/`,
 85 |       {
 86 |         method: "POST",
 87 |         headers: {
 88 |           Authorization: `Token ${this.token}`,
 89 |         },
 90 |         body: formData,
 91 |       }
 92 |     );
 93 | 
 94 |     if (!response.ok) {
 95 |       throw new Error(`HTTP error! status: ${response.status}`);
 96 |     }
 97 | 
 98 |     return response.json();
 99 |   }
100 | 
101 |   async getDocuments(query = "") {
102 |     return this.request(`/documents/${query}`);
103 |   }
104 | 
105 |   async getDocument(id) {
106 |     return this.request(`/documents/${id}/`);
107 |   }
108 | 
109 |   async searchDocuments(query) {
110 |     return this.request(`/documents/?query=${encodeURIComponent(query)}`);
111 |   }
112 | 
113 |   async downloadDocument(id, asOriginal = false) {
114 |     const query = asOriginal ? "?original=true" : "";
115 |     const response = await fetch(
116 |       `${this.baseUrl}/api/documents/${id}/download/${query}`,
117 |       {
118 |         headers: {
119 |           Authorization: `Token ${this.token}`,
120 |         },
121 |       }
122 |     );
123 |     return response;
124 |   }
125 | 
126 |   // Tag operations
127 |   async getTags() {
128 |     return this.request("/tags/");
129 |   }
130 | 
131 |   async createTag(data) {
132 |     return this.request("/tags/", {
133 |       method: "POST",
134 |       body: JSON.stringify(data),
135 |     });
136 |   }
137 | 
138 |   async updateTag(id, data) {
139 |     return this.request(`/tags/${id}/`, {
140 |       method: "PUT",
141 |       body: JSON.stringify(data),
142 |     });
143 |   }
144 | 
145 |   async deleteTag(id) {
146 |     return this.request(`/tags/${id}/`, {
147 |       method: "DELETE",
148 |     });
149 |   }
150 | 
151 |   // Correspondent operations
152 |   async getCorrespondents() {
153 |     return this.request("/correspondents/");
154 |   }
155 | 
156 |   async createCorrespondent(data) {
157 |     return this.request("/correspondents/", {
158 |       method: "POST",
159 |       body: JSON.stringify(data),
160 |     });
161 |   }
162 | 
163 |   // Document type operations
164 |   async getDocumentTypes() {
165 |     return this.request("/document_types/");
166 |   }
167 | 
168 |   async createDocumentType(data) {
169 |     return this.request("/document_types/", {
170 |       method: "POST",
171 |       body: JSON.stringify(data),
172 |     });
173 |   }
174 | 
175 |   // Bulk object operations
176 |   async bulkEditObjects(objects, objectType, operation, parameters = {}) {
177 |     return this.request("/bulk_edit_objects/", {
178 |       method: "POST",
179 |       body: JSON.stringify({
180 |         objects,
181 |         object_type: objectType,
182 |         operation,
183 |         ...parameters,
184 |       }),
185 |     });
186 |   }
187 | }
188 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
  3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  4 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
  5 | import express from "express";
  6 | import { PaperlessAPI } from "./api/PaperlessAPI";
  7 | import { registerCorrespondentTools } from "./tools/correspondents";
  8 | import { registerDocumentTools } from "./tools/documents";
  9 | import { registerDocumentTypeTools } from "./tools/documentTypes";
 10 | import { registerTagTools } from "./tools/tags";
 11 | 
 12 | // Simple CLI argument parsing
 13 | const args = process.argv.slice(2);
 14 | const useHttp = args.includes("--http");
 15 | let port = 3000;
 16 | const portIndex = args.indexOf("--port");
 17 | if (portIndex !== -1 && args[portIndex + 1]) {
 18 |   const parsed = parseInt(args[portIndex + 1], 10);
 19 |   if (!isNaN(parsed)) port = parsed;
 20 | }
 21 | 
 22 | async function main() {
 23 |   let baseUrl: string | undefined;
 24 |   let token: string | undefined;
 25 | 
 26 |   if (useHttp) {
 27 |     baseUrl = process.env.PAPERLESS_URL;
 28 |     token = process.env.API_KEY;
 29 |     if (!baseUrl || !token) {
 30 |       console.error(
 31 |         "When using --http, PAPERLESS_URL and API_KEY environment variables must be set."
 32 |       );
 33 |       process.exit(1);
 34 |     }
 35 |   } else {
 36 |     baseUrl = args[0];
 37 |     token = args[1];
 38 |     if (!baseUrl || !token) {
 39 |       console.error(
 40 |         "Usage: paperless-mcp <baseUrl> <token> [--http] [--port <port>]"
 41 |       );
 42 |       console.error(
 43 |         "Example: paperless-mcp http://localhost:8000 your-api-token --http --port 3000"
 44 |       );
 45 |       console.error(
 46 |         "When using --http, PAPERLESS_URL and API_KEY environment variables must be set."
 47 |       );
 48 |       process.exit(1);
 49 |     }
 50 |   }
 51 | 
 52 |   // Initialize API client and server once
 53 |   const api = new PaperlessAPI(baseUrl, token);
 54 |   const server = new McpServer({ name: "paperless-ngx", version: "1.0.0" });
 55 |   registerDocumentTools(server, api);
 56 |   registerTagTools(server, api);
 57 |   registerCorrespondentTools(server, api);
 58 |   registerDocumentTypeTools(server, api);
 59 | 
 60 |   if (useHttp) {
 61 |     const app = express();
 62 |     app.use(express.json());
 63 | 
 64 |     // Store transports for each session
 65 |     const sseTransports: Record<string, SSEServerTransport> = {};
 66 | 
 67 |     app.post("/mcp", async (req, res) => {
 68 |       try {
 69 |         const transport = new StreamableHTTPServerTransport({
 70 |           sessionIdGenerator: undefined,
 71 |         });
 72 |         res.on("close", () => {
 73 |           transport.close();
 74 |         });
 75 |         await server.connect(transport);
 76 |         await transport.handleRequest(req, res, req.body);
 77 |       } catch (error) {
 78 |         console.error("Error handling MCP request:", error);
 79 |         if (!res.headersSent) {
 80 |           res.status(500).json({
 81 |             jsonrpc: "2.0",
 82 |             error: {
 83 |               code: -32603,
 84 |               message: "Internal server error",
 85 |             },
 86 |             id: null,
 87 |           });
 88 |         }
 89 |       }
 90 |     });
 91 | 
 92 |     app.get("/mcp", async (req, res) => {
 93 |       res.writeHead(405).end(
 94 |         JSON.stringify({
 95 |           jsonrpc: "2.0",
 96 |           error: {
 97 |             code: -32000,
 98 |             message: "Method not allowed.",
 99 |           },
100 |           id: null,
101 |         })
102 |       );
103 |     });
104 | 
105 |     app.delete("/mcp", async (req, res) => {
106 |       res.writeHead(405).end(
107 |         JSON.stringify({
108 |           jsonrpc: "2.0",
109 |           error: {
110 |             code: -32000,
111 |             message: "Method not allowed.",
112 |           },
113 |           id: null,
114 |         })
115 |       );
116 |     });
117 | 
118 |     app.get("/sse", async (req, res) => {
119 |       console.log("SSE request received");
120 |       try {
121 |         const transport = new SSEServerTransport("/messages", res);
122 |         sseTransports[transport.sessionId] = transport;
123 |         res.on("close", () => {
124 |           delete sseTransports[transport.sessionId];
125 |           transport.close();
126 |         });
127 |         await server.connect(transport);
128 |       } catch (error) {
129 |         console.error("Error handling SSE request:", error);
130 |         if (!res.headersSent) {
131 |           res.status(500).json({
132 |             jsonrpc: "2.0",
133 |             error: {
134 |               code: -32603,
135 |               message: "Internal server error",
136 |             },
137 |             id: null,
138 |           });
139 |         }
140 |       }
141 |     });
142 | 
143 |     app.post("/messages", async (req, res) => {
144 |       const sessionId = req.query.sessionId as string;
145 |       const transport = sseTransports[sessionId];
146 |       if (transport) {
147 |         await transport.handlePostMessage(req, res, req.body);
148 |       } else {
149 |         res.status(400).send("No transport found for sessionId");
150 |       }
151 |     });
152 | 
153 |     app.listen(port, () => {
154 |       console.log(
155 |         `MCP Stateless Streamable HTTP Server listening on port ${port}`
156 |       );
157 |     });
158 |   } else {
159 |     const transport = new StdioServerTransport();
160 |     await server.connect(transport);
161 |   }
162 | }
163 | 
164 | main().catch((e) => console.error(e.message));
165 | 
```

--------------------------------------------------------------------------------
/src/tools/tags.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | 
  3 | export function registerTagTools(server, api) {
  4 |   server.tool(
  5 |     "list_tags",
  6 |     "Retrieve all available tags for labeling and organizing documents. Returns tag names, colors, and matching rules for automatic assignment.",
  7 |     {
  8 |     // No parameters - returns all available tags
  9 |   }, async (args, extra) => {
 10 |     if (!api) throw new Error("Please configure API connection first");
 11 |     return api.getTags();
 12 |   });
 13 | 
 14 |   server.tool(
 15 |     "create_tag",
 16 |     "Create a new tag for labeling and organizing documents. Tags can have colors for visual identification and automatic matching rules for smart assignment.",
 17 |     {
 18 |       name: z.string().describe("Tag name for labeling and organizing documents (e.g., 'important', 'taxes', 'receipts'). Must be unique and descriptive."),
 19 |       color: z
 20 |         .string()
 21 |         .regex(/^#[0-9A-Fa-f]{6}$/)
 22 |         .optional().describe("Hex color code for visual identification (e.g., '#FF0000' for red, '#00FF00' for green). If not provided, Paperless assigns a random color."),
 23 |       match: z.string().optional().describe("Text pattern to automatically assign this tag to matching documents. Use keywords, phrases, or regular expressions depending on matching_algorithm."),
 24 |       matching_algorithm: z.number().int().min(0).max(4).optional().describe("How to match text patterns: 0=any word, 1=all words, 2=exact phrase, 3=regular expression, 4=fuzzy matching. Default is 0 (any word)."),
 25 |     },
 26 |     async (args, extra) => {
 27 |       if (!api) throw new Error("Please configure API connection first");
 28 |       return api.createTag(args);
 29 |     }
 30 |   );
 31 | 
 32 |   server.tool(
 33 |     "update_tag",
 34 |     "Modify an existing tag's name, color, or automatic matching rules. Useful for refining tag organization and improving automatic document classification.",
 35 |     {
 36 |       id: z.number().describe("ID of the tag to update. Use list_tags to find existing tag IDs."),
 37 |       name: z.string().describe("New tag name. Must be unique among all tags."),
 38 |       color: z
 39 |         .string()
 40 |         .regex(/^#[0-9A-Fa-f]{6}$/)
 41 |         .optional().describe("New hex color code for visual identification (e.g., '#FF0000' for red). Leave empty to keep current color."),
 42 |       match: z.string().optional().describe("Text pattern for automatic tag assignment. Empty string removes auto-matching. Use keywords, phrases, or regex depending on matching_algorithm."),
 43 |       matching_algorithm: z.number().int().min(0).max(4).optional().describe("Algorithm for pattern matching: 0=any word, 1=all words, 2=exact phrase, 3=regular expression, 4=fuzzy matching."),
 44 |     },
 45 |     async (args, extra) => {
 46 |       if (!api) throw new Error("Please configure API connection first");
 47 |       return api.updateTag(args.id, args);
 48 |     }
 49 |   );
 50 | 
 51 |   server.tool(
 52 |     "delete_tag",
 53 |     "Permanently delete a tag from the system. This removes the tag from all documents that currently use it. Use with caution as this action cannot be undone.",
 54 |     {
 55 |       id: z.number().describe("ID of the tag to permanently delete. This will remove the tag from all documents that currently use it. Use list_tags to find tag IDs."),
 56 |     },
 57 |     async (args, extra) => {
 58 |       if (!api) throw new Error("Please configure API connection first");
 59 |       return api.deleteTag(args.id);
 60 |     }
 61 |   );
 62 | 
 63 |   server.tool(
 64 |     "bulk_edit_tags",
 65 |     "Perform bulk operations on multiple tags: set permissions to control access or permanently delete multiple tags at once. Efficient for managing large tag collections.",
 66 |     {
 67 |       tag_ids: z.array(z.number()).describe("Array of tag IDs to perform bulk operations on. Use list_tags to get valid tag IDs."),
 68 |       operation: z.enum(["set_permissions", "delete"]).describe("Bulk operation: 'set_permissions' to control who can use these tags, 'delete' to permanently remove all specified tags from the system."),
 69 |       owner: z.number().optional().describe("User ID to set as owner when operation is 'set_permissions'. Owner has full control over the tags."),
 70 |       permissions: z
 71 |         .object({
 72 |           view: z.object({
 73 |             users: z.array(z.number()).optional().describe("User IDs who can see and use these tags"),
 74 |             groups: z.array(z.number()).optional().describe("Group IDs who can see and use these tags"),
 75 |           }).describe("Users and groups with view/use permissions for these tags"),
 76 |           change: z.object({
 77 |             users: z.array(z.number()).optional().describe("User IDs who can modify these tags (name, color, matching rules)"),
 78 |             groups: z.array(z.number()).optional().describe("Group IDs who can modify these tags"),
 79 |           }).describe("Users and groups with edit permissions for these tags"),
 80 |         })
 81 |         .optional().describe("Permission settings when operation is 'set_permissions'. Defines who can view/use and modify these tags."),
 82 |       merge: z.boolean().optional().describe("Whether to merge with existing permissions (true) or replace them entirely (false). Default is false."),
 83 |     },
 84 |     async (args, extra) => {
 85 |       if (!api) throw new Error("Please configure API connection first");
 86 |       return api.bulkEditObjects(
 87 |         args.tag_ids,
 88 |         "tags",
 89 |         args.operation,
 90 |         args.operation === "set_permissions"
 91 |           ? {
 92 |               owner: args.owner,
 93 |               permissions: args.permissions,
 94 |               merge: args.merge,
 95 |             }
 96 |           : {}
 97 |       );
 98 |     }
 99 |   );
100 | }
101 | 
```

--------------------------------------------------------------------------------
/src/tools/documents.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | 
  3 | export function registerDocumentTools(server, api) {
  4 |   server.tool(
  5 |     "bulk_edit_documents",
  6 |     "Perform bulk operations on multiple documents simultaneously: set correspondent/type/tags, delete, reprocess, merge, split, rotate, or manage permissions. Efficient for managing large document collections.",
  7 |     {
  8 |       documents: z.array(z.number()).describe("Array of document IDs to perform bulk operations on. Get document IDs from list_documents or search_documents first."),
  9 |       method: z.enum([
 10 |         "set_correspondent",
 11 |         "set_document_type",
 12 |         "set_storage_path",
 13 |         "add_tag",
 14 |         "remove_tag",
 15 |         "modify_tags",
 16 |         "delete",
 17 |         "reprocess",
 18 |         "set_permissions",
 19 |         "merge",
 20 |         "split",
 21 |         "rotate",
 22 |         "delete_pages",
 23 |       ]).describe("The bulk operation to perform: set_correspondent (assign sender/receiver), set_document_type (categorize documents), set_storage_path (organize file location), add_tag/remove_tag/modify_tags (manage labels), delete (permanently remove), reprocess (re-run OCR/indexing), set_permissions (control access), merge (combine documents), split (separate into multiple), rotate (adjust orientation), delete_pages (remove specific pages)"),
 24 |       correspondent: z.number().optional().describe("ID of correspondent to assign when method is 'set_correspondent'. Use list_correspondents to get valid IDs."),
 25 |       document_type: z.number().optional().describe("ID of document type to assign when method is 'set_document_type'. Use list_document_types to get valid IDs."),
 26 |       storage_path: z.number().optional().describe("ID of storage path to assign when method is 'set_storage_path'. Storage paths organize documents in folder hierarchies."),
 27 |       tag: z.number().optional().describe("Single tag ID to add or remove when method is 'add_tag' or 'remove_tag'. Use list_tags to get valid IDs."),
 28 |       add_tags: z.array(z.number()).optional().describe("Array of tag IDs to add when method is 'modify_tags'. Use list_tags to get valid IDs."),
 29 |       remove_tags: z.array(z.number()).optional().describe("Array of tag IDs to remove when method is 'modify_tags'. Use list_tags to get valid IDs."),
 30 |       permissions: z
 31 |         .object({
 32 |           owner: z.number().nullable().optional().describe("User ID to set as document owner, or null to remove ownership"),
 33 |           set_permissions: z
 34 |             .object({
 35 |               view: z.object({
 36 |                 users: z.array(z.number()).describe("User IDs granted view permission"),
 37 |                 groups: z.array(z.number()).describe("Group IDs granted view permission"),
 38 |               }).describe("Users and groups who can view these documents"),
 39 |               change: z.object({
 40 |                 users: z.array(z.number()).describe("User IDs granted edit permission"),
 41 |                 groups: z.array(z.number()).describe("Group IDs granted edit permission"),
 42 |               }).describe("Users and groups who can edit these documents"),
 43 |             })
 44 |             .optional().describe("Specific permission settings for users and groups"),
 45 |           merge: z.boolean().optional().describe("Whether to merge with existing permissions (true) or replace them (false)"),
 46 |         })
 47 |         .optional().describe("Permission settings when method is 'set_permissions'. Controls who can view and edit the documents."),
 48 |       metadata_document_id: z.number().optional().describe("Source document ID when merging documents. The metadata from this document will be preserved."),
 49 |       delete_originals: z.boolean().optional().describe("Whether to delete original documents after merge/split operations. Use with caution."),
 50 |       pages: z.string().optional().describe("Page specification for delete_pages method. Format: '1,3,5-7' to delete pages 1, 3, and 5 through 7."),
 51 |       degrees: z.number().optional().describe("Rotation angle in degrees when method is 'rotate'. Use 90, 180, or 270 for standard rotations."),
 52 |     },
 53 |     async (args, extra) => {
 54 |       if (!api) throw new Error("Please configure API connection first");
 55 |       const { documents, method, ...parameters } = args;
 56 |       return api.bulkEditDocuments(documents, method, parameters);
 57 |     }
 58 |   );
 59 | 
 60 |   server.tool(
 61 |     "post_document",
 62 |     "Upload a new document to Paperless-NGX with metadata. Supports PDF, images (PNG/JPG/TIFF), and text files. Automatically processes for OCR and indexing.",
 63 |     {
 64 |       file: z.string().describe("Base64 encoded file content. Convert your file to base64 before uploading. Supports PDF, images (PNG, JPG, TIFF), and text files."),
 65 |       filename: z.string().describe("Original filename with extension (e.g., 'invoice.pdf', 'receipt.png'). This helps Paperless determine file type and initial document title."),
 66 |       title: z.string().optional().describe("Custom document title. If not provided, Paperless will extract title from filename or document content."),
 67 |       created: z.string().optional().describe("Document creation date in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss). If not provided, uses current date."),
 68 |       correspondent: z.number().optional().describe("ID of the correspondent (sender/receiver) for this document. Use list_correspondents to find or create_correspondent to add new ones."),
 69 |       document_type: z.number().optional().describe("ID of document type for categorization (e.g., Invoice, Receipt, Letter). Use list_document_types to find or create_document_type to add new ones."),
 70 |       storage_path: z.number().optional().describe("ID of storage path to organize document location in folder hierarchy. Leave empty for default storage."),
 71 |       tags: z.array(z.number()).optional().describe("Array of tag IDs to label this document. Use list_tags to find existing tags or create_tag to add new ones."),
 72 |       archive_serial_number: z.string().optional().describe("Custom archive number for document organization and reference. Useful for maintaining external filing systems."),
 73 |       custom_fields: z.array(z.number()).optional().describe("Array of custom field IDs to associate with this document. Custom fields store additional metadata."),
 74 |     },
 75 |     async (args, extra) => {
 76 |       if (!api) throw new Error("Please configure API connection first");
 77 |       const binaryData = Buffer.from(args.file, "base64");
 78 |       const blob = new Blob([binaryData]);
 79 |       const file = new File([blob], args.filename);
 80 |       const { file: _, filename: __, ...metadata } = args;
 81 |       return api.postDocument(file, metadata);
 82 |     }
 83 |   );
 84 | 
 85 |   server.tool(
 86 |     "list_documents",
 87 |     "Retrieve paginated list of all documents in the system with basic metadata. Use for browsing document collections or getting document IDs for other operations.",
 88 |     {
 89 |       page: z.number().optional().describe("Page number for pagination (starts at 1). Use this to browse through large document collections."),
 90 |       page_size: z.number().optional().describe("Number of documents per page (default 25, max 100). Larger page sizes return more documents but may be slower."),
 91 |     },
 92 |     async (args, extra) => {
 93 |       if (!api) throw new Error("Please configure API connection first");
 94 |       const query = new URLSearchParams();
 95 |       if (args.page) query.set("page", args.page);
 96 |       if (args.page_size) query.set("page_size", args.page_size);
 97 |       return api.getDocuments(query.toString() ? `?${query.toString()}` : "");
 98 |     }
 99 |   );
100 | 
101 |   server.tool(
102 |     "get_document",
103 |     "Get complete details for a specific document including full metadata, content preview, tags, correspondent, and document type information.",
104 |     {
105 |       id: z.number().describe("Unique document ID. Get this from list_documents or search_documents results. Returns full document metadata, content preview, and associated tags/correspondent/type."),
106 |     },
107 |     async (args, extra) => {
108 |       if (!api) throw new Error("Please configure API connection first");
109 |       return api.getDocument(args.id);
110 |     }
111 |   );
112 | 
113 |   server.tool(
114 |     "search_documents",
115 |     "Search through documents using full-text search across content, titles, tags, and metadata. Supports advanced query syntax and filtering.",
116 |     {
117 |       query: z.string().describe("Search query to find documents. Supports full-text search through document content, titles, tags, and metadata. Use keywords, phrases in quotes, or advanced syntax like 'tag:important' or 'correspondent:john' for targeted searches."),
118 |     },
119 |     async (args, extra) => {
120 |       if (!api) throw new Error("Please configure API connection first");
121 |       return api.searchDocuments(args.query);
122 |     }
123 |   );
124 | 
125 |   server.tool(
126 |     "download_document",
127 |     "Download a document file as base64-encoded data. Choose between original uploaded file or processed/archived version with OCR improvements.",
128 |     {
129 |       id: z.number().describe("Document ID to download. Get this from list_documents, search_documents, or get_document results."),
130 |       original: z.boolean().optional().describe("Whether to download the original uploaded file (true) or the processed/archived version (false, default). Original files preserve exact formatting but may not include OCR improvements."),
131 |     },
132 |     async (args, extra) => {
133 |       if (!api) throw new Error("Please configure API connection first");
134 |       const response = await api.downloadDocument(args.id, args.original);
135 |       return {
136 |         blob: Buffer.from(await response.arrayBuffer()).toString("base64"),
137 |         filename:
138 |           response.headers
139 |             .get("content-disposition")
140 |             ?.split("filename=")[1]
141 |             ?.replace(/"/g, "") || `document-${args.id}`,
142 |       };
143 |     }
144 |   );
145 | }
146 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "compilerOptions": {
  3 |     /* Visit https://aka.ms/tsconfig to read more about this file */
  4 |     /* Projects */
  5 |     // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
  6 |     // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
  7 |     // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
  8 |     // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
  9 |     // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
 10 |     // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
 11 |     /* Language and Environment */
 12 |     "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
 13 |     "lib": [
 14 |       "es2016",
 15 |       "ES2015"
 16 |     ],
 17 |     // "jsx": "preserve",                                /* Specify what JSX code is generated. */
 18 |     // "libReplacement": true,                           /* Enable lib replacement. */
 19 |     // "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
 20 |     // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
 21 |     // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
 22 |     // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
 23 |     // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
 24 |     // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
 25 |     // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
 26 |     // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
 27 |     // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
 28 |     /* Modules */
 29 |     "module": "CommonJS",
 30 |     "moduleResolution": "node",
 31 |     // "rootDir": "./",                                  /* Specify the root folder within your source files. */
 32 |     // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
 33 |     // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
 34 |     // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
 35 |     // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
 36 |     // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
 37 |     // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
 38 |     // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
 39 |     // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
 40 |     // "rewriteRelativeImportExtensions": true,          /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
 41 |     // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
 42 |     // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
 43 |     // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
 44 |     // "noUncheckedSideEffectImports": true,             /* Check side effect imports. */
 45 |     // "resolveJsonModule": true,                        /* Enable importing .json files. */
 46 |     // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
 47 |     // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
 48 |     /* JavaScript Support */
 49 |     // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
 50 |     // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
 51 |     // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
 52 |     /* Emit */
 53 |     // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
 54 |     // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
 55 |     // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
 56 |     // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
 57 |     // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
 58 |     // "noEmit": true,                                   /* Disable emitting files from a compilation. */
 59 |     // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
 60 |     "outDir": "build",
 61 |     // "removeComments": true,                           /* Disable emitting comments. */
 62 |     // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
 63 |     // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
 64 |     // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
 65 |     // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
 66 |     // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
 67 |     // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
 68 |     // "newLine": "crlf",                                /* Set the newline character for emitting files. */
 69 |     // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
 70 |     // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
 71 |     // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
 72 |     // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
 73 |     // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
 74 |     /* Interop Constraints */
 75 |     // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
 76 |     // "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
 77 |     // "isolatedDeclarations": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
 78 |     // "erasableSyntaxOnly": true,                       /* Do not allow runtime constructs that are not part of ECMAScript. */
 79 |     // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
 80 |     "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
 81 |     // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
 82 |     "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
 83 |     /* Type Checking */
 84 |     "strict": true, /* Enable all strict type-checking options. */
 85 |     "noImplicitAny": false,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
 86 |     // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
 87 |     // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
 88 |     // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
 89 |     // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
 90 |     // "strictBuiltinIteratorReturn": true,              /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
 91 |     // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
 92 |     // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
 93 |     // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
 94 |     // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
 95 |     // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
 96 |     // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
 97 |     // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
 98 |     // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
 99 |     // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
100 |     // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
101 |     // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
102 |     // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
103 |     // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
104 |     /* Completeness */
105 |     // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
106 |     "skipLibCheck": true /* Skip type checking all .d.ts files. */
107 |   }
108 | }
```