paperless-mcp-oidc
Allows interaction with a Paperless-ngx API server, providing tools for managing documents, tags, correspondents, and document types in a Paperless-ngx instance, with per-user identity via OIDC.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@paperless-mcp-oidcfind my scanned receipts from last month"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
paperless-mcp-oidc
A drop-in replacement for barryw/PaperlessMCP (v0.6.0) with one difference: every call acts as the calling person (Keycloak/OIDC identity passthrough) instead of a shared Paperless API token.
Same 44 tools, same names, same parameter names/defaults, same result shapes — existing clients and a gateway allowlist keep working unchanged. See Parity below.
Why
The upstream server uses one Paperless API token for every caller. In a household/shared-Paperless setup that means every person (or every agent acting on their behalf) sees every other person's documents — there is no way to tell Paperless "this call is really from Alice, not from whoever holds the token". This server closes that gap: it verifies the caller's own Keycloak JWT and exchanges it, through Keycloak's own token-exchange support and Paperless-ngx's headless allauth login, for that person's own Paperless API token — never a shared token, and never a header Paperless would have to blindly trust.
Related MCP server: paperlessngx-mcp
Auth modes
AUTH_MODE=oidc (default)
Every call acts as the calling person, through a two-step token exchange (see
paperless_mcp_oidc/identity.py):
Inbound: FastMCP verifies the caller's Keycloak access token (signature, issuer, audience, expiry) against
OIDC_JWKS_URI/OIDC_ISSUER/OIDC_AUDIENCE.Keycloak Standard Token Exchange (V2): that access token is exchanged, as the confidential
EXCHANGE_CLIENT_IDclient (client_secret_basic,EXCHANGE_CLIENT_SECRET), for an OIDCid_token—POST OIDC_TOKEN_ENDPOINTwithgrant_type=urn:ietf:params:oauth:grant-type:token-exchange.Paperless-ngx headless allauth login: that
id_tokenis redeemed atPOST {PAPERLESS_BASE_URL}/api/auth/headless/app/v1/auth/provider/tokenfor the person's own persistent Paperless DRF API token.Every Paperless REST call then carries
Authorization: Token <that DRF token>— never a remote-user header, never the inbound Keycloak token.
The resulting DRF token is cached in memory only, per verified sub, for
PAPERLESS_TOKEN_CACHE_SECONDS (default 1h) — most calls don't repeat steps
2–3. On a 401/403 from Paperless with a cached token, the server drops it,
redoes the exchange once, and retries the request once; a second 401/403
surfaces as the usual NOT_ALLOWED error.
All of OIDC_JWKS_URI, OIDC_ISSUER, OIDC_AUDIENCE, MCP_BASE_URL,
OIDC_TOKEN_ENDPOINT, EXCHANGE_CLIENT_ID, and EXCHANGE_CLIENT_SECRET are
required — the server refuses to start otherwise (a half-configured identity
check is worse than none, because it looks safe). No PAPERLESS_API_TOKEN is
read in this mode; if one happens to be set, the server logs a warning and
ignores it. A missing token, a failed Keycloak exchange, or a Paperless
account that has never been linked to Keycloak all fail the tool call before
Paperless is asked for any data.
Keycloak setup required:
The Paperless OIDC client (
EXCHANGE_CLIENT_ID, e.g.paperless) must be confidential (has a client secret) and have "Standard token exchange" enabled (Keycloak 26.x: Client → Advanced tab → Fine grain OpenID Connect configuration → Standard token exchange, or Client → Permissions → enable "Set custom permissions" then granttoken-exchange).Whatever issues the inbound MCP access token needs
paperless(or yourOIDC_AUDIENCE) present in itsaudclaim — add an audience mapper forEXCHANGE_CLIENT_IDon that client.Every person must link their Paperless account to Keycloak once (Paperless UI → user menu → My Profile → connect the Keycloak provider) before their first call. Until they do, calls fail with a clear "connect Keycloak" error rather than silently falling back to anything.
AUTH_MODE=token
Behaves like upstream barryw/PaperlessMCP: one shared PAPERLESS_API_TOKEN,
sent as Authorization: Token <token>. Documented here as single-user —
use it only when there genuinely is one Paperless identity behind this
server (no IdP available). It does not require any OIDC_* setting.
Configuration
Variable | Mode | Default | Description |
| both | — (required) | Paperless-ngx instance URL |
| both | (unset) | Sent as the |
| both |
|
|
| oidc | — (required) | Keycloak JWKS endpoint (inbound token verification) |
| oidc | — (required) | Keycloak realm issuer URL (inbound token verification) |
| oidc | — (required) | Expected |
| oidc | — (required) | Public address of this server (OAuth metadata discovery) |
| oidc |
| JWT claim used to label the caller for the per-person outbox dir and debug logging (not for Paperless identity — that comes from the exchange) |
| oidc | — (required) | Keycloak token endpoint, used for the access_token → id_token exchange |
| oidc | — (required) | The Paperless OIDC client in Keycloak (confidential, Standard token exchange enabled) |
| oidc | — (required) | That client's secret ( |
| oidc |
| Paperless allauth provider id for the Keycloak connection |
| oidc |
| How long an exchanged Paperless DRF token is cached in memory, per person |
| token | — (required in token mode) | Shared Paperless API token |
| both |
| Timeout for calls to Paperless and to Keycloak |
| both |
| Upper bound clamp for any |
| both |
| Directory |
| both |
| Bind address for the streamable-HTTP transport |
Copy .env.example to .env and fill in the required values for your mode.
Running
uv sync --all-extras # or: pip install -e ".[dev]"
cp .env.example .env # fill in your values
paperless-mcp-oidc # streamable HTTP on :8000/mcp, health at :8000/healthzdocker build -t paperless-mcp-oidc .
docker run --rm -p 8000:8000 --env-file .env paperless-mcp-oidcSecurity
No remote-user header, ever. Every Paperless call carries
Authorization: Token <drf-token> — the person's own token, obtained through
the Keycloak → Paperless exchange (see Auth modes above), never a header
Paperless would have to blindly trust. Unlike the remote-user approach, this
server does not depend on Paperless-ngx being unreachable from anything
else — there is no NetworkPolicy this server's correctness relies on, since
nothing downstream of it can forge another person's identity by setting a
header. Paperless-ngx's own permission model still decides what an
authenticated user can see, same as if they had logged in directly.
DRF tokens live in memory only. The per-person cache (see
identity.TokenExchanger) is process memory, never written to disk or a
external store, and never logged. Restarting the server drops it; the next
call for each person simply re-exchanges.
File tools are confined per person in oidc mode. paperless_documents_export_to_outbox
writes into PAPERLESS_OUTBOX_DIR/<user>-<hash>/, and paperless_documents_upload_from_path
only reads files inside the caller's own outbox (resolved path, so .. and symlinks cannot
escape; the check runs before the existence check, so it never reveals which other files
exist). This is a deliberate deviation from the upstream, where both tools see the whole
container — fine for one user, a cross-user leak for many. token mode keeps the upstream
behaviour.
Superusers still see everything. The exchanged DRF token belongs to a Paperless user, and Paperless's own permission model (not this server) decides what that user can see. A caller whose linked Paperless account is a superuser sees every document, same as they would logging in directly — this server does not add per-document access control beyond what Paperless itself enforces for that user.
AUTH_MODE=token has no per-caller isolation — it is the same
single-shared-identity model as upstream barryw/PaperlessMCP. Use it only
when that is actually what you want.
Parity with barryw/PaperlessMCP
All 44 tools, their names, parameter names (including the C# camelCase
originals like pageSize, documentType, matchingAlgorithm — not Python's
snake_case convention, since MCP clients call this server by parameter
name), defaults, and JSON result shapes ({ok, result, meta, warnings} /
{ok, error: {code, message, details}, meta}) mirror the C# reference
1:1 where practical. Notable, deliberately-ported non-obvious behaviours:
MAX_PAGE_SIZEclamp: anypageSizea caller requests is clamped to[1, MAX_PAGE_SIZE]; a non-positive configured maximum falls back to 100.Destructive/bulk tools default to a dry run:
delete-style tools show what would be deleted unlessconfirm=true; bulk tools defaultdryRun=trueand additionally requireconfirm=trueto execute.paperless_documents_update:correspondent/documentType/storagePath= -1is documented upstream as "clear the field", but the C# reference's JSON serializer omits null fields from the request body — so-1has the same wire effect as not passing the field at all (it does not actually clear it on Paperless). Ported verbatim rather than silently fixed, so this server's requests match what upstream actually sends.paperless_documents_uploadvs..._upload_from_path:uploadpasses the caller'stitlethrough as-is (or omits it) and lets Paperless derive a title from the filename server-side;upload_from_pathalways computes and sends the fallback title client-side. Both validate the effective title against Paperless's real 127-character limit before sending (the model allows 128, but Paperless's consumer truncates totitle[:127]on ingestion — silently, so this server rejects instead of corrupting the title).paperless_documents_download/export_to_outbox: inline base64 is capped at 12 KiB — anything larger must go throughexport_to_outbox, which streams the file server-side intoPAPERLESS_OUTBOX_DIR(for another MCP server, e.g. a mail client, to pick up by path) without the bytes ever passing through the model context. Export file names are sanitized to the base name only (no path traversal) and, unless an explicitfilenameis given, have the document ID inserted for uniqueness.Custom field
selectoptions: sent as{"label": ...}objects (Paperless-ngx 2.14+ shape); updating an existing select field preserves each option'sidby matching on label.
Not ported 1:1 (and why)
Strict-typed JSON deserialization failures. The C# client fails a request outright if Paperless returns a field with the wrong JSON type (e.g. a document
idthat isn't a number) and reportsUPSTREAM_ERROR. Python's dynamic typing has no equivalent static deserialization step — malformed values pass through as-is. This is a real behavioural gap, but a narrow one: Paperless-ngx never actually returns non-conforming JSON in practice.Upload-from-path retry/backoff. The C# tool retries the upload up to 3 times with exponential backoff on transient I/O or HTTP errors. This server does not retry — a failed upload returns an error immediately. Pure resilience, doesn't affect the tool's name/parameters/result shape.
Legacy
selectcustom-field format for Paperless-ngx < 2.14. The C# client auto-detects the server version and falls back to bare-string select options for very old Paperless instances. This server always sends the 2.14+ object shape (current Paperless-ngx has been on it for a long time).Dual-shape
DocumentNoteUserparsing (numeric ID vs. object, depending on Paperless API version) is a response-parsing nuance with no effect on this server's tool inputs/outputs, since notes are passed through as received; not specially handled either way.
Credit
Tool design, naming, parameter shapes, and the non-obvious Paperless-ngx behaviours documented above are from barryw/PaperlessMCP by Barry Walker, licensed MIT. This project reimplements that design in Python with per-caller identity passthrough as the only functional difference.
Development
uv sync --all-extras
uv run pytest -q
uvx ruff check .License
MIT — see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
Zero-setup MCP gateway securely connecting AI to your tools with authentication and workflows
OAuth-protected, read-only-by-default MCP server for provenance-labeled QuillCaddie project memory.
Hosted MCP catalog with 30 tenant-isolated browser, RAG, AI, mail and media tools.
Related MCP Servers
- AlicenseCqualityAmaintenanceEnables searching, tagging, uploading, and reading documents in Paperless-NGX, with management of tags, correspondents, document types, and custom fields via MCP tools and resources.50379 PyPI1MIT
- FlicenseAqualityBmaintenanceA privacy-first MCP server for Paperless-ngx that lets an LLM agent search, organize, tag, and reference documents without exposing full text unless explicitly requested.13-
- FlicenseAqualityCmaintenanceMCP server for Paperless-ngx document management. Enables AI models to search, retrieve, update documents and manage metadata.7-
- FlicenseNot gradedqualityCmaintenanceEnables interaction with a paperless-ngx document management system through MCP tools, allowing document search, retrieval, upload, and metadata management via natural language.-