How it works¶
paperless-mcp downloads the OpenAPI spec from your Paperless-ngx, turns its operations into MCP tools, and forwards each call to Paperless with an API token: either the one the client sent, or, with OAuth, the one it fetched for that user at login.
flowchart LR
app["App client<br>OAuth login"]
headless["Headless client<br>API token"]
lan["Client on a trusted LAN<br>API token"]
proxy["Reverse proxy<br>TLS"]
subgraph host["Docker host"]
mcp["paperless-mcp<br>:8081 /mcp"]
oauth[("mcp-oauth<br>encrypted")]
paperless["Paperless-ngx<br>:8000 /api"]
data[("documents")]
end
app -- HTTPS --> proxy
headless -- HTTPS --> proxy
lan -- HTTP --> mcp
proxy --> mcp
mcp -- OAuth logins --> oauth
mcp -- "OpenAPI spec, at startup" --> paperless
mcp -- REST API --> paperless
paperless --> data
classDef accent fill:#98D486,stroke:#2f7a27,color:#10200c,stroke-width:1.5px
class mcp accent
paperless-mcp runs as a container sidecar and talks to Paperless over the internal Docker network. Clients reach it through a TLS-terminating reverse proxy or directly over a trusted LAN.
Tools from the OpenAPI spec¶
At startup the server downloads the spec from PAPERLESS_URL/api/schema/, trims it, and generates one tool per remaining operation with FastMCP.from_openapi.
sequenceDiagram
autonumber
actor O as Container runtime
box rgba(76, 165, 62, 0.1)
participant M as paperless-mcp
end
participant P as Paperless-ngx
O->>M: Start
M->>P: GET /api/schema/?format=json
Note over M,P: Retried while Paperless boots,<br>up to PAPERLESS_SPEC_WAIT seconds
P-->>M: OpenAPI spec, about 160 operations
M->>M: Read the API version<br>from info.version
M->>M: Keep the enabled toolsets,<br>drop credential operations and PUT
M->>M: from_openapi → 80 tools<br>+ 4 written by hand
Note over M: Download failed:<br>use the bundled snapshot.
M->>M: Serve /mcp behind the<br>auth-mode gate
The trimming is what makes the result usable:
- Toolsets. Only the operations under the enabled path prefixes are kept, see Tools.
- Never tools. Operations that mint or replace the API token, or change how the account logs in, are dropped in every configuration.
PUTnext toPATCHis dropped: the partial update does the same job.- Small repairs. Request bodies are reduced to JSON, the
fieldsparameter is sent comma-separated, and a stray queryidon the notes endpoints is removed, because the spec taken literally produces requests Paperless can't read. - Output schemas are cleared. Paperless answers with objects, bare arrays, strings and empty bodies; a declared schema would make strict clients reject calls that succeeded.
Four operations need real code and are replaced by hand-written tools of the same name: the multipart upload (documents_post_document_create) and the three that return a file (documents_download_retrieve, documents_preview_retrieve, documents_thumb_retrieve).
API token¶
The client's token passes through unchanged. paperless-mcp stores nothing.
sequenceDiagram
autonumber
participant C as MCP client
box rgba(76, 165, 62, 0.1)
participant M as paperless-mcp
end
participant P as Paperless-ngx
C->>M: Tool call<br>Authorization: API token
M->>P: Authorization: Token …<br>Accept: version=N
P->>P: Check token and<br>the user's permissions
P-->>M: JSON
M-->>C: Tool result
Note over M: Holds no secret. The client's<br>token is the only credential.
In detail: tool call in token mode
The middleware rejects any request without an Authorization header. The token is carried per request, in a context variable, from the middleware to the outgoing API call, where whatever prefix the client used (Bearer, Token, or none) is replaced by the Token scheme Paperless expects.
sequenceDiagram
autonumber
participant C as MCP client
box rgba(76, 165, 62, 0.1) paperless-mcp
participant MW as TokenCapture<br>Middleware
participant F as FastMCP<br>tools
participant H as httpx +<br>PaperlessTokenAuth
end
participant P as Paperless-ngx
Note over C,P: Missing token
C->>MW: POST /mcp, no Authorization
MW-->>C: 401 WWW-Authenticate: Bearer
Note over C,P: Tool call
C->>MW: POST /mcp<br>Authorization: token
MW->>MW: Stash token in<br>_incoming_auth
MW->>F: Forward request
F->>H: Call the mapped<br>API operation
H->>H: Read token,<br>strip its prefix
H->>P: /api/…<br>Authorization: Token …
P->>P: Validate token
P-->>H: JSON
H-->>F: Response
F-->>MW: Tool result
MW->>MW: Reset context variable
MW-->>C: Tool result
OAuth¶
A one-time browser login fetches the user's API token. After that, the client only holds opaque OAuth tokens that map to it.
sequenceDiagram
autonumber
participant C as MCP client
box rgba(76, 165, 62, 0.1)
participant M as paperless-mcp
end
participant P as Paperless-ngx
Note over C,P: Once per client
C->>M: Browser login with the<br>Paperless username and password
M->>P: POST /api/token/
P-->>M: The user's API token<br>(created now if there was none)
M-->>C: OAuth access + refresh token
Note over C,P: Every tool call
C->>M: Tool call<br>Authorization: Bearer access token
M->>P: Mapped API token, to /api
P-->>M: JSON
M-->>C: Tool result
Note over M: Stores the API token encrypted.<br>The client never sees it.
In detail: login (discovery, registration, credentials, code exchange)
The client discovers the OAuth endpoints from the 401, registers itself, and sends the user to the login page. The credentials are exchanged once for the user's API token; the client only ever holds opaque pmcp_ tokens that map to it.
sequenceDiagram
autonumber
actor U as User
participant C as MCP client
box rgba(76, 165, 62, 0.1) paperless-mcp
participant A as OAuth<br>provider
participant S as mcp-oauth<br>store
end
participant P as Paperless-ngx
Note over U,P: Discovery and registration
C->>A: POST /mcp, no token
A-->>C: 401 with resource metadata
C->>A: GET /.well-known/…<br>POST /register
A->>S: Save client
A-->>C: client_id
Note over U,P: Login
C->>A: GET /authorize (PKCE)
A->>S: Pending login, 10 min
A-->>U: Redirect to /login
U->>A: POST /login<br>username, password, 2FA code
A->>P: POST /api/token/
P-->>A: API token
Note over A: The password is used once,<br>never stored or logged.
A->>S: Auth code → API token
A-->>C: Redirect with code
Note over U,P: Code exchange
C->>A: POST /token<br>code + verifier
A->>S: pmcp_ tokens → API token
A-->>C: Access token (1 h)<br>refresh token (30 d)
An account that signs in to Paperless through single sign-on has no password to exchange. It pastes its API token into the login page instead, and the server checks it against GET /api/profile/ before issuing anything.
In detail: tool call in oauth or both mode
verify_token maps a pmcp_ token to its API token. In both mode any other token is passed through as a raw API token, and an expired pmcp_ token gets a 401 so the client refreshes.
sequenceDiagram
autonumber
participant C as MCP client
box rgba(76, 165, 62, 0.1) paperless-mcp
participant F as FastMCP<br>auth + tools
participant A as verify_token
participant H as httpx +<br>PaperlessTokenAuth
end
participant P as Paperless-ngx
C->>F: POST /mcp<br>Bearer pmcp_ token or raw API token
Note over F: both mode: a raw or "Token …" header<br>is rewritten to "Bearer …" first
F->>A: verify_token(token)
A->>A: Look up in the<br>mcp-oauth store
alt Issued by us (pmcp_)
A-->>F: paperless_token = mapped token
else Not ours, both mode
A-->>F: paperless_token = the raw token
else Expired or revoked, or oauth mode
A-->>F: None
F-->>C: 401, client refreshes or logs in
end
F->>H: Call the mapped<br>API operation
H->>H: Read paperless_token<br>from the access token
H->>P: /api/…<br>Authorization: Token …
P-->>H: JSON
H-->>F: Response
F-->>C: Tool result
In detail: refresh and revoke
Refreshing rotates the OAuth token pair onto the same API token. Revoking drops the pair and nothing else: Paperless has one API token per user, shared by all of that user's clients, so it is left alone.
sequenceDiagram
autonumber
participant C as MCP client
box rgba(76, 165, 62, 0.1) paperless-mcp
participant A as OAuth<br>provider
participant S as mcp-oauth<br>store
end
Note over C,S: Refresh, when the access token expires
C->>A: POST /token<br>grant_type=refresh_token
A->>S: Drop the old pair
A->>S: New pmcp_ pair → same API token
A-->>C: New access + refresh token
Note over C,S: Revoke, to disconnect this client
C->>A: POST /revoke
A->>S: Drop the pair
Note over S: The API token in Paperless<br>is not touched.
Startup errors and health¶
If the auth configuration is invalid, or a pinned spec can't be loaded, the server serves a startup_error-only tool list instead of crashing, so the client shows the reason. GET /health always answers 200 ok without authentication, for the container healthcheck.