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 from Docker.
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). 45 tools by default, 54 with deletes enabled. - 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. - 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+.
Quick start — Claude Desktop
Add this to your claude_desktop_config.json and restart the app:
{
"mcpServers": {
"paperless": {
"command": "uvx",
"args": ["--from", "paperless-mcp", "paperless-mcp"],
"env": {
"PAPERLESS_URL": "https://paperless.example.com",
"PAPERLESS_TOKEN": "your-paperless-api-token"
}
}
}
}
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/: the published package, a git
checkout via uv run, and a networked Docker container (see
below).
Get the API token from Paperless-ngx under Settings → API tokens (or
/api/token/). The server runs read+write by default; add
"PAPERLESS_MCP_READONLY": "true" to the env block if you want Claude to
look but not touch, or "PAPERLESS_MCP_ENABLE_DELETE": "true" to also allow
deletions.
Troubleshooting
- "Server disconnected" right after startup — almost always a bad
command. Claude Desktop does not inherit your shellPATH, so use an absolute path (/opt/homebrew/bin/uvx,which uvxto find it) if the bare name does not resolve. - Tools appear but every call returns
connection_error—PAPERLESS_URLis wrong or unreachable from the machine running Claude Desktop. 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.
Quick start — Docker (HTTP transport)
cp .env.example .env
# edit .env: PAPERLESS_URL, PAPERLESS_TOKEN, PAPERLESS_MCP_AUTH_TOKEN
docker compose up -d --build
The server listens on http://<host>:8000/mcp. Point a network-capable MCP
client at it:
{
"mcpServers": {
"paperless": {
"url": "http://paperless-mcp.lan:8000/mcp",
"headers": { "Authorization": "Bearer <PAPERLESS_MCP_AUTH_TOKEN>" }
}
}
}
Running it without Docker:
uvx --from paperless-mcp paperless-mcp --http --host 0.0.0.0 --port 8000
Connecting Claude Desktop to the container
Claude Desktop cannot point at a LAN address directly. Its custom connector UI hands the URL to Anthropic's cloud, which then calls it — so the endpoint would have to be reachable from the public internet, and that path currently offers only OAuth credentials, no bearer header. Everything Claude Desktop launches itself speaks stdio.
The bridge for this is mcp-remote: Claude Desktop starts it over
stdio, and it forwards to the container's HTTP endpoint. It needs Node.js on
the machine running Claude Desktop.
{
"mcpServers": {
"paperless": {
"command": "npx",
"args": [
"-y",
"mcp-remote@latest",
"http://paperless-mcp.lan:8000/mcp",
"--allow-http",
"--transport", "http-only",
"--header", "Authorization:${AUTH_HEADER}"
],
"env": { "AUTH_HEADER": "Bearer your-paperless-mcp-auth-token" }
}
}
}
Also in examples/claude_desktop_config.docker-http.json.
Four details that are easy to get wrong:
- The token goes in
env, not inline inargs. Claude Desktop on Windows does not escape spaces insideargs, which would splitBearer <token>in two — henceAuthorization:${AUTH_HEADER}with no space after the colon. --allow-httpis required for a plainhttp://URL; drop it once you put the endpoint behind TLS.--transport http-onlyskips a pointless SSE fallback — this server only implements Streamable HTTP.- Claude Desktop reads the config once at startup. Quit it completely (not just close the window) and reopen.
If the token is missing or wrong, mcp-remote treats the 401 as an
authentication challenge, tries to discover an OAuth server, and exits with a
bare Fatal error: ServerError that never mentions the token. Check that
AUTH_HEADER matches the container's PAPERLESS_MCP_AUTH_TOKEN and starts
with Bearer . curl http://<host>:8000/healthz (no auth needed) tells you
whether the container itself is reachable.
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; the image uses 0.0.0.0) |
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_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, 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.
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.
Upgrading from 0.1.x
0.2.0 pins pypaperless==6.0.0rc2 from PyPI (0.1.x tracked the library's git
main) and follows its API changes. What that means for callers:
- Task fields follow Paperless-ngx 3.0:
type→task_type,result→result_data,related_document→related_document_ids.task_file_nameis gone. - Saved views no longer report
show_on_dashboard/show_in_sidebar; they reportpage_size,display_modeanddisplay_fieldsinstead. - List responses gained a
totalfield, andhas_moreis now derived from the server-reported match count rather than from over-fetching. - Taxonomy objects report
matching_algorithmas a number plus a readablematching_algorithm_name. - The default transport is now
stdio, not HTTP. Pass--http(or setPAPERLESS_MCP_TRANSPORT=http) for the old behaviour; the Docker image does this for you. - The default bind address is
127.0.0.1instead of0.0.0.0. The Docker image sets0.0.0.0. chat_with_documentswas removed (see above).
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.
# Manual setup (without the devcontainer):
uv sync --group dev
uv run pytest
uv run ruff check . && uv run ruff format --check .
uv run mypy
uv run paperless-mcp --help
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.1.tar.gz.
File metadata
- Download URL: paperless_mcp-0.0.1.tar.gz
- Upload date:
- Size: 119.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d90095dd68b3fa460643a5a8824a1cdc83a0addbde9b716661949a4af961d93a
|
|
| MD5 |
a93de4e79d9329e65960a1411c1da2de
|
|
| BLAKE2b-256 |
813127b5157995d35b128ec0e4d1823ec17d577403095572ed2bffcf02400392
|
Provenance
The following attestation bundles were made for paperless_mcp-0.0.1.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.1.tar.gz -
Subject digest:
d90095dd68b3fa460643a5a8824a1cdc83a0addbde9b716661949a4af961d93a - Sigstore transparency entry: 2312005103
- Sigstore integration time:
-
Permalink:
tb1337/paperless-mcp@5a6c4da476dca90d0621c09a61051e4dd449e015 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/tb1337
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5a6c4da476dca90d0621c09a61051e4dd449e015 -
Trigger Event:
release
-
Statement type:
File details
Details for the file paperless_mcp-0.0.1-py3-none-any.whl.
File metadata
- Download URL: paperless_mcp-0.0.1-py3-none-any.whl
- Upload date:
- Size: 40.3 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 |
df0d94d135dd7c119e8d2d695d06bcc049e00add9880988c734c7056ea067a7d
|
|
| MD5 |
6a53dfba8a1ffd105a23da388aa2b9d2
|
|
| BLAKE2b-256 |
5fcf2e15a4d63250a229b8d9bdcca6d473e64488286df9e3988c72000a3b9da2
|
Provenance
The following attestation bundles were made for paperless_mcp-0.0.1-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.1-py3-none-any.whl -
Subject digest:
df0d94d135dd7c119e8d2d695d06bcc049e00add9880988c734c7056ea067a7d - Sigstore transparency entry: 2312005116
- Sigstore integration time:
-
Permalink:
tb1337/paperless-mcp@5a6c4da476dca90d0621c09a61051e4dd449e015 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/tb1337
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5a6c4da476dca90d0621c09a61051e4dd449e015 -
Trigger Event:
release
-
Statement type: