Skip to content

2.0

Paperless MCP moves onto the current shared platform, three template majors and three library majors ahead of where it was. Operators get unauthenticated liveness and readiness routes, a compose.yml that runs as shipped, container logs a machine can parse, and two new variables for shaping the server's instructions without replacing them wholesale. get_server_info now also reports the Paperless-NGX version installed on the instance it is connected to. Several deployment defaults changed in ways that need action, so read Upgrading before you deploy this one.

A current platform underneath

The server carried its shared runtime from fastmcp-pvl-core 4.11.3 to 7.2.0 and its project template from v5.6.3 to v8.2.0, in one step rather than in stages (#102, #108). The motivation was not the dependency itself. As #102 put it, the weekly update bot "advances one major at a time and opens a v6 pull request first, and the same server.py and README hunks re-conflict at each step." Taking the jump whole produced "one conflicted file instead of seven."

Three things that upgrade unlocks are visible to anyone running the server.

Liveness and readiness routes

Under the HTTP transport the server now answers GET /health (a static 200 for as long as the process serves) and GET /health/ready (503 when a backing check fails). One check ships today, covering the key-value store.

Both routes sit outside the MCP mount, which is also what makes them unauthenticated: they answer normally while the MCP endpoint still returns 401. Treat that as a posture change. A deployment that was fully authenticated on its published port now serves two endpoints that are not, and at the default detail level their bodies disclose the server name and version.

PAPERLESS_MCP_HEALTH_DETAIL controls how much they say. status returns the status alone; the default standard adds the server name and version on /health and a verdict per check on /health/ready; full adds a redacted reason for each check that raised and belongs only where the port is reachable from a trusted network. The shipped compose.yml and the image's own HEALTHCHECK both probe /health rather than /health/ready, deliberately, because restarting a container does not fix an unreachable backing store.

The route prefix follows the mount: PAPERLESS_MCP_HTTP_PATH has a trailing mcp segment stripped, so the default /mcp gives /health, and a mount at /paperless-mcp/mcp gives /paperless-mcp/health. The shipped probes assume the default, so a non-default mount means moving both by hand. See Docker deployment for the probe configuration.

Instructions you can shape instead of replace

Two new variables let an operator add to the generated MCP instructions rather than overwrite them. PAPERLESS_MCP_INSTANCE_DESCRIPTION carries concise routing context that distinguishes this deployment from another one a client may also have mounted. PAPERLESS_MCP_INSTRUCTIONS_EXTRA carries deployment-specific behavioural policy, appended to the generated text. Neither has a default; both are documented in Configuration.

The older PAPERLESS_MCP_INSTRUCTIONS still works and still replaces all generated text, but it is now deprecated, logs a warning at startup, and silently ignores both additive variables when it is set. If you set it only to append a sentence, the two variables above are what you actually wanted.

Generated guidance targets 1,536 UTF-16 units, reserving 512 units for operator routing and policy inside Claude Code's 2,048-unit limit. Going over warns; it never truncates and never fails startup.

A compose file that runs

The shipped compose.yml used to be a reverse-proxy stub that built from the checkout. It now pulls the published image and starts as a quick start: copy .env.example to .env and run docker compose up -d. It publishes port 8000, tolerates a missing .env, and carries a health probe.

The Traefik labels it used to ship are gone, and that is a breaking change for anyone who built on them. See Upgrading.

Knowing which Paperless you are talking to

get_server_info already reported this server's own build. It now also reports the Paperless-NGX version installed on the instance it is connected to, in the same call (#117, #121). The motivating report, quoted in #40 from a client validation write-up, describes the situation exactly:

When debugging "is the latest fix deployed?" (exactly the situation we hit ten minutes ago when a pull-without-rebuild left v0.x running), having a one-call sanity check would help.

The response gains a paperless block:

{
  "server_name": "paperless-mcp",
  "server_version": "1.0.2",
  "core_version": "7.2.0",
  "paperless": {"version": "2.14.7"}
}

That block carries identity only, and deliberately holds no update_available key: mixing identity with an update check into one field is what produced the labelling bug described below, so the update question stays with get_remote_version.

Nothing needs enabling. The version is read from the Paperless UI settings endpoint, reusing the connection the server already holds, and costs one extra request per call.

It does cost one permission. The endpoint that publishes the installed version is gated on a model permission, so the token's Paperless user needs the view permission on UI settings. A narrow service account without it gets a 403. That degrades to {"version": null} rather than failing the call, because the version is optional enrichment of a tool whose real job is reporting this server's build. Note that {"version": null} is also what you get when Paperless is down, when the URL is wrong, and when the token is rejected, so a null here means "could not ask," not "no version." Granting the permission is the fix if you want it populated; everything else the server does is unaffected either way. Tools documents the full shape.

get_remote_version now says which question it answers

get_remote_version reports the newest Paperless-NGX release published on GitHub, and whether that is newer than the connected instance. It never reported the version the instance runs. Its description said "Fetch Paperless version info," and the matching resource said it returned "the remote Paperless-NGX version," which reads as the identity of the instance you are connected to (#123, #127).

The wording was wrong in a way that hid itself: the two answers coincide while an instance is up to date, and diverge exactly when an update is pending. The tool, the remote-version://paperless resource and both documentation tables now say which question each answers and point at get_server_info for the installed version.

The behaviour did not change. The tool calls the same endpoint and returns the same fields; only the descriptions changed, so no client needs updating. A dated reference recording what each of the three Paperless version endpoints reports, and what each costs in permissions, now ships in the repository for whoever next has to reason about this.

A read-only switch that never worked is gone

PAPERLESS_MCP_READ_ONLY was documented in the README and in the example compose file as a way to disable every mutating tool at startup. No code ever read it, in any released version. As #101 puts it, "an operator who sets PAPERLESS_MCP_READ_ONLY=true today gets a fully writable server and no warning."

The documentation for it is gone (#104), and the config://paperless resource no longer reports a read_only key that was always false.

Nothing stops working, because nothing ever worked. But if you set that variable believing it protected your archive, it did not, and it has not in any version you could have installed. PAPERLESS_MCP_TOOLS_ALLOW and PAPERLESS_MCP_TOOLS_DENY are the supported way to restrict the tool surface, and they do work.

The tool reference now matches the server

The published tool reference documented 38 of the 49 registered tools. Eleven that the server had always exposed, including upload_document, get_document_thumbnail, get_document_suggestions and both remaining bulk-edit tools, were missing from it. The table also listed a create_document tool that has never existed; the real tool for that job is upload_document, and it was one of the undocumented eleven.

All 49 are now listed, the invented row is gone, and the MCP resources have a reference page of their own for the first time. No tool or resource was added, removed or renamed in this release: the server's surface is the same 49 tools and 18 resources it was before, and only the description of it changed.

Upgrading

This release changes deployment defaults. The MCP tool and resource surface is unchanged, and nothing importable from paperless_mcp changed, so clients and any downstream Python consumer are unaffected.

Refresh the lock file before you build. A stale lock file builds an image that fails at import time, because the image installs from the lock with --frozen.

If you used the shipped Traefik labels. compose.yml no longer carries them and no longer builds from the checkout. The reverse-proxy configuration moved to an overlay you copy from Docker deployment. Worth knowing while you migrate: the labels never routed as shipped, because the file declared no network for Traefik to join.

If you set PAPERLESS_MCP_PORT for a container. The image now pins --port 8000, so that variable no longer moves the listener and no error is raised. A container started with -e PAPERLESS_MCP_PORT=9000 -p 9000:9000 serves on 8000 behind a mapping pointing at a closed port. Change the host side instead, as in -p 9000:8000. Outside a container the variable still works.

If you put a hostname in PAPERLESS_MCP_HOST. It used to be interpolated into the Traefik router rule as a public name as well as being the bind address. Only the bind address remains, so a public FQDN left there now fails to start when it does not resolve to a local interface. This cannot affect the container, which pins its own host, but it does affect systemd and bare serve deployments.

If you parse the logs. The container image and the packaged systemd unit now disable Rich output, because neither stream is a terminal and Rich was wrapping each structured record across three lines at an assumed 80 columns. Anything matching the old layout, with its leading timestamp and trailing source location, matches nothing now. This is not a blanket switch to JSON: request-logging records become one JSON object per line, other library loggers become LEVEL: message, and this project's own log lines were always one line. To keep the old output in a container, set FASTMCP_ENABLE_RICH_LOGGING=true and COLUMNS=200.

One asymmetry is worth knowing: an existing .deb or .rpm install keeps the old Rich output. The package only creates /etc/paperless-mcp/env when it is absent, that file overrides the unit's own setting, and the older example enabled Rich. Fresh package installs and all container deployments get the new behaviour.

If you run Docker Compose. The shipped file needs Compose 2.24.0 or newer, and the reverse-proxy overlay needs 2.24.4 or newer. An older Compose fails to parse them.

If you read the package description. The text in server.json, on PyPI, in the MCP registry entry and on the Claude Code plugin manifest was shortened from 148 to 96 characters to fit a registry limit. The server behaves identically; only the advertised description changed.