Model Context Protocol server for Paperless-ngx, backed by pypaperless.
Project description
paperless-mcp
A Model Context Protocol server for Paperless-ngx, powered by pypaperless 6.0. It speaks stdio — so Claude Desktop (or any other MCP client) can launch it directly — and Streamable HTTP, so one instance can serve a whole network.
[!WARNING]
Highly experimental — use at your own risk.
This project is young and moving fast. Tool names, parameters and return shapes still change; such changes are labelled
breaking-changein the release notes, but they do happen, and there is no deprecation window yet. It also rides on a pypaperless release candidate and needs a Paperless-ngx 3.0 that is itself fresh.Nothing here has been proven against your instance, and the caller is an LLM deciding on its own which tools to invoke. If the documents matter to you: have backups, start with
PAPERLESS_MCP_READONLY=true, and leave the delete tools switched off (that is the default) until you trust the setup.
By an LLM, for LLMs
The thing on the other end of this protocol is a language model — and so is the thing that wrote most of the code behind it. @tb1337 builds this project deliberately together with Claude: he sets the direction, writes the specs and reviews every diff; Claude does the typing. That is a stated design choice, not an embarrassing detail buried in the commit log.
It also shapes the server itself. Every decision here optimises for a model as the caller, not a human clicking through a UI:
- errors come back as structured results the model can read and recover from, never as protocol-level failures that just end the conversation,
- list tools page server-side, so a model can walk deep into a result set without burning its context on 500 documents it did not ask for,
- the tool surface can be narrowed (read-only, deletes off by default), because an autonomous caller should not be handed a destructive verb by accident,
- and things like thumbnails come back as real images the model can actually look at.
Features
- Two transports:
stdio(default, for locally spawned clients) andhttp(Streamable HTTP at/mcp, for network use). - Bearer-token auth on the HTTP endpoint, plus an unauthenticated
/healthzfor container probes. - Tunable surface: writes can be disabled (
PAPERLESS_MCP_READONLY=true) and deletes require explicit opt-in (PAPERLESS_MCP_ENABLE_DELETE=true). 47 tools by default, 56 with deletes enabled. - Names, not just IDs: Paperless reports correspondents, tags, document
types, storage paths and owners as bare numbers. The master data is read once
per connection and cached, so every result carries
correspondent_name,tag_names,owner_nameand friends next to the IDs — including the label behind aselectcustom field — without a lookup call per document. - Server-side pagination on every list-shaped tool:
offset/limitare translated into Paperless page requests, so paging deep into a result set costs at most two HTTP calls and each response reportstotalandhas_more. - Structured errors: pypaperless exceptions become results like
{"error": "not_found", "detail": "...", "cause": "..."}instead of protocol-level failures, so the model can recover rather than give up. - Behaviour hints on every tool: each of the 56 tools ships MCP tool
annotations (
readOnlyHint,destructiveHint,idempotentHint,openWorldHint) plus a display title, so a client can wave a search through and stop to ask before a rotate, a merge or anempty_trash. - Survives an unreachable Paperless: the connection is established lazily and retried per call, so the MCP handshake never fails just because the server was briefly down.
- Thumbnails as real images, viewable inline in the client.
- Built on pypaperless 6.0.0rc2 and the MCP Python SDK 2.0 — requires Paperless-ngx 3.0+.
Requirements
- uv on the machine that runs the MCP client.
It manages the virtualenv and the Python toolchain — no system Python 3.13
needed,
uvfetches one if it has to. - Paperless-ngx 3.0+ with an API token
(Settings → API tokens, or
/api/token/).
Installation — uv
paperless-mcp is on PyPI, so uv
can install it without a clone. One wrinkle applies to every command below: this
release depends on pypaperless==6.0.0rc2, a release candidate, and uv
ignores pre-releases unless told otherwise. Two flags handle that:
--prerelease=if-necessary-or-explicit --with 'pypaperless==6.0.0rc2'
Naming pypaperless explicitly makes it the one package allowed to be a
pre-release, so everything else stays on stable versions — the result is the
same dependency set this repo's uv.lock pins and CI tests against. A bare
--prerelease=allow is the trap to avoid: it opens the door for every
dependency at once, so uv installs alpha and dev builds across the stack. On
0.0.1 that includes httpx==1.0.dev3, which has no AsyncClient, and the
server dies on import with module 'httpx' has no attribute 'AsyncClient'.
Both flags become unnecessary once pypaperless 6.0.0 final ships.
Install it as a tool (recommended)
uv tool install --prerelease=if-necessary-or-explicit \
--with 'pypaperless==6.0.0rc2' paperless-mcp
That leaves a standalone paperless-mcp executable on your PATH. Note down
its absolute path — the MCP client will need it — and check it runs:
command -v paperless-mcp # macOS/Linux, typically ~/.local/bin/paperless-mcp
where paperless-mcp # Windows
paperless-mcp --help
Later releases: uv tool upgrade paperless-mcp, then restart the client.
Or run it straight from PyPI, without installing
uvx --prerelease=if-necessary-or-explicit --with 'pypaperless==6.0.0rc2' \
--from paperless-mcp paperless-mcp --help
uvx resolves into a cache on first use and reuses it afterwards. Handy for a
quick look or for running the HTTP transport ad hoc; for a client that spawns
the server on every launch, the installed tool above is the tidier option.
Connect Claude Desktop
Add the paperless entry to your claude_desktop_config.json and restart the
app completely (quit it, don't just close the window — the config is read once
at startup).
With the tool installed, command is the only path involved:
{
"mcpServers": {
"paperless": {
"command": "/absolute/path/to/paperless-mcp",
"env": {
"PAPERLESS_URL": "https://paperless.example.com",
"PAPERLESS_TOKEN": "your-paperless-api-token",
"PAPERLESS_MCP_READONLY": "false",
"PAPERLESS_MCP_ENABLE_DELETE": "false"
}
}
}
}
Or let the client drive uvx, with the resolver flags moved into args:
{
"mcpServers": {
"paperless": {
"command": "/absolute/path/to/uvx",
"args": [
"--prerelease=if-necessary-or-explicit",
"--with", "pypaperless==6.0.0rc2",
"--from", "paperless-mcp",
"paperless-mcp"
],
"env": {
"PAPERLESS_URL": "https://paperless.example.com",
"PAPERLESS_TOKEN": "your-paperless-api-token",
"PAPERLESS_MCP_READONLY": "false",
"PAPERLESS_MCP_ENABLE_DELETE": "false"
}
}
}
}
command must be absolute in both cases. Claude Desktop does not inherit
your shell PATH, so a bare paperless-mcp or uvx usually fails to resolve —
paste what command -v / where reported above.
The last two env entries are the safety switches, spelled out here so they are
easy to find: PAPERLESS_MCP_READONLY=true hides every write and delete tool,
PAPERLESS_MCP_ENABLE_DELETE=true adds the delete tools on top of the default
read+write set. See Configuration for the rest.
Config file locations:
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| Linux | ~/.config/Claude/claude_desktop_config.json |
Ready-made files live in examples/:
claude_desktop_config.json for the
installed tool, and
claude_desktop_config.local-checkout.json
for the clone below.
Any other MCP client that spawns stdio servers takes the same command/args/env triple.
From a git clone instead
For hacking on the server, or to run something newer than the last release:
git clone https://github.com/tb1337/paperless-mcp.git
cd paperless-mcp
uv sync
uv run paperless-mcp --help
The clone needs no resolver flags — uv.lock already pins the pre-release. Give
the clone a permanent home and point the client at it; both paths absolute:
{
"mcpServers": {
"paperless": {
"command": "/absolute/path/to/uv",
"args": ["run", "--directory", "/absolute/path/to/paperless-mcp", "paperless-mcp"],
"env": {
"PAPERLESS_URL": "https://paperless.example.com",
"PAPERLESS_TOKEN": "your-paperless-api-token",
"PAPERLESS_MCP_READONLY": "false",
"PAPERLESS_MCP_ENABLE_DELETE": "false"
}
}
}
}
uv run switches into --directory and uses that project's environment,
whatever working directory the client happens to start it in. After every
git pull, run uv sync again so the environment matches the committed
lockfile, then restart the client.
Troubleshooting
No solution found when resolving tool dependenciesnamingpypaperless==6.0.0rc2— the pre-release flags are missing; see above.module 'httpx' has no attribute 'AsyncClient'— installed 0.0.1 with--prerelease=allow, which let uv pick an httpx dev build. Use the targeted flags above; releases after 0.0.1 cap httpx below 1.0 themselves.- "Server disconnected" right after startup — almost always the
command. Use an absolute path; see above. - "No such file or directory" / the server starts but nothing works — for
the clone setup, the
--directorypath does not point at the clone (it needs the directory containingpyproject.toml). - Tools appear but every call returns
connection_error—PAPERLESS_URLis wrong or unreachable from the machine running the client. The server starts anyway by design; the error text names the cause. auth_failed— the API token is wrong, or belongs to a deactivated user.- Self-signed certificate — set
"PAPERLESS_MCP_VERIFY_SSL": "false". - Logs go to stderr and end up in Claude Desktop's MCP log
(
~/Library/Logs/Claude/mcp-server-paperless.logon macOS). Set"PAPERLESS_MCP_LOG_LEVEL": "DEBUG"for more detail.
Configuration
Every setting has an environment variable and a command-line flag; the flag
wins. A .env file in the working directory is loaded automatically (override
with --env-file) and never overwrites variables the MCP client already set.
| Variable | Flag | Default | Purpose |
|---|---|---|---|
PAPERLESS_URL |
--url |
— (required) | Base URL of your Paperless-ngx instance |
PAPERLESS_TOKEN |
--token |
— (required) | Paperless-ngx API token |
PAPERLESS_MCP_TRANSPORT |
--transport / --stdio / --http |
stdio |
stdio or http |
PAPERLESS_MCP_HOST |
--host |
127.0.0.1 |
Bind address (http only) |
PAPERLESS_MCP_PORT |
--port |
8000 |
Bind port (http only) |
PAPERLESS_MCP_AUTH_TOKEN |
--auth-token |
empty | Bearer token required on /mcp |
PAPERLESS_MCP_READONLY |
--readonly |
false |
If true: hide every write/delete tool |
PAPERLESS_MCP_ENABLE_DELETE |
--enable-delete |
false |
If true: expose delete tools |
PAPERLESS_MCP_MAX_FILE_BYTES |
--max-file-bytes |
25000000 |
Cap for file/thumbnail payloads |
PAPERLESS_MCP_VERIFY_SSL |
--no-verify-ssl |
true |
TLS certificate verification |
PAPERLESS_MCP_TIMEOUT |
--timeout |
30 |
Per-request HTTP timeout, seconds |
PAPERLESS_MCP_NAME_CACHE_TTL |
--name-cache-ttl |
300 |
Lifetime of the ID→name snapshot, seconds (0: forever) |
PAPERLESS_MCP_LOG_LEVEL |
--log-level |
INFO |
Verbosity (always logged to stderr) |
Tool visibility matrix
| Mode | Read tools | Write tools | Delete tools |
|---|---|---|---|
READONLY=true |
✅ | ❌ | ❌ |
| Default | ✅ | ✅ | ❌ |
ENABLE_DELETE=true |
✅ | ✅ | ✅ |
READONLY=true always wins, even if ENABLE_DELETE=true.
Tools
Read: search_documents, get_document, get_document_content,
get_document_metadata, get_document_notes, get_document_history,
find_similar_documents, download_document, get_document_thumbnail,
list_tags, list_correspondents, list_document_types,
list_storage_paths, list_custom_fields, list_share_links,
list_saved_views, get_saved_view, list_trash, list_active_tasks,
list_tasks, get_task, get_statistics, get_paperless_info,
get_document_suggestions, get_document_ai_suggestions.
Write (default-on, suppressed by READONLY): upload_document,
update_document, add_document_note, bulk_edit_documents,
bulk_reprocess_documents, bulk_merge_documents, bulk_rotate_documents,
acknowledge_tasks, create_tag, update_tag, create_correspondent,
update_correspondent, create_document_type, update_document_type,
create_storage_path, update_storage_path, create_custom_field,
update_custom_field, set_document_custom_field,
remove_document_custom_field, create_share_link, restore_documents.
Delete (requires ENABLE_DELETE=true): delete_document,
delete_document_note, delete_tag, delete_correspondent,
delete_document_type, delete_storage_path, delete_custom_field,
delete_share_link, empty_trash.
Notes on semantics:
search_documentscombines a Whoosh full-textquerywith Django-style filters, and takesorder_by/descending.update_documentreplaces the tag list and accepts aclear_fieldslist (correspondent,document_type,storage_path,archive_serial_number) to unset foreign keys. Setting and clearing the same field in one call is rejected. Usebulk_edit_documentsto add or remove individual tags.upload_documentreturns a task UUID; poll it withget_task.set_document_custom_fieldupserts one field value on one document: a field the document does not carry yet is added, an existing one is replaced. The value is checked against the field'sdata_typefirst, so1is not quietly stored astrueand1.0is rejected instead of rounded. Setting the value a field already holds writes nothing and reportschanged: false. Two things to know before adocumentlinkwrite: the list of IDs replaces the stored one (to add a link, read the current list fromget_documentand send it back with the new ID appended), and Paperless maintains the reverse link itself, so linking A to B makes B show A — never set both directions.remove_document_custom_fieldclears the value on one document; the field definition and its values elsewhere are untouched, which is whatdelete_custom_fieldwould destroy instead. A field that is not set is not an error: the call reportsremoved: falseand changes nothing.- Both write the document's custom fields as one array, because that is the only thing the API accepts — a value another client stored between the read and the write is lost.
Tool annotations
Every tool carries MCP annotations, so a client can decide how much ceremony a call deserves without parsing the description:
readOnlyHintis true for all 25 read tools, and only for those.destructiveHint/idempotentHintare left unset there — the spec only gives them meaning once a tool can write.destructiveHintis true where a call overwrites what was already stored: everyupdate_*, everydelete_*, and the bulk operations. It is false for additive tools (upload_document,create_*,add_document_note,restore_documents,acknowledge_tasks).idempotentHintis true only where repeating the identical call converges on the same state. Notably false forbulk_rotate_documents(twice by 90° is 180°),bulk_merge_documentsandupload_document(each call mints another document) andbulk_reprocess_documents(each call queues another task).openWorldHintis false everywhere: the tools reach exactly one configured Paperless instance, not an open-ended set of external entities.
They are hints, not a permission system — the actual gate is
PAPERLESS_MCP_READONLY / PAPERLESS_MCP_ENABLE_DELETE, which decide whether a
tool is advertised at all.
Running over HTTP / in Docker
The server also speaks Streamable HTTP at /mcp, guarded by a bearer token,
and the repo ships a Dockerfile and a docker-compose.yml for exactly that.
Documentation for this path is coming — start with the uv setup above; it is
the supported route for now.
Out of scope (for now)
- Workflows, mail accounts/rules, users, groups, config: admin-tier concerns where letting an LLM make autonomous changes is rarely the right answer.
- Executing saved views:
get_saved_viewreturns the filter rules so the model can translate them into asearch_documentscall, but there is no auto-execution — Paperless' filter-rule numbering is internal and mapping it to Django-style lookups is brittle. - The document chat endpoint (
/api/documents/chat/): it streams plain text, and pypaperless 6.0'sDocumentChatmodel carries only the echoed query, so no answer ever reaches the caller. A tool for it would fail every time; it will come back if the library starts returning the response body.
Development
This repo ships a VS Code devcontainer based on
mcr.microsoft.com/devcontainers/python:3-3.13, with uv, ruff, pytest and
the docker CLI preinstalled. Open the project in VS Code and select
"Reopen in Container" — the dev venv lands at
/home/vscode/.local/dev-venv and dependencies are synced via uv on
container start.
script/bootstrap # uv sync --group dev
uv run pytest # suite + coverage (gate: 80 %)
uv run ruff check --fix . # lint
uv run ruff format . # format
uv run mypy # strict, on the paperless_mcp package
prek run --all-files # everything CI lints, in one go
uv run paperless-mcp --help
AGENTS.md documents the module layout and the conventions that hold this together — the tool surface as public API, why tools never raise, why list tools must paginate. Worth reading before adding a tool, whether you are human or not.
License
MIT.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file paperless_mcp-0.0.5.tar.gz.
File metadata
- Download URL: paperless_mcp-0.0.5.tar.gz
- Upload date:
- Size: 141.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f83a7cb61e9c0009e16e656bcbdc0132e0bc1a0f7c0f5d5b000a8a3094c08f3
|
|
| MD5 |
470dd08a56c7e6c69a3343b284c3b0f3
|
|
| BLAKE2b-256 |
bf1427b914deb4888ea4fc86ac65d5fe49f88555dce03069d1cf3e931b22e1a6
|
Provenance
The following attestation bundles were made for paperless_mcp-0.0.5.tar.gz:
Publisher:
release.yml on tb1337/paperless-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
paperless_mcp-0.0.5.tar.gz -
Subject digest:
3f83a7cb61e9c0009e16e656bcbdc0132e0bc1a0f7c0f5d5b000a8a3094c08f3 - Sigstore transparency entry: 2314780698
- Sigstore integration time:
-
Permalink:
tb1337/paperless-mcp@aa9322919ce3a23f41da9a6b74962f6641354bb8 -
Branch / Tag:
refs/tags/v0.0.5 - Owner: https://github.com/tb1337
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@aa9322919ce3a23f41da9a6b74962f6641354bb8 -
Trigger Event:
release
-
Statement type:
File details
Details for the file paperless_mcp-0.0.5-py3-none-any.whl.
File metadata
- Download URL: paperless_mcp-0.0.5-py3-none-any.whl
- Upload date:
- Size: 54.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c16afb5184c5de8030e9fa774f617f368f67a517504c445df254f9d2f004fb5
|
|
| MD5 |
771cbee526733c2ba05de0f94cbd65a3
|
|
| BLAKE2b-256 |
dc07bec337052a3973a4a02c1731b3f194b333a3020d53dd660052258fc5940f
|
Provenance
The following attestation bundles were made for paperless_mcp-0.0.5-py3-none-any.whl:
Publisher:
release.yml on tb1337/paperless-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
paperless_mcp-0.0.5-py3-none-any.whl -
Subject digest:
8c16afb5184c5de8030e9fa774f617f368f67a517504c445df254f9d2f004fb5 - Sigstore transparency entry: 2314780788
- Sigstore integration time:
-
Permalink:
tb1337/paperless-mcp@aa9322919ce3a23f41da9a6b74962f6641354bb8 -
Branch / Tag:
refs/tags/v0.0.5 - Owner: https://github.com/tb1337
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@aa9322919ce3a23f41da9a6b74962f6641354bb8 -
Trigger Event:
release
-
Statement type: