Labsco
baruchiro logo

Paperless-MCP

β˜… 121

from baruchiro

An MCP 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.

πŸ”₯πŸ”₯πŸ”₯βœ“ VerifiedAccount requiredAdvanced setup
<!-- [![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

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.

Available Tools

Document Operations

list_documents

Get a paginated list of all documents.

Parameters:

  • page (optional): Page number
  • page_size (optional): Number of documents per page
list_documents({
  page: 1,
  page_size: 25
})

get_document

Get a specific document by ID.

Parameters:

  • id: Document ID
get_document({
  id: 123
})

search_documents

Full-text search across documents.

Parameters:

  • query: Search query string
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
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
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:

// 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

// 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]
})

Tag Operations

list_tags

Get all tags.

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
create_tag({
  name: "Invoice",
  color: "#ff0000",
  match: "invoice",
  matching_algorithm: 5
})

Correspondent Operations

list_correspondents

Get all correspondents.

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
create_correspondent({
  name: "ACME Corp",
  match: "ACME",
  matching_algorithm: 5
})

Document Type Operations

list_document_types

Get all document types.

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
create_document_type({
  name: "Invoice",
  match: "invoice total amount due",
  matching_algorithm: 1
})

Custom Field Operations

list_custom_fields

Get all custom fields.

list_custom_fields()

get_custom_field

Get a specific custom field by ID.

Parameters:

  • id: Custom field ID
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
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
update_custom_field({
  id: 1,
  name: "Updated Invoice Number",
  data_type: "string"
})

delete_custom_field

Delete a custom field.

Parameters:

  • id: Custom field ID
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"
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
list_mail_accounts()

get_mail_account

Get a single mail account by ID. Password/token fields are redacted.

Parameters:

  • id: Mail account ID
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
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
list_mail_rules()

get_mail_rule

Get a single mail rule by ID.

Parameters:

  • id: Mail rule ID
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
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
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
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):

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.

# 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:
npm install
  1. Make your changes to server.js
  2. Test locally:
node server.js http://localhost:8000 your-test-token

The server is built with:

  • litemcp: A TypeScript framework for building MCP servers
  • 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.

Debugging

To debug the MCP server in VS Code, use the following launch configuration:

{
    "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):

// await new Promise((resolve) => setTimeout(resolve, 1000000));

This prevents the server from exiting immediately and allows you to set breakpoints and debug the code.