@kjanat/paperless-mcp
v2.13.1
Published
MCP server for interacting with Paperless-ngx document management system.
Downloads
444
Maintainers
Readme
Paperless-ngx MCP Server
An MCP (Model Context Protocol) server for interacting with a Paperless-ngx API server. This server provides tools for managing documents, tags, correspondents, and document types in your Paperless-ngx instance.
Quick Start
Installation
Get your API token:
- Log into your Paperless-ngx instance
- Click your username in the top right
- Select "My Profile"
- Click the circular arrow button to generate a new token
Add it to your MCP client configuration (using env vars):
{ "mcpServers": { "paperless": { "command": "bunx", // or npx "args": ["@kjanat/paperless-mcp"], "env": { "PAPERLESS_URL": "http://your-paperless-instance:8000", "PAPERLESS_API_KEY": "your-api-token", }, }, }, }Or pass them as positional arguments:
{ "mcpServers": { "paperless": { "command": "bunx", // or npx "args": [ "@kjanat/paperless-mcp", "http://your-paperless-instance:8000", "your-api-token", ], }, }, }CLI args take precedence over env vars when both are provided.
That's it!
Agent Skill
This package ships an Agent Skill in
skills/paperless-ngx/ with decision trees, tool reference docs, query syntax
guide, and workflow templates for AI agents.
View the skill on the registry: https://skills.sh/kjanat/paperless-mcp/paperless-ngx
Add using:
bunx skills add kjanat/paperless-mcp --skill paperless-ngx# or npx -y skills...
Example Usage
Here are some things you can ask Claude to do:
- "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'"
Available Tools
Document Operations
get_document
Get a specific document by ID.
Parameters:
id: Document ID
get_document({
id: 123,
});update_document
Update metadata on a single document via PATCH /api/documents/{id}/, and/or
add a note (sent to POST /api/documents/{id}/notes/ under the hood). Use
bulk_edit_documents for tags, correspondent, or document type — those are
bulk-optimised on the backend.
Parameters:
id: Document IDtitle(optional): New document title (max 128 characters)archive_serial_number(optional): Archive serial number, ornullto clear itcustom_fields(optional): Array of{ field, value }pairs — replaces the document's custom field instancesnote(optional): Note text to append to the document (existing notes are kept)
update_document({
id: 123,
title: "Electricity Invoice March 2024",
note: "Renamed after inbox triage",
});search_documents
Full-text search across documents.
Parameters:
query: Search query string
search_documents({
query: "invoice 2024",
});download_document
Download a document file by ID.
Parameters:
id: Document IDoriginal(optional): If true, downloads original file instead of archived versionas_resource_link(optional): Return apaperless://resource link instead of inline base64; the client fetches the bytes viaresources/read
download_document({
id: 123,
original: false,
});bulk_edit_documents
Perform bulk operations on multiple documents.
Parameters:
documents: Array of document IDsmethod: One of:set_correspondent: Set correspondent for documentsset_document_type: Set document type for documentsset_storage_path: Set storage path for documentsadd_tag: Add a tag to documentsremove_tag: Remove a tag from documentsmodify_tags: Add and/or remove multiple tagsmodify_custom_fields: Add and/or remove custom fieldsdelete: Delete documentsreprocess: Reprocess documentsset_permissions: Set document permissionsmerge: Merge multiple documentssplit: Split a document into multiple documentsrotate: Rotate document pagesdelete_pages: Delete specific pages from a documentedit_pdf: Reorder, rotate, split, or discard PDF pagesremove_password: Remove PDF password protection
- Additional parameters based on
method:correspondent: ID for set_correspondentdocument_type: ID for set_document_typestorage_path: ID for set_storage_pathtag: ID for add_tag/remove_tagadd_tags: Array of tag IDs for modify_tagsremove_tags: Array of tag IDs for modify_tagsadd_custom_fields: Custom field IDs or id:value pairs for modify_custom_fieldsremove_custom_fields: Array of custom field IDs for modify_custom_fieldspermissions: Object for set_permissions with owner, permissions, merge flagmetadata_document_id: ID for merge to specify metadata sourcedelete_originals: Boolean for merge/splitpages: String for split ("1-2,3-4,5") or number array for delete_pages ([2, 3, 4])degrees: Number for rotate (90, 180, or 270)operations: Array of PDF edit operations for edit_pdfpassword: Password for remove_passwordupdate_document,delete_original,include_metadata: PDF edit/remove-password flags
Examples:
// Add a tag to multiple documents
bulk_edit_documents({
documents: [1, 2, 3],
method: "add_tag",
tag: 5,
});
// Set correspondent and document type
bulk_edit_documents({
documents: [4, 5],
method: "set_correspondent",
correspondent: 2,
});
// Merge documents
bulk_edit_documents({
documents: [6, 7, 8],
method: "merge",
metadata_document_id: 6,
delete_originals: true,
});
// Split document into parts
bulk_edit_documents({
documents: [9],
method: "split",
pages: "[1-2,3-4,5]",
});
// Modify multiple tags at once
bulk_edit_documents({
documents: [10, 11],
method: "modify_tags",
add_tags: [1, 2],
remove_tags: [3, 4],
});post_document
Upload a new document to Paperless-ngx. Provide the file as a local path
(file_path — preferred; the MCP server reads it directly from disk, so large
PDFs never pass through the model) or as inline base64 (file).
Parameters:
file_path: Path to a file on the machine running the MCP server (supports a leading~). Stdio transport only: the HTTP transport rejects it, since the path would resolve on the server host.file: Base64 encoded file content (alternative tofile_path; only practical for small files)filename: Name of the file — required withfile, defaults to thefile_pathbasename otherwisetitle(optional): Title for the documentcreated(optional): DateTime when the document was created (e.g. "2024-01-19" or "2024-01-19 06:15:00+02:00")correspondent(optional): ID of a correspondentdocument_type(optional): ID of a document typestorage_path(optional): ID of a storage pathtags(optional): Array of tag IDsarchive_serial_number(optional): Archive serial numbercustom_fields(optional): Array of custom field IDs
Returns a consume-task UUID, not a document ID — poll get_task with it to
find out when processing finishes and which document was created.
post_document({
file_path: "~/Downloads/invoice.pdf",
title: "January Invoice",
created: "2024-01-19",
correspondent: 1,
document_type: 2,
tags: [1, 3],
archive_serial_number: "2024-001",
});delete_document_note
Delete a note from a document. Note IDs come from the notes array on
get_document results. Returns the remaining notes.
Parameters:
id: Document IDnote_id: ID of the note to delete
delete_document_note({
id: 123,
note_id: 7,
});Tag Operations
list_tags
Get all tags. Optional name filters on a case-insensitive substring.
list_tags({ name: "invoice" });create_tag
Create a new tag.
Parameters:
name: Tag namecolor(optional): Hex color code (e.g. "#ff0000")match(optional): Text pattern to matchmatching_algorithm(optional): Integer 0-6 (0=none, 1=any, 2=all, 3=exact, 4=regex, 5=fuzzy, 6=auto)
create_tag({
name: "Invoice",
color: "#ff0000",
match: "invoice",
matching_algorithm: 5,
});update_tag
Update an existing tag's name, color, or matching rules.
Parameters:
id: Tag IDname: New tag namecolor(optional): Hex color code (e.g. "#ff0000")match(optional): Text pattern to matchmatching_algorithm(optional): Integer 0-6 (0=none, 1=any, 2=all, 3=exact, 4=regex, 5=fuzzy, 6=auto)
update_tag({
id: 5,
name: "Invoices",
color: "#00ff00",
});get_tag
Get a single tag by ID.
get_tag({
id: 5,
});delete_tag
Deprecated — use
bulk_edit_tagswithoperation: "delete"instead. Will be removed in v3.0.0.
Permanently delete a tag. Removes it from all documents.
Parameters:
id: Tag ID
delete_tag({
id: 5,
});bulk_edit_tags
Bulk set permissions or delete multiple tags.
Parameters:
tag_ids: Array of tag IDsoperation: "set_permissions" or "delete"owner(optional): User ID (for set_permissions)permissions(optional): Object with view/change user and group IDsmerge(optional): Merge with existing permissions (default false)
bulk_edit_tags({
tag_ids: [1, 2, 3],
operation: "delete",
});Correspondent Operations
list_correspondents
Get all correspondents. Optional name filters on a case-insensitive substring.
list_correspondents({ name: "acme" });get_correspondent
Get a single correspondent by ID — cheaper than listing all correspondents to
resolve a document's correspondent field.
get_correspondent({
id: 63,
});create_correspondent
Create a new correspondent.
Parameters:
name: Correspondent namematch(optional): Text pattern to matchmatching_algorithm(optional): Integer 0-6 (0=none, 1=any, 2=all, 3=exact, 4=regex, 5=fuzzy, 6=auto)
create_correspondent({
name: "ACME Corp",
match: "ACME",
matching_algorithm: 5,
});update_correspondent
Update an existing correspondent's name or matching rules via
PATCH /api/correspondents/{id}/.
Parameters:
id: Correspondent IDname(optional): New correspondent namematch(optional): Text pattern to match (empty string removes auto-matching)matching_algorithm(optional): Integer 0-6 (0=none, 1=any, 2=all, 3=exact, 4=regex, 5=fuzzy, 6=auto)is_insensitive(optional): Whether matching is case-insensitive
update_correspondent({
id: 9,
match: "ACME Corp Inc",
matching_algorithm: 3,
is_insensitive: true,
});bulk_edit_correspondents
Bulk set permissions or delete multiple correspondents.
Parameters:
correspondent_ids: Array of correspondent IDsoperation: "set_permissions" or "delete"owner(optional): User ID (for set_permissions)permissions(optional): Object with view/change user and group IDsmerge(optional): Merge with existing permissions (default false)
bulk_edit_correspondents({
correspondent_ids: [1, 2],
operation: "delete",
});Document Type Operations
list_document_types
Get all document types. Optional name filters on a case-insensitive substring.
list_document_types({ name: "invoice" });get_document_type
Get a single document type by ID — cheaper than listing all types to resolve
a document's document_type field.
get_document_type({
id: 2,
});create_document_type
Create a new document type.
Parameters:
name: Document type namematch(optional): Text pattern to matchmatching_algorithm(optional): Integer 0-6 (0=none, 1=any, 2=all, 3=exact, 4=regex, 5=fuzzy, 6=auto)
create_document_type({
name: "Invoice",
match: "invoice total amount due",
matching_algorithm: 1,
});update_document_type
Update an existing document type's name or matching rules via
PATCH /api/document_types/{id}/.
Parameters:
id: Document type IDname(optional): New document type namematch(optional): Text pattern to match (empty string removes auto-matching)matching_algorithm(optional): Integer 0-6 (0=none, 1=any, 2=all, 3=exact, 4=regex, 5=fuzzy, 6=auto)is_insensitive(optional): Whether matching is case-insensitive
update_document_type({
id: 4,
match: "invoice number",
matching_algorithm: 3,
is_insensitive: true,
});bulk_edit_document_types
Bulk set permissions or delete multiple document types.
Parameters:
document_type_ids: Array of document type IDsoperation: "set_permissions" or "delete"owner(optional): User ID (for set_permissions)permissions(optional): Object with view/change user and group IDsmerge(optional): Merge with existing permissions (default false)
bulk_edit_document_types({
document_type_ids: [1, 2],
operation: "delete",
});Storage Path Operations
list_storage_paths
Get all storage paths (where document files land on disk). Optional name
filters on a case-insensitive substring.
list_storage_paths({ name: "tax" });create_storage_path
Create a new storage path.
Parameters:
name: Storage path namepath: Path template, e.g."{{ created_year }}/{{ correspondent }}/{{ title }}"match(optional): Text pattern to matchmatching_algorithm(optional): Integer 0-6 (0=none, 1=any, 2=all, 3=exact, 4=regex, 5=fuzzy, 6=auto)is_insensitive(optional): Whether matching is case-insensitive
create_storage_path({
name: "Tax Archive",
path: "{{ created_year }}/taxes/{{ title }}",
});update_storage_path
Update an existing storage path's name, path template, or matching rules via
PATCH /api/storage_paths/{id}/.
Parameters:
id: Storage path IDname(optional): New namepath(optional): New path templatematch(optional): Text pattern to match (empty string removes auto-matching)matching_algorithm(optional): Integer 0-6is_insensitive(optional): Whether matching is case-insensitive
update_storage_path({
id: 1,
path: "{{ created_year }}/{{ correspondent }}/{{ title }}",
});bulk_edit_storage_paths
Bulk set permissions or delete multiple storage paths. Deleting a storage path does not delete documents — they fall back to the default storage location.
Parameters:
storage_path_ids: Array of storage path IDsoperation: "set_permissions" or "delete"owner(optional): User ID (for set_permissions)permissions(optional): Object with view/change user and group IDsmerge(optional): Merge with existing permissions (default false)
bulk_edit_storage_paths({
storage_path_ids: [1, 2],
operation: "delete",
});Custom Field Operations
list_custom_fields
Get all custom field definitions (optional name substring filter) — the numeric IDs that
update_document.custom_fields and bulk_edit_documents.modify_custom_fields
require.
list_custom_fields({ name: "invoice" });create_custom_field
Create a new custom field definition.
Parameters:
name: Field namedata_type: One ofstring,url,date,boolean,integer,float,monetary,documentlink,selectextra_data(optional): Extra configuration, e.g.{ select_options: [{ label: "Open" }, { label: "Paid" }] }
create_custom_field({
name: "Invoice Number",
data_type: "string",
});update_custom_field
Update an existing custom field definition via PATCH /api/custom_fields/{id}/.
Changing the data type of a field with existing values may invalidate them.
Parameters:
id: Custom field IDname(optional): New field namedata_type(optional): New data typeextra_data(optional): New extra configuration
update_custom_field({
id: 7,
name: "Invoice No.",
});delete_custom_field
Permanently delete a custom field definition via
DELETE /api/custom_fields/{id}/. Removes the field and all its values from
every document that uses it. Cannot be undone.
Single-delete only: the backend has no bulk endpoint for custom fields (unlike tags, correspondents, document types, and storage paths).
Parameters:
id: Custom field ID
delete_custom_field({
id: 7,
});Task Operations
get_task
Look up a consume task by the UUID that post_document returns. Shows
processing status and, once finished, related_document with the
resulting document ID(s).
Parameters:
task_id: Task UUID frompost_document
get_task({
task_id: "a1b2c3d4-...",
});list_tasks
List recent consumer/queue tasks, newest first. Useful when the
post_document UUID is lost, or to find failed consumptions.
Parameters:
status(optional):PENDING,STARTED,SUCCESS,FAILURE,RETRY, orREVOKEDacknowledged(optional):falsereturns tasks still visible in the Paperless tasks viewtask_name(optional): e.g.consume_file(uploads),train_classifier,check_sanitylimit(optional): max tasks returned (default 25, max 100)
list_tasks({
task_name: "consume_file",
status: "FAILURE",
});Trash Operations
list_trash
List soft-deleted documents awaiting purge. Documents land here via
bulk_edit_documents with method: "delete".
list_trash();restore_from_trash
Restore soft-deleted documents back into the archive, metadata intact.
Parameters:
documents: Document IDs to restore (fromlist_trash)
restore_from_trash({
documents: [123, 124],
});empty_trash
Permanently delete documents from the trash. This is the irreversible
step. Omit documents to purge the entire trash.
Parameters:
documents(optional): Document IDs to purge; omit for everything
empty_trash({
documents: [123],
});Mail Operations
list_mail_accounts
List mail accounts that Paperless polls for ingestion. Credentials are stripped; account setup stays in the web UI.
list_mail_accounts();process_mail_account
Trigger an immediate mail poll for one account.
Parameters:
id: Mail account ID
process_mail_account({ id: 1 });list_mail_rules
List mail rules (filters that decide which emails get imported and how).
list_mail_rules();create_mail_rule
Create a rule that imports matching emails: filter on sender/subject/body/ attachment, assign tags/correspondent/type, and control what happens to the email afterwards. Affects future ingestion runs.
Parameters (subset):
account: Mail account IDname: Unique rule namefilter_from,filter_to,filter_subject,filter_body(optional): substring filtersfilter_attachment_filename_include/_exclude(optional): wildcard patternsmaximum_age(optional): only emails younger than this many daysaction,action_parameter(optional): what happens to the email (delete, move, mark_read, flag, tag)assign_tags,assign_correspondent,assign_document_type(optional): metadata for consumed documentsenabled,order,stop_processing(optional): rule chain control
create_mail_rule({
account: 1,
name: "Vendor invoices",
filter_from: "[email protected]",
filter_subject: "invoice",
assign_tags: [12],
action: 3, // mark_read
});update_mail_rule
Modify an existing mail rule. Same fields as create, all optional, plus id.
Pause a rule with enabled: false instead of deleting it.
update_mail_rule({
id: 5,
enabled: false,
});delete_mail_rule
Permanently delete a mail rule. Affects future ingestion only; already consumed documents are untouched.
delete_mail_rule({ id: 5 });MCP Resources
Document binaries are exposed as MCP resources, so clients can fetch bytes via
resources/read instead of receiving base64 through a tool response:
paperless://documents/{id}/archive # processed/OCR version
paperless://documents/{id}/original # file as uploaded
paperless://documents/{id}/thumbnail # preview image (webp)download_document returns such a link when called with
as_resource_link: true. The thumbnail variant lets agents preview a document
without downloading it.
Error Handling
The server will show clear error messages if:
- The Paperless-ngx URL or API token is incorrect
- The Paperless-ngx server is unreachable
- The requested operation fails
- The provided parameters are invalid
Development
Want to contribute or modify the server? Here's what you need to know:
Clone the repository
Install dependencies:
bun installMake your changes in
src/(seesrc/tools/for MCP tools,src/api/for API client)Test locally:
bun src/index.ts http://localhost:8000 your-test-token
The server is built with:
- @modelcontextprotocol/sdk: Official TypeScript SDK for building MCP servers
- zod: TypeScript-first schema validation
API Documentation
This MCP server implements endpoints from the Paperless-ngx REST API. For more details about the underlying API, see the official documentation.
Running the MCP Server
The MCP server can be run in two modes:
1. stdio (default)
This is the default mode. The server communicates over stdio, suitable for CLI and direct integrations.
# via .env file (Bun loads .env automatically)
bun start
# via inline env vars
PAPERLESS_URL=http://localhost:8000 PAPERLESS_API_KEY=your-token bun start
# via positional args
bun start http://localhost:8000 your-token[!TIP] When using Bun, env vars in a
.envfile are loaded automatically — no extra setup needed. Just create a.envwithPAPERLESS_URLandPAPERLESS_API_KEYand runbun start.
2. HTTP (Streamable HTTP Transport)
To run the server as an HTTP service, use the --http flag. You can also specify the port with --port (default: 3000).
PAPERLESS_URL=http://localhost:8000 PAPERLESS_API_KEY=your-token bun start --http --port 3000With a .env file, this simplifies to bun start --http.
- The MCP API will be available at
POST /mcpon the specified port. - Each request is handled statelessly, following the StreamableHTTPServerTransport pattern.
- GET and DELETE requests to
/mcpwill return 405 Method Not Allowed.
Bind host and DNS-rebinding protection
By default the HTTP server binds to loopback (127.0.0.1), for which the MCP SDK
automatically enables Host-header validation (DNS-rebinding protection). To expose
it on another interface, set --host (or PAPERLESS_MCP_HOST):
# Bind all interfaces — pass an allowlist so protection stays on
bun start --http --host 0.0.0.0 --allowed-hosts paperless.example.com,localhost--host/PAPERLESS_MCP_HOST— interface to bind (default127.0.0.1).--allowed-hosts/PAPERLESS_MCP_ALLOWED_HOSTS— comma-separated Host-header allowlist. Set this whenever you bind a non-loopback host (e.g.0.0.0.0); otherwise the SDK applies no DNS-rebinding protection and logs a warning.- The server shuts down gracefully on
SIGINT/SIGTERM, draining in-flight requests before exiting.
Per-request Bearer auth (multi-user)
With --per-request-token (or PAPERLESS_MCP_PER_REQUEST_TOKEN=1), the server
holds no Paperless credentials. Every MCP request must carry that user's own
token, which is forwarded to Paperless as-is, so Paperless permissions stay
per-user behind one hosted MCP server:
# No token argument needed; clients authenticate themselves
paperless-mcp http://localhost:8000 --http --per-request-tokenClients send Authorization: Bearer <paperless-api-token> on each request.
Requests without it get a 401 with WWW-Authenticate: Bearer. There is no
fallback to a shared token. Run this behind TLS (a reverse proxy): tokens
travel in headers.
