UNPKG

@baruchiro/paperless-mcp

Version:

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.

938 lines (744 loc) 25.9 kB
<!-- [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/nloui-paperless-mcp-badge.png)](https://mseep.ai/app/nloui-paperless-mcp) --> # Paperless-NGX MCP Server ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/baruchiro/paperless-mcp?utm_source=oss&utm_medium=github&utm_campaign=baruchiro%2Fpaperless-mcp&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) 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. ## Quick Start [![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](https://cursor.com/install-mcp?name=paperless&config=eyJjb21tYW5kIjoibnB4IC15IEBiYXJ1Y2hpcm8vcGFwZXJsZXNzLW1jcEBsYXRlc3QiLCJlbnYiOnsiUEFQRVJMRVNTX1VSTCI6Imh0dHA6Ly95b3VyLXBhcGVybGVzcy1pbnN0YW5jZTo4MDAwIiwiUEFQRVJMRVNTX0FQSV9LRVkiOiJ5b3VyLWFwaS10b2tlbiJ9fQ%3D%3D) ### Installation Add these to your MCP config file: // STDIO mode (recommended for local or CLI use) ```json "paperless": { "command": "npx", "args": [ "-y", "@baruchiro/paperless-mcp@latest", ], "env": { "PAPERLESS_URL": "http://your-paperless-instance:8000", "PAPERLESS_API_KEY": "your-api-token", "PAPERLESS_PUBLIC_URL": "https://your-public-domain.com" } } ``` // HTTP mode (recommended for Docker or remote use) ```json "paperless": { "command": "docker", "args": [ "run", "-i", "--rm", "ghcr.io/baruchiro/paperless-mcp:latest", ], "env": { "PAPERLESS_URL": "http://your-paperless-instance:8000", "PAPERLESS_API_KEY": "your-api-token", "PAPERLESS_PUBLIC_URL": "https://your-public-domain.com" } } ``` 3. Get your API token: 1. Log into your Paperless-NGX instance 2. Click your username in the top right 3. Select "My Profile" 4. Click the circular arrow button to generate a new token 4. Replace the placeholders in your MCP config: - `http://your-paperless-instance:8000` with your Paperless-NGX URL - `your-api-token` with the token you just generated - `https://your-public-domain.com` with your public Paperless-NGX URL (optional, falls back to PAPERLESS_URL) ### Environment Variables | Variable | Required | Default | Description | |---|---|---|---| | `PAPERLESS_URL` | Yes | | Base URL of your Paperless-NGX instance | | `PAPERLESS_API_KEY` | Yes | | API token from your Paperless-NGX profile | | `PAPERLESS_PUBLIC_URL` | No | `PAPERLESS_URL` | Public-facing URL for document links | | `PAPERLESS_API_VERSION` | No | `5` | Paperless-ngx REST API version. Use `10` for Paperless-ngx v3+. If you see HTTP 406 errors, set this to `10`. | | `PAPERLESS_MCP_UPLOAD_PATHS` | No | | Colon-separated list of allowed directories for `file_path` uploads. **Recommended for security.** Example: `/var/uploads:/tmp/scans` | That's it! Now you can ask Claude to help you manage your Paperless-NGX documents. ### Example Usage Here are some things you can ask Claude to do: - "Show me all documents tagged as 'Invoice'" - "Search for documents containing 'tax return'" - "Create a new tag called 'Receipts' with color #FF0000" - "Download document #123" - "List all correspondents" - "Create a new document type called 'Bank Statement'" ## Available Tools ### Document Operations #### list_documents Get a paginated list of documents with simple filters. Use this for straightforward listing tasks. For full-text queries, structured custom field filtering, or advanced Paperless filters, use `query_documents`. Parameters: - page (optional): Page number - page_size (optional): Number of documents per page - search (optional): Simple Paperless search term - correspondent (optional): Correspondent ID - document_type (optional): Document type ID - tag (optional): Tag ID - storage_path (optional): Storage path ID - created__date__gte (optional): Created date on or after YYYY-MM-DD - created__date__lte (optional): Created date on or before YYYY-MM-DD - ordering (optional): Paperless ordering field - archive_serial_number (optional): Archive serial number - archive_serial_number__isnull (optional): Whether the archive serial number is empty - custom_field_query (optional): Raw JSON-encoded Paperless custom field query string - custom_fields__icontains (optional): Case-insensitive substring match across custom field values ```typescript list_documents({ page: 1, page_size: 25 }) ``` #### query_documents Canonical document query tool. Supports full-text querying, simple Paperless search, custom field filters, and documented `/api/documents/` Paperless query parameters. Parameters: - page (optional): Page number - page_size (optional): Number of documents per page - ordering (optional): Paperless ordering field - query (optional): Full-text query string - search (optional): Simple Paperless search term - more_like_id (optional): Find documents similar to this document ID - correspondent (optional): Correspondent ID - document_type (optional): Document type ID - tag (optional): Tag ID - storage_path (optional): Storage path ID - created__date__gte (optional): Created date on or after YYYY-MM-DD - created__date__lte (optional): Created date on or before YYYY-MM-DD - custom_field_query (optional): Structured Paperless custom field query using `[field_name_or_id, operator, value]` leaves or `["AND" | "OR", [clause1, clause2]]` groups - paperless_filters (optional): Additional documented `/api/documents/` Paperless query parameters, passed as key/value pairs ```typescript // Full-text query query_documents({ query: "invoice 2024" }) // Simple search term query_documents({ search: "acme" }) // Custom field exact match query_documents({ custom_field_query: ["Invoice Number", "exact", "12345"] }) // Custom field empty query_documents({ custom_field_query: ["OR", [ ["Invoice Number", "isnull", true], ["Invoice Number", "exact", ""] ]] }) // Custom field missing query_documents({ custom_field_query: ["Invoice Number", "exists", false] }) // Combined filters query_documents({ query: "invoice", tag: 5, created__date__gte: "2024-01-01", custom_field_query: ["Invoice Number", "exists", true] }) // One documented Paperless filter that is not a first-class argument query_documents({ paperless_filters: { id__in: [101, 202, 303] } }) ``` #### get_document Get a specific document by ID. Parameters: - id: Document ID ```typescript get_document({ id: 123 }) ``` #### search_documents Deprecated compatibility wrapper for full-text search. Prefer `query_documents({ query: ... })` for new integrations. Parameters: - query: Search query string ```typescript search_documents({ query: "invoice 2024" }) ``` #### download_document Download a document file by ID. Parameters: - id: Document ID - original (optional): If true, downloads original file instead of archived version ```typescript download_document({ id: 123, original: false }) ``` #### get_document_thumbnail Get a document thumbnail (image preview) by ID. Returns the thumbnail as a base64-encoded WebP image resource. Parameters: - id: Document ID ```typescript get_document_thumbnail({ id: 123 }) ``` #### bulk_edit_documents Perform bulk operations on multiple documents. Parameters: - documents: Array of document IDs - method: One of: - set_correspondent: Set correspondent for documents - set_document_type: Set document type for documents - set_storage_path: Set storage path for documents - add_tag: Add a tag to documents - remove_tag: Remove a tag from documents - modify_tags: Add and/or remove multiple tags - delete: Delete documents - reprocess: Reprocess documents - set_permissions: Set document permissions - merge: Merge multiple documents - split: Split a document into multiple documents - rotate: Rotate document pages - delete_pages: Delete specific pages from a document - Additional parameters based on method: - correspondent: ID for set_correspondent - document_type: ID for set_document_type - storage_path: ID for set_storage_path - tag: ID for add_tag/remove_tag - add_tags: Array of tag IDs for modify_tags - remove_tags: Array of tag IDs for modify_tags - permissions: Object for set_permissions with owner, permissions, merge flag - metadata_document_id: ID for merge to specify metadata source - delete_originals: Boolean for merge/split - pages: String for split "[1,2-3,4,5-7]" or delete_pages "[2,3,4]" - degrees: Number for rotate (90, 180, or 270) Examples: ```typescript // Add a tag to multiple documents bulk_edit_documents({ documents: [1, 2, 3], method: "add_tag", tag: 5 }) // Set correspondent and document type bulk_edit_documents({ documents: [4, 5], method: "set_correspondent", correspondent: 2 }) // Merge documents bulk_edit_documents({ documents: [6, 7, 8], method: "merge", metadata_document_id: 6, delete_originals: true }) // Split document into parts bulk_edit_documents({ documents: [9], method: "split", pages: "[1-2,3-4,5]" }) // Modify multiple tags at once bulk_edit_documents({ documents: [10, 11], method: "modify_tags", add_tags: [1, 2], remove_tags: [3, 4] }) // Modify custom fields bulk_edit_documents({ documents: [12, 13], method: "modify_custom_fields", add_custom_fields: [ { field: 2, value: "year" } ], remove_custom_fields: [] }) // Set an empty custom field value, e.g. a date field used as a pending marker bulk_edit_documents({ documents: [14], method: "modify_custom_fields", add_custom_fields: [ { field: 9, value: "" } ], remove_custom_fields: [] }) ``` #### post_document Upload a new document to Paperless-NGX. **Two upload modes:** 1. **Base64 mode** (traditional): Provide `file` (base64-encoded content) + `filename` 2. **Filesystem mode** (efficient): Provide `file_path` (absolute path on server) **Security Note:** When using `file_path`, set the `PAPERLESS_MCP_UPLOAD_PATHS` environment variable (colon-separated list of allowed directories) to restrict uploads to specific locations. Without this, any file on the server's filesystem could be uploaded. Parameters: - file (optional): Base64 encoded file content. Either `file` or `file_path` required. - file_path (optional): Absolute path to file on server's filesystem. Either `file` or `file_path` required. - filename (optional): Name of the file. Required with `file`, optional with `file_path` (derives from path). - title (optional): Title for the document - created (optional): DateTime when the document was created (e.g. "2024-01-19" or "2024-01-19 06:15:00+02:00") - correspondent (optional): ID of a correspondent - document_type (optional): ID of a document type - storage_path (optional): ID of a storage path - tags (optional): Array of tag IDs - archive_serial_number (optional): Archive serial number - custom_fields (optional): Array of custom field IDs **File size limit:** 100MB for both modes ```typescript // Base64 mode (traditional) post_document({ file: "base64_encoded_content", filename: "invoice.pdf", title: "January Invoice", created: "2024-01-19", correspondent: 1, document_type: 2, tags: [1, 3], archive_serial_number: "2024-001", custom_fields: [1, 2] }) // Filesystem mode (more efficient for large files) post_document({ file_path: "/var/uploads/invoice.pdf", title: "January Invoice", correspondent: 1, document_type: 2, tags: [1, 3] }) ``` ### Document Notes #### list_document_notes List all notes attached to a document. Parameters: - id: Document ID ```typescript list_document_notes({ id: 123 }) ``` #### create_document_note Add a note to a document. Returns the document's full list of notes. Parameters: - id: Document ID - note: The note text to add ```typescript create_document_note({ id: 123, note: "Invoice paid on 2026-06-30 from Commerzbank account." }) ``` #### delete_document_note ⚠️ Delete a single note from a document by its note ID. This operation is irreversible. Parameters: - id: Document ID - note_id: The ID of the note to delete - confirm: Must be `true` to confirm this destructive operation ```typescript delete_document_note({ id: 123, note_id: 5, confirm: true }) ``` ### Tag Operations #### list_tags Get all tags. ```typescript list_tags() ``` #### create_tag Create a new tag. Parameters: - name: Tag name - color (optional): Hex color code (e.g. "#ff0000") - match (optional): Text pattern to match - matching_algorithm (optional): Number between 0 and 6: 0 - None 1 - Any word 2 - All words 3 - Exact match 4 - Regular expression 5 - Fuzzy word 6 - Automatic ```typescript create_tag({ name: "Invoice", color: "#ff0000", match: "invoice", matching_algorithm: 5 }) ``` ### Correspondent Operations #### list_correspondents Get all correspondents. ```typescript list_correspondents() ``` #### create_correspondent Create a new correspondent. Parameters: - name: Correspondent name - match (optional): Text pattern to match - matching_algorithm (optional): Number between 0 and 6: 0 - None 1 - Any word 2 - All words 3 - Exact match 4 - Regular expression 5 - Fuzzy word 6 - Automatic ```typescript create_correspondent({ name: "ACME Corp", match: "ACME", matching_algorithm: 5 }) ``` ### Document Type Operations #### list_document_types Get all document types. ```typescript list_document_types() ``` #### create_document_type Create a new document type. Parameters: - name: Document type name - match (optional): Text pattern to match - matching_algorithm (optional): Number between 0 and 6: 0 - None 1 - Any word 2 - All words 3 - Exact match 4 - Regular expression 5 - Fuzzy word 6 - Automatic ```typescript create_document_type({ name: "Invoice", match: "invoice total amount due", matching_algorithm: 1 }) ``` ### Custom Field Operations #### list_custom_fields Get all custom fields. ```typescript list_custom_fields() ``` #### get_custom_field Get a specific custom field by ID. Parameters: - id: Custom field ID ```typescript get_custom_field({ id: 1 }) ``` #### create_custom_field Create a new custom field. Parameters: - name: Custom field name - data_type: One of "string", "url", "date", "boolean", "integer", "float", "monetary", "documentlink", "select" - extra_data (optional): Extra data for the custom field, such as select options ```typescript create_custom_field({ name: "Invoice Number", data_type: "string" }) ``` #### update_custom_field Update an existing custom field. Parameters: - id: Custom field ID - name (optional): New custom field name - data_type (optional): New data type - extra_data (optional): Extra data for the custom field ```typescript update_custom_field({ id: 1, name: "Updated Invoice Number", data_type: "string" }) ``` #### delete_custom_field Delete a custom field. Parameters: - id: Custom field ID ```typescript delete_custom_field({ id: 1 }) ``` #### bulk_edit_custom_fields Perform bulk operations on multiple custom fields. Parameters: - custom_fields: Array of custom field IDs - operation: One of "delete" ```typescript bulk_edit_custom_fields({ custom_fields: [1, 2, 3], operation: "delete" }) ``` ### Mail Operations Tools for managing Paperless mail accounts and the mail rules that drive automatic email ingestion. Account passwords/tokens are never exposed: they are redacted from every tool response. #### list_mail_accounts List mail accounts so you can pick the account ID needed when creating a mail rule. Passwords are redacted. Parameters: - page (optional): Page number - page_size (optional): Number of results per page ```typescript list_mail_accounts() ``` #### get_mail_account Get a single mail account by ID. Password/token fields are redacted. Parameters: - id: Mail account ID ```typescript get_mail_account({ id: 1 }) ``` #### process_mail_account Manually trigger Paperless mail processing for one account. This can consume matching mails according to the account's enabled mail rules. Parameters: - id: Mail account ID ```typescript process_mail_account({ id: 1 }) ``` #### list_mail_rules List mail rules with optional pagination. Parameters: - page (optional): Page number - page_size (optional): Number of results per page ```typescript list_mail_rules() ``` #### get_mail_rule Get a single mail rule by ID. Parameters: - id: Mail rule ID ```typescript get_mail_rule({ id: 1 }) ``` #### create_mail_rule Create a mail rule. Use `list_mail_accounts` first to choose the account. Required parameters: - name: Rule name - account: Mail account ID - folder: IMAP folder to scan (e.g. "INBOX") Common optional parameters: - enabled (default true): Whether the rule is active - filter_from / filter_to / filter_subject / filter_body: Match incoming mail - maximum_age: Only process mail newer than this many days - action: 1=Delete, 2=Move to folder, 3=Mark as read, 4=Flag, 5=Tag - action_parameter: Target folder/tag for the chosen action - assign_title_from: 1=Subject, 2=Attachment filename, 3=Do not assign - assign_tags / assign_correspondent / assign_document_type: Metadata to apply - assign_correspondent_from: 1=None, 2=Mail address, 3=Sender name, 4=Use assign_correspondent - attachment_type: 1=Attachments only, 2=All files incl. inline - consumption_scope: 1=Attachments only, 2=Full mail as .eml, 3=Both - pdf_layout: 0=System default, 1=Text+HTML, 2=HTML+text, 3=HTML only, 4=Text only ```typescript create_mail_rule({ name: "Invoices", account: 1, folder: "INBOX", filter_subject: "invoice", action: 3, attachment_type: 1 }) ``` #### update_mail_rule Patch an existing mail rule. Only the fields you supply are changed. Parameters: - id: Mail rule ID - ...any of the `create_mail_rule` fields to update ```typescript update_mail_rule({ id: 1, enabled: false }) ``` #### delete_mail_rule Delete a mail rule. Requires an explicit confirmation flag. This changes future mail ingestion behavior but does not delete any existing documents. Parameters: - id: Mail rule ID - confirm: Must be `true` to confirm deletion ```typescript delete_mail_rule({ id: 1, confirm: true }) ``` ## Error Handling The server will show clear error messages if: - The Paperless-NGX URL or API token is incorrect - The Paperless-NGX server is unreachable - The requested operation fails - The provided parameters are invalid ## Testing ### Unit tests Run the unit test suite (no external dependencies required): ```bash npm test ``` ### E2E tests The E2E suite boots an empty Paperless-ngx instance, runs the compiled MCP server, and drives a deterministic serial scenario through `tools/call` requests creating a tag, correspondent, and document type, uploading a PDF, then exercising list / get / search / download / thumbnail / bulk-edit on the same document. No LLM and no Paperless REST client outside MCP. **Prerequisites:** Docker, Docker Compose, and `jq`. ```bash # 1. Build the MCP server npm run build # 2. Start Paperless-ngx docker compose -f docker-compose.e2e.yml up -d # 3. Wait for Paperless to be ready, then get a token TOKEN=$(curl -s -X POST http://localhost:8000/api/token/ \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"admin123"}' | jq -r '.token') # 4. Start the MCP server node build/index.js --http --port 3001 \ --baseUrl http://localhost:8000 --token "$TOKEN" & MCP_PID=$! # 5. Run the E2E tests MCP_URL=http://localhost:3001/mcp \ PAPERLESS_URL=http://localhost:8000 \ PAPERLESS_TOKEN="$TOKEN" \ npm run test:e2e # 6. Cleanup kill "$MCP_PID" docker compose -f docker-compose.e2e.yml down -v ``` E2E tests also run automatically in CI on every pull request and push to `main`, covering both the `build/index.js` CLI and the published Docker image. ## Development Want to contribute or modify the server? Here's what you need to know: 1. Clone the repository 2. Install dependencies: ```bash npm install ``` 3. Make your changes to server.js 4. Test locally: ```bash node server.js http://localhost:8000 your-test-token ``` The server is built with: - [litemcp](https://github.com/wong2/litemcp): A TypeScript framework for building MCP servers - [zod](https://github.com/colinhacks/zod): TypeScript-first schema validation ## API Documentation 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/). ## Running the MCP Server The MCP server can be run in two modes: ### 1. stdio (default) This is the default mode. The server communicates over stdio, suitable for CLI and direct integrations. ``` npm run start -- <baseUrl> <token> ``` ### 2. HTTP (Streamable HTTP Transport) 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). ``` npm run start -- <baseUrl> <token> --http --port 3000 ``` - The MCP API will be available at `POST /mcp` on the specified port. - Each request is handled statelessly, following the [StreamableHTTPServerTransport](https://github.com/modelcontextprotocol/typescript-sdk) pattern. - GET and DELETE requests to `/mcp` will return 405 Method Not Allowed. #### Per-request API token (HTTP/Docker mode) In HTTP mode, clients authenticate by supplying a Paperless-NGX API token via the standard `Authorization` header: ``` Authorization: Bearer <paperless-ngx-api-token> ``` The token is passed straight through to Paperless-NGX, so each client's own Paperless permissions are enforced end-to-end. This lets a single server instance serve multiple users, each with their own token. The same behaviour applies to both `/mcp` and `/sse` endpoints. > **⚠️ Breaking change in v2.0.0 HTTP mode is now authenticated by default.** > > Previously, a request with no `Authorization` header silently fell back to the server-configured `PAPERLESS_API_KEY`, which left the HTTP endpoint open to anyone who could reach the port. As of v2.0.0, requests without a `Bearer` token are rejected with `401 Unauthorized`. The server token is **never** used for unauthenticated requests unless you explicitly opt in with `--no-auth`. | Scenario | `--no-auth` off (default) | `--no-auth` on | |---|---|---| | Client sends `Authorization: Bearer <tok>` | `<tok>` (client-supplied) | `<tok>` (client-supplied) | | No header, `PAPERLESS_API_KEY` / `--token` set | `401 Unauthorized` | server token | | No header, no server token | `401 Unauthorized` | `401 Unauthorized` | **Migrating from v1.x:** if you relied on the old fallback (a single shared `PAPERLESS_API_KEY` with clients that don't send a token), you have two options: 1. **Recommended:** have each client send `Authorization: Bearer <paperless-token>`. 2. **Restore the old behaviour** (trusted/local networks only): start the server with the `--no-auth` flag, e.g. append it to the Docker `command`/args or your CLI invocation. This requires a server token (`PAPERLESS_API_KEY` or `--token`) to be configured. <details> <summary>Docker Deployment</summary> 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. ### Docker Compose Configuration Create a `docker-compose.yml` file: ```yaml services: paperless-mcp: container_name: paperless-mcp image: ghcr.io/baruchiro/paperless-mcp:latest environment: - PAPERLESS_URL=http://your-paperless-ngx-server:8000 - PAPERLESS_API_KEY=your-paperless-api-key - PAPERLESS_PUBLIC_URL=https://paperless-ngx.yourpublicurl.com ports: - "3000:3000" restart: unless-stopped ``` Then run: ```bash docker-compose up -d ``` ### Using with Continue VS Code Extension If you're using the [Continue VS Code extension](https://continue.dev/), you can configure it to use the Dockerized MCP server via SSE. Create or edit `.continue/mcpServers/paperless-mcp.yaml` at your workspace root: ```yaml name: Paperless version: 0.0.1 schema: v1 mcpServers: - name: Paperless type: sse url: http://localhost:3000/sse ``` **Notes:** - Replace `localhost` with your Docker host's IP address or hostname if running on a remote server - The Docker container handles authentication via environment variables, so no credentials are needed in the Continue config - The SSE endpoint is available at `/sse` on the configured port (default: 3000) </details> # Credits 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. ## Debugging To debug the MCP server in VS Code, use the following launch configuration: ```json { "type": "node", "request": "launch", "name": "Debug Paperless MCP (HTTP, ts-node ESM)", "program": "${workspaceFolder}/node_modules/ts-node/dist/bin.js", "args": [ "--esm", "src/index.ts", "--http", "--baseUrl", "http://your-paperless-instance:8000", "--token", "your-api-token", "--port", "3002" ], "env": { "NODE_OPTIONS": "--loader ts-node/esm", }, "console": "integratedTerminal", "skipFiles": [ "<node_internals>/**" ] } ``` **Important:** Before debugging, uncomment the following line in `src/index.ts` (around line 175): ```typescript // await new Promise((resolve) => setTimeout(resolve, 1000000)); ``` This prevents the server from exiting immediately and allows you to set breakpoints and debug the code.