paperless-ngx-mcp
# paperless-ngx-mcp
[](https://github.com/cubinet-code/paperless-ngx-mcp/actions/workflows/ci.yml)
[](LICENSE)
[](https://www.npmjs.com/package/paperless-ngx-mcp)
[](https://www.npmjs.com/package/paperless-ngx-mcp)
A [Model Context Protocol](https://modelcontextprotocol.io/) server for [Paperless-NGX](https://docs.paperless-ngx.com/). Exposes the full Paperless-NGX REST API to AI assistants — documents, tags, correspondents, document types, custom fields, storage paths, saved views, share links and bundles, workflows, mail accounts and rules, document versions, notes, trash, and tasks.

## Why this one?
- **Complete, and it stays that way.** A CI test checks every endpoint in Paperless's `/api/schema/` against the tools here and fails when upstream adds one that is neither wrapped nor deliberately skipped.
- **Tested against real Paperless.** The end-to-end suite runs the tools against a live Paperless-ngx 3.2.1 container, not mocks.
- **Asks before it breaks things.** Every `delete_*` tool, `empty_trash`, `merge_documents_as_versions` and bulk `delete` require `confirm: true`; bulk edits across "all matching documents" refuse filters Paperless would silently ignore; and the [`triage_inbox`](#triage_inbox) prompt proposes changes and waits for your go-ahead before writing anything.
- **Easy to allowlist.** Verb-first tool names (`list_*`, `get_*`, `delete_*`, …) group into [one permission wildcard each](#tool-naming-convention-for-permission-allowlists).
- **Install it your way:** `npx`, a Docker image, a one-click Claude Desktop extension, or the official MCP Registry.
## Compatibility
Targets **Paperless-ngx 3.2** (tested against 3.2.1). Older Paperless versions are not supported — use `paperless-ngx-mcp@3.1.1` for Paperless 2.x. The package major version tracks the Paperless-ngx major it targets; there are no `1.x` or `2.x` releases.
## Quick Start
The server is published to npm as [`paperless-ngx-mcp`](https://www.npmjs.com/package/paperless-ngx-mcp). You can run it with `npx` — no clone or build required.
### Claude Code
```bash
claude mcp add paperless --scope user \
--env PAPERLESS_URL=https://your-paperless-instance \
--env PAPERLESS_API_KEY=your-api-token \
-- npx -y paperless-ngx-mcp
```
Drop `--scope user` to install for the current project only. See `claude mcp add --help` for more options.
### Codex CLI
```bash
codex mcp add paperless \
--env PAPERLESS_URL=https://your-paperless-instance \
--env PAPERLESS_API_KEY=your-api-token \
-- npx -y paperless-ngx-mcp
```
This writes the entry to `~/.codex/config.toml`.
### Claude Desktop (extension)
Download [`paperless-ngx-mcp.mcpb`](https://github.com/cubinet-code/paperless-ngx-mcp/releases/latest/download/paperless-ngx-mcp.mcpb) from the latest release and double-click it, or install it from **Settings → Extensions**. Claude Desktop asks for your Paperless URL and API token.
### Claude Desktop, Cursor, Cline, and other MCP clients
[](https://cursor.com/en/install-mcp?name=paperless&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInBhcGVybGVzcy1uZ3gtbWNwIl0sImVudiI6eyJQQVBFUkxFU1NfVVJMIjoiaHR0cHM6Ly95b3VyLXBhcGVybGVzcy1pbnN0YW5jZSIsIlBBUEVSTEVTU19BUElfS0VZIjoieW91ci1hcGktdG9rZW4ifX0%3D)
[](https://insiders.vscode.dev/redirect/mcp/install?name=paperless&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22paperless-ngx-mcp%22%5D%2C%22env%22%3A%7B%22PAPERLESS_URL%22%3A%22https%3A%2F%2Fyour-paperless-instance%22%2C%22PAPERLESS_API_KEY%22%3A%22your-api-token%22%7D%7D)
The buttons install placeholder values; replace `PAPERLESS_URL` and `PAPERLESS_API_KEY` afterwards. Or add this to your client's MCP config file (e.g. `claude_desktop_config.json`, `~/.cursor/mcp.json`, `~/.config/cline/mcp.json`):
```json
{
"mcpServers": {
"paperless": {
"command": "npx",
"args": ["-y", "paperless-ngx-mcp"],
"env": {
"PAPERLESS_URL": "https://your-paperless-instance",
"PAPERLESS_API_KEY": "your-api-token",
"PAPERLESS_PUBLIC_URL": "https://your-public-domain"
}
}
}
}
```
### Docker
A multi-arch image (amd64, arm64) is published to `ghcr.io/cubinet-code/paperless-ngx-mcp`. As a stdio server in any MCP client config:
```json
{
"mcpServers": {
"paperless": {
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "PAPERLESS_URL", "-e", "PAPERLESS_API_KEY", "ghcr.io/cubinet-code/paperless-ngx-mcp"],
"env": {
"PAPERLESS_URL": "https://your-paperless-instance",
"PAPERLESS_API_KEY": "your-api-token"
}
}
}
}
```
Or as a long-running [Streamable HTTP](#http-streamable-http-transport) server. It has no authentication, so keep it off untrusted networks:
```bash
docker run -d -p 127.0.0.1:3000:3000 \
-e PAPERLESS_URL=https://your-paperless-instance \
-e PAPERLESS_API_KEY=your-api-token \
ghcr.io/cubinet-code/paperless-ngx-mcp --http --port 3000
```
### Get your Paperless-NGX API token
1. Log into your Paperless-NGX instance.
2. Click your username (top right) → **My Profile**.
3. Click the circular arrow button to generate a new token.
### Configuration
| Variable | Required | Purpose |
|---|---|---|
| `PAPERLESS_URL` | yes | Base URL the MCP server uses to talk to Paperless-NGX. |
| `PAPERLESS_API_KEY` | yes | API token (see above). |
| `PAPERLESS_PUBLIC_URL` | no | Public URL the assistant uses when constructing browser links to documents. Falls back to `PAPERLESS_URL`. |
CLI flags (`--baseUrl`, `--token`, `--publicUrl`, `--http`, `--port`, plus the HTTP [session limits](#session-limits)) take precedence over environment variables.
### Example Usage
Things you can ask Claude (or any MCP-aware assistant):
- "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'"
- "Empty the trash"
- "Show me pending consumption tasks"
- "Move every document from correspondent 'ACME GmbH (mail)' to 'ACME GmbH'"
- "Remove the password from document #55 and keep the unlocked file as a new version"
- "Create a workflow that strips PDF passwords from uploaded bank statements"
- "Which mail rule is creating a new correspondent for every sender?"
## Available Tools
The server registers tools across twelve domains.
### Documents
`list_documents`, `get_document`, `get_document_content`, `search_documents`, `download_document`, `download_documents_bulk`, `get_document_thumbnail`, `get_document_preview`, `get_document_history`, `get_document_metadata`, `update_document`, `post_document`, `email_document`, `edit_documents_bulk`, `delete_document`, `search_autocomplete`, `get_document_suggestions`, `get_document_ai_suggestions`, `get_next_asn`, `upload_document_version`, `update_document_version`, `delete_document_version`, `merge_documents_as_versions`
### Tags
`list_tags`, `get_tag`, `create_tag`, `update_tag`, `delete_tag`, `edit_tags_bulk`
### Correspondents
`list_correspondents`, `get_correspondent`, `create_correspondent`, `update_correspondent`, `delete_correspondent`, `edit_correspondents_bulk`
### Document Types
`list_document_types`, `get_document_type`, `create_document_type`, `update_document_type`, `delete_document_type`, `edit_document_types_bulk`
### Custom Fields
`list_custom_fields`, `get_custom_field`, `create_custom_field`, `update_custom_field`, `delete_custom_field`, `edit_custom_fields_bulk`
### Storage Paths
`list_storage_paths`, `get_storage_path`, `create_storage_path`, `update_storage_path`, `delete_storage_path`, `test_storage_path`
### Saved Views
`list_saved_views`, `get_saved_view`, `create_saved_view`, `update_saved_view`, `delete_saved_view`
### Share Links
`list_share_links`, `list_document_share_links`, `get_share_link`, `create_share_link`, `delete_share_link`, `list_share_link_bundles`, `get_share_link_bundle`, `create_share_link_bundle`, `rebuild_share_link_bundle`, `delete_share_link_bundle`
### Workflows
`list_workflows`, `get_workflow`, `create_workflow`, `update_workflow`, `delete_workflow`, `list_workflow_actions`, `get_workflow_action`, `create_workflow_action`, `update_workflow_action`, `delete_workflow_action`, `list_workflow_triggers`, `get_workflow_trigger`, `create_workflow_trigger`, `update_workflow_trigger`, `delete_workflow_trigger`
### Mail
`list_mail_accounts`, `get_mail_account`, `create_mail_account`, `update_mail_account`, `delete_mail_account`, `test_mail_account`, `process_mail_account`, `list_mail_rules`, `get_mail_rule`, `create_mail_rule`, `update_mail_rule`, `delete_mail_rule`
### System / Notes / Trash / Tasks
`get_statistics`, `get_system_status`, `get_filing_options`, `list_document_notes`, `create_document_note`, `delete_document_note`, `list_trash`, `restore_from_trash`, `empty_trash`, `list_tasks`, `list_active_tasks`, `get_task_status_counts`, `get_task_summary`, `acknowledge_tasks`
### Tool naming convention (for permission allowlists)
Tool names are **verb-first**, so wildcard-based permission rules group cleanly by operation:
| Wildcard | Covers |
|---|---|
| `mcp__paperless__list_*` | All list/index reads |
| `mcp__paperless__get_*` | All single-item reads |
| `mcp__paperless__search_*` | Full-text search and autocomplete |
| `mcp__paperless__download_*` | `download_document` and `download_documents_bulk` |
| `mcp__paperless__create_*` | All create endpoints |
| `mcp__paperless__update_*` | Per-item PATCH updates |
| `mcp__paperless__edit_*_bulk` | All bulk-edit operations across entity types |
| `mcp__paperless__delete_*` | ⚠️ Destructive — system-wide deletes |
| `mcp__paperless__test_*` | Dry-run checks: `test_storage_path`, `test_mail_account` |
| `mcp__paperless__upload_*` | `upload_document_version` |
| `mcp__paperless__rebuild_*` | `rebuild_share_link_bundle` |
| `mcp__paperless__merge_*` | ⚠️ `merge_documents_as_versions` — merged documents stop existing on their own |
| `mcp__paperless__process_*` | `process_mail_account` — fetches mail now and runs its rules (which may delete or move mail on the server) |
A read-only allowlist is therefore: `list_*`, `get_*`, `search_*`, `download_*`, `test_*`. Write access without destructive operations: add `create_*`, `update_*`, `edit_*_bulk`, `upload_*`, `rebuild_*`, `post_document`, `email_document`, `restore_from_trash`, `acknowledge_tasks`. `delete_*`, `merge_*`, `process_*` and `empty_trash` should require explicit user approval.
## Prompts
The server also registers MCP **prompts** — reusable, parameterized instructions that surface as slash commands in clients like Claude Code (e.g. `/mcp__paperless__triage_inbox`).
### `triage_inbox`
Walks the assistant through inbox triage: gather existing tags / correspondents / document types, propose metadata for each inbox document **preferring existing items**, present a confirmation table, and only apply changes after the user replies `apply`. New correspondents / types / tags are flagged `(NEW)` so you can veto creations before they happen.
Argument:
- `limit` (optional, default `25`): maximum number of inbox documents to triage in one pass.
### Notable tool details
#### `edit_documents_bulk`
Perform bulk operations on multiple documents.
Parameters:
- Selection: `documents` (array of IDs), **or** `all: true` + `filters` with optional `excluded_documents`. `filters` takes Paperless document filter names such as `correspondent__id`, `tags__id__all`, `document_type__id`, `title_content` or `query` — not `list_documents`' tool arguments. Keys Paperless doesn't know are refused (it would otherwise ignore them and select every document), a preview query catches invalid values, and the result reports `matched_documents`. `all: true` isn't supported for `merge`, `split`, `delete_pages`, `edit_pdf` or `remove_password`.
- `method`: one of `set_correspondent`, `set_document_type`, `set_storage_path`, `add_tag`, `remove_tag`, `modify_tags`, `modify_custom_fields`, `delete`, `reprocess`, `set_permissions`, `merge`, `split`, `rotate`, `delete_pages`, `edit_pdf`, `remove_password`
- Method-specific parameters: `correspondent`, `document_type`, `storage_path`, `tag`, `add_tags`, `remove_tags`, `add_custom_fields`, `remove_custom_fields`, `set_permissions`, `owner`, `merge`, `metadata_document_id`, `delete_originals`, `pages`, `degrees`, `operations`, `update_document`, `include_metadata`, `password`, `delete_original`, `remote_ocr`
```typescript
// Add a tag to multiple documents
edit_documents_bulk({ documents: [1, 2, 3], method: "add_tag", tag: 5 })
// Merge documents
edit_documents_bulk({
documents: [6, 7, 8],
method: "merge",
metadata_document_id: 6,
delete_originals: true,
})
// Split a document into parts
edit_documents_bulk({ documents: [9], method: "split", pages: "[1-2,3-4,5]" })
// Modify multiple tags at once
edit_documents_bulk({
documents: [10, 11],
method: "modify_tags",
add_tags: [1, 2],
remove_tags: [3, 4],
})
// Move every document from a duplicate correspondent (12) to the canonical one (34)
edit_documents_bulk({ all: true, filters: { correspondent__id: 12 }, method: "set_correspondent", correspondent: 34 })
// Unlock a password-protected PDF, keeping the result as a new version
edit_documents_bulk({ documents: [55], method: "remove_password", password: "…", update_document: true })
```
Paperless answers `remove_password` with `OK` even when it skips a document (its latest version isn't encrypted) or the password is wrong, so check the document's `versions` afterwards.
#### Workflows
A workflow — its triggers plus the actions they run — is what Paperless executes; `create_workflow` builds one in a single call. Standalone triggers and actions (`create_workflow_trigger` / `create_workflow_action`) do nothing on their own, and Paperless deletes unattached ones whenever any workflow is updated.
- Action types: 1 assignment, 2 removal, 3 email, 4 webhook, 5 password removal, 6 move to trash, 7 remote OCR, 8 apply AI suggestions. Remote OCR needs a consumption-started trigger; AI suggestions need a trigger other than consumption-started.
- `update_workflow` changes only what you pass — but a `triggers` or `actions` list **replaces** the whole list: entries with an `id` are updated, entries without one are created, and omitted ones are deleted. `get_workflow` output can be edited and sent straight back.
- PDF passwords of password-removal actions are always returned masked (`**********`). Sending the masked list back keeps the stored passwords; a new action needs the real ones.
- Which filters a trigger checks depends on its type: `sources`, `filter_path` and `filter_mailrule` only work on consumption-started triggers (type 1); tag/correspondent/type/storage-path filters, `filter_custom_field_query` and content matching only on added/updated/scheduled (types 2–4); `filter_filename` on every type.
#### Document versions
`upload_document_version` adds a new file to an existing document (a signed copy, a corrected scan), and `merge_documents_as_versions` folds duplicate documents into one. Content, search, downloads and `get_document_metadata` follow the latest version, but `page_count` and the file names on `get_document` describe the original (root) version — check `get_document_content` to see whether the current version is readable.
#### Reading and editing single documents
- `get_filing_options` returns every tag, correspondent, document type, storage path and custom field (id + name) in one call — fetch it once before filing instead of guessing IDs.
- `update_document` takes `add_tags` / `remove_tags` to change single tags without touching the rest. `tags` replaces the whole list. Every tag change returns `tag_changes: { added, removed }`.
- `list_documents` takes `ids` to read several known documents in one call, and `fields` (on `get_document` too) to return only what you need, e.g. `["title", "tags"]`.
- `get_document_content` takes `max_chars` and `offset` for long documents; the result then reports `total_chars` and `truncated`. Paperless stores the text without page boundaries, so slices are by characters.
- `search_documents` searches the full-text index (`mode`: `query` for advanced syntax — the default, `text` for plain words, `title` for titles only). The index can lag behind; `list_documents` with `search` matches substrings straight from the database.
#### `post_document`
Upload a new document.
Parameters: `file`, `filename`, plus optional `title`, `created`, `correspondent`, `document_type`, `storage_path`, `tags`, `archive_serial_number`, `custom_fields`, `poll`, `poll_timeout_seconds`.
`file` accepts **base64-encoded contents** (the universal method — works for any deployment, since the bytes travel over the wire) or an **absolute file path** that the server reads from its own filesystem. The path option only works when the MCP server runs on the same machine as the file (local/stdio deployments); for a remote server, use base64.
Upload is asynchronous. By default the tool returns a task UUID (track it with `list_tasks`). Set `poll: true` to wait for the consumer to finish and get the result in one call — the new `document_id` on success, or the consumer error on failure. `poll_timeout_seconds` (default 30, max 300) caps the wait; raise it for large scans where OCR is slow.
#### Matching algorithms
`create_tag`, `create_correspondent`, `create_document_type`, and `create_storage_path` accept a `matching_algorithm` (0–6). Workflow triggers accept 0–5 (no Automatic):
| Value | Meaning |
|---|---|
| 0 | None |
| 1 | Any word |
| 2 | All words |
| 3 | Exact match |
| 4 | Regular expression |
| 5 | Fuzzy word |
| 6 | Automatic |
Since Paperless-ngx 3.2, Automatic matching only assigns when the classifier is confident enough (`PAPERLESS_CLASSIFIER_MATCH_THRESHOLD`, default 0.6), and regular-expression matching gives up after `PAPERLESS_MATCH_REGEX_TIMEOUT_SECONDS` (default 0.1 s) — raise it if regex rules miss on long documents.
## Running the MCP Server
### stdio (default)
The default mode. The server communicates over stdio — that's what every MCP client config in the Quick Start uses. You usually never run this manually; the MCP client launches it for you.
If you do want to run it directly (e.g. for debugging):
```bash
# via env vars (recommended)
PAPERLESS_URL=http://localhost:8000 PAPERLESS_API_KEY=xxx npx -y paperless-ngx-mcp
# or via CLI flags
npx -y paperless-ngx-mcp --baseUrl http://localhost:8000 --token xxx
```
### HTTP (Streamable HTTP transport)
Use the `--http` flag to expose the server over HTTP. `--port` defaults to `3000`.
```bash
npx -y paperless-ngx-mcp --baseUrl http://localhost:8000 --token xxx --http --port 3000
```
- The MCP API is available at `POST /mcp` on the chosen port, backed by [`StreamableHTTPServerTransport`](https://github.com/modelcontextprotocol/typescript-sdk) in **stateful** mode.
- The first request (an `initialize` call) creates a session and returns an `Mcp-Session-Id` header; subsequent requests must send that header back to reuse the same session. Transports are kept in an in-memory `Map`, so this only works for single-instance deployments.
- `GET /mcp` streams server-initiated messages for a session; `DELETE /mcp` terminates it and evicts it from the map. Both require a valid `Mcp-Session-Id` header.
- A legacy `GET /sse` + `POST /messages` SSE transport is also exposed for clients that don't yet support the streamable transport.
#### Session limits
Every session holds its own MCP server instance (~3.5 MB), and the HTTP port has **no authentication** — so sessions are bounded:
| Flag | Environment variable | Default | Purpose |
|---|---|---|---|
| `--maxSessions` | `PAPERLESS_MAX_SESSIONS` | `50` | Concurrent sessions allowed. Past this, `initialize` is refused with HTTP 503. |
| `--sessionIdleMinutes` | `PAPERLESS_SESSION_IDLE_MINUTES` | `30` | Evict a session after this long with no activity. |
A client that is actively connected — including one holding a `GET /mcp` stream open — is never evicted, no matter how long it stays idle. Only genuinely abandoned sessions are reclaimed.
Because sessions live in memory, `--http` only works for **single-instance** deployments. Do not expose the port to an untrusted network: there is no auth, and it binds all interfaces.
## Error Handling
Tool calls return clear errors when:
- `PAPERLESS_URL` or `PAPERLESS_API_KEY` is missing or wrong
- The Paperless-NGX server is unreachable
- The underlying API rejects the operation — Paperless's own message is passed through (e.g. `{"non_field_errors":["password not specified"]} (HTTP 400)`), while HTML error pages are reduced to the status line
- A Paperless call doesn't finish within 90 seconds (downloads are exempt); for a timed-out write the error warns that the change may already have been applied
- Tool parameters fail validation
## Development
You only need this section if you're modifying the server itself. End users should follow the [Quick Start](#quick-start) instead — there's no need to clone or build.
```bash
git clone https://github.com/cubinet-code/paperless-ngx-mcp.git
cd paperless-ngx-mcp
npm install # install dependencies
npm run start # run the server with tsx (no build step)
npm run build # compile TypeScript to build/
npm test # unit tests (node:test + tsx)
npm run inspect # build, then launch @modelcontextprotocol/inspector
```
`npm run start` accepts the same flags / env vars as the built binary.
### End-to-end tests
E2E tests spin up a real Paperless-NGX container via Docker Compose:
```bash
npm run test:e2e:up # start the test stack (paperless + redis)
npm run test:e2e # run the e2e suite against it
npm run test:e2e:down # tear down and remove volumes
```
Runs against `ghcr.io/paperless-ngx/paperless-ngx:3.2.1`.
Built with:
- [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) — MCP server SDK
- [zod](https://github.com/colinhacks/zod) — schema validation
- [axios](https://github.com/axios/axios) — HTTP client (with keep-alive agents, a 60s idle timeout and a 90s per-request deadline)
## API Documentation
This MCP server wraps endpoints from the Paperless-NGX REST API. See the [official API documentation](https://docs.paperless-ngx.com/api/) for details on the underlying behaviour and field semantics.
## License
ISC. See [LICENSE](LICENSE).
TDQS
Scored across 109 tools
Most tools target distinct resource+action pairs and the descriptions go out of their way to disambiguate (e.g. bulk object edits vs. document edits, classifier vs. AI suggestions). However, with 109 tools there are large overlapping clusters that will still confuse selection: four task tools (list_tasks, list_active_tasks, get_task_status_counts, get_task_summary), three share-link listers, three document-version mutators, and multiple search/filter entry points (search_documents vs list_documents search vs search_autocomplete). The density of near-boundary tools keeps this from being high.
Overwhelmingly a verb_noun convention: list_/get_/create_/update_/delete_ across every resource, plus uniform patterns like edit_*_bulk and test_*/process_* for mail. Minor deviations (post_document instead of create_document, download_document/bulk, upload_document_version, get_next_asn) are readable but break strict predictability.
109 tools is far beyond any reasonable scope for a single server and lands in the extreme-mismatch band. Even with a broad domain (documents plus all metadata objects, workflows, mail, sharing, tasks), the surface is massively bloated and should be consolidated or split into sub-servers.
The surface covers the full Paperless-NGX lifecycle: documents (create/read/update/delete/trash/restore/versions/notes/history/preview/download/email), all metadata objects with CRUD and bulk operations, workflows with triggers/actions, mail accounts and rules, share links and bundles, tasks, saved views, statistics and system status. There are no obvious dead ends for the stated domain.