Skip to main content
Glama
Barrow1990

prowlarr-mcp-server

by Barrow1990

prowlarr-mcp-server

A minimal Model Context Protocol server that connects to Prowlarr, packaged for Docker.

It runs as a standing network service (streamable-http transport, not stdio), so any MCP client on your internal network can connect to http://<host>:<port>/mcp — the container isn't spawned per-client, and container lifecycle/updates can be handed off to a tool like Dockhand.

Tools

Tool

Description

list_indexers

List indexers configured in Prowlarr, optionally filtered to only enabled ones

indexer_details

Full details for one indexer by ID

test_indexer

Trigger Prowlarr to test connectivity for an already-configured indexer

search_releases

Run a manual release search across indexers (does not grab/download anything)

system_status

Prowlarr system status and health checks

test_indexer is the only tool that reaches out and exercises a real indexer connection. Everything else is read-only. Unlike Sonarr/Radarr, Prowlarr has no library on disk, so there's no missing_*/diskspace-style tool here — it manages indexers, not downloaded media.

Related MCP server: media-stack-mcp

Health endpoints

Two plain HTTP endpoints, reachable without MCP_AUTH_TOKEN (so Docker's HEALTHCHECK, Dockhand, or any other monitor can poll them without the secret):

Endpoint

Checks

Healthy

Unhealthy

GET /health

The process is up and serving HTTP. Does not call Prowlarr.

200 {"status": "ok"}

(doesn't respond)

GET /ready

PROWLARR_URL is reachable, PROWLARR_API_KEY is accepted (via Prowlarr's /system/status), and PROWLARR_API_VERSION is still an API version Prowlarr serves (see API version checking).

200 {"status": "ok", "reachable": true, "authenticated": true, "prowlarr": {...}, "apiVersion": {...}}

503 {"status": "error", "reachable": ..., "authenticated": ..., "error": "..."}

They're split deliberately: /health is what the container's own HEALTHCHECK uses (so a transient Prowlarr outage doesn't get the container itself restarted in a loop), while /ready is for verifying config — after changing PROWLARR_URL/PROWLARR_API_KEY, curl http://<host>:8934/ready tells you plainly whether the host is reachable, the key is valid, or both.

Authentication

Set MCP_AUTH_TOKEN (a random shared secret — openssl rand -hex 32) and every request must carry Authorization: Bearer <token> or the server returns 401. This is checked by a small Starlette middleware in front of the MCP app, not the mcp SDK's built-in OAuth support (mcp.server.auth) — that machinery expects a full OAuth authorization server (issuer/resource metadata, RFC 8414/8707/9068 discovery), which is unnecessary complexity for one secret shared by trusted LAN clients.

Leave MCP_AUTH_TOKEN unset and the server runs with no auth — anything that can reach http://<host>:<port>/mcp can call every tool, including test_indexer. The server logs a warning on startup when it's running this way. Either way, the trust boundary is still the network:

  • Do not publish this port through any reverse proxy, port-forward, or anything else reachable from outside your LAN/VLAN — the bearer token protects against anyone on the network, not against the open internet.

  • Bind the compose ports: mapping to a specific internal interface (e.g. 192.168.1.50:8934:8934) rather than all interfaces, if you want to be stricter about which hosts on your network can reach it at all.

Configuration

Environment variables (see .env.example):

Variable

Required

Default

Description

PROWLARR_URL

yes

e.g. http://192.168.1.50:9696

PROWLARR_API_KEY

yes

Prowlarr > Settings > General > API Key

PROWLARR_API_VERSION

no

v1

Prowlarr REST API version to call (/api/<version>/...)

MCP_HOST

no

0.0.0.0

Interface the server binds to inside the container

MCP_PORT

no

8934

Port the server listens on

MCP_AUTH_TOKEN

no

Shared secret required as Authorization: Bearer <token>. Unset = no auth (see above)

API version checking

Prowlarr shares its underlying HTTP framework with Sonarr/Radarr (all three are Servarr apps built on the same *arr common codebase), so it exposes the same unauthenticated, unversioned GET /api endpoint reporting which API version is current and which are deprecated (e.g. {"current": "v1", "deprecated": []}). GET /ready calls it and compares it against PROWLARR_API_VERSION:

  • version matches current, or is listed under deprecated (still served, just on notice) → healthy, reported under the response's apiVersion key.

  • version isn't offered at all any more → 503, since every tool call would otherwise start failing with 404s. Bump PROWLARR_API_VERSION to match what Prowlarr now reports.

  • Prowlarr doesn't have this endpoint (very old versions) or it's unreachable → non-fatal, apiVersion: {"checked": false}.

This turns a silent break on a Prowlarr upgrade into a readiness-probe failure instead. Prowlarr's API has been on v1 since it was introduced, so in practice this is unlikely to trip — it's here for consistency with the sonarr/radarr servers and as a safety net if that ever changes.

Image

Built and pushed to ghcr.io/barrow1990/prowlarr-mcp-server by .github/workflows/ci.yml on every push to main that passes tests, tagged :latest, :<commit-sha>, and :prowlarr-<api-version> (e.g. :prowlarr-v1 — the Prowlarr API version this build targets, read out of server.py's PROWLARR_API_VERSION default so it can't drift from what the code actually calls). docker-compose.yml pulls :latest by default; swap in build: . there instead if you'd rather build locally from the Dockerfile.

The image is a three-stage build: builder compiles dependencies into --target=/deps (all of them, including cryptography's compiled cffi extension, ship musllinux wheels, so this needs no compiler even on alpine); prep starts fresh from python:3.12-alpine, drops pip/setuptools/wheel, strips stdlib pieces this headless server never touches (tkinter, idlelib, lib2to3, ensurepip, ...), adds the non-root app user, and copies in /deps and server.py; runtime then does a single COPY --from=prep / / onto a scratch base. That last step matters more than it looks — a plain RUN rm -rf only hides files still physically present in the base image's own layers underneath, so it doesn't shrink a normal layered image at all; copying the already-trimmed filesystem onto scratch is what actually drops those bytes from what gets pushed.

That takes the published image to roughly ~98MB. The floor from here is mcp's own dependency graph: mcp.server.request_state unconditionally imports cryptography's AES-GCM/HKDF (spec-mandated integrity protection for MCP's requestState, not something gated behind JWT/OAuth use), so its ~15MB native extension ships regardless. Dependencies in requirements.txt are pinned to exact versions rather than >= ranges, so a routine docker build can't silently pull in a heavier resolution than the one that was actually tested.

Running with Docker Compose

cp .env.example .env   # fill in PROWLARR_URL / PROWLARR_API_KEY
docker compose up -d --pull always

The server is then reachable at http://<docker-host>:8934/mcp from anything on your internal network.

Managing with Dockhand

Point Dockhand at ghcr.io/barrow1990/prowlarr-mcp-server and let it track new tags — this is the registry-pull model Dockhand's image-update tracking (Grype/Trivy scans, tag tracking, scheduled updates) is actually built around. The alternative, pointing Dockhand at this repo as a Git-deployed Compose stack with build: ., works too, but syncing new Git commits does not imply rebuilding the image — those are two separate steps for a build-from- source stack.

Make the GHCR package public, or every pull will need docker login ghcr.io with a PAT on each deploy host — a private package by default requires auth even to docker pull, which most homelab boxes won't have configured.

Set a restart policy of unless-stopped (already in docker-compose.yml) so Dockhand-driven restarts and host reboots bring it back up without manual intervention. The HEALTHCHECK in the Dockerfile (GET /health) drives Docker's/Dockhand's container health status; use GET /ready (see above) separately if you want to alert on Prowlarr connectivity specifically rather than container liveness.

Environment variables in Dockhand: docker-compose.yml loads PROWLARR_URL/PROWLARR_API_KEY/MCP_AUTH_TOKEN via env_file: [.env, .env.dockhand] (both optional; .env.dockhand loads second, so it wins for any key it also sets). This is deliberate — a Git-deployed stack's .env is whatever's checked out from the repo (i.e. .env.example's placeholders, since real .env is gitignored and not committed), while Dockhand writes the values you configure in its UI to .env.dockhand instead. If you set PROWLARR_URL in Dockhand's UI and the container is still using a placeholder, check that Dockhand is actually writing to .env.dockhand in the stack directory (not some other file) and that a rebuild has run since — a synced Git file change alone doesn't rebuild the image; see GET /ready to confirm what's live.

Connecting a client

Claude Code

claude mcp add prowlarr -s user --transport http http://<docker-host>:8934/mcp \
  --header "Authorization: Bearer <MCP_AUTH_TOKEN>"

(Drop the --header flag if you're running with MCP_AUTH_TOKEN unset.)

Claude Desktop

Claude Desktop's built-in config expects a locally-spawned command, so for a network server like this you'll need an HTTP-to-stdio bridge such as mcp-remote:

{
  "mcpServers": {
    "prowlarr": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "http://<docker-host>:8934/mcp",
        "--header", "Authorization: Bearer <MCP_AUTH_TOKEN>"
      ]
    }
  }
}

Running without Docker

pip install -r requirements.txt
PROWLARR_URL=http://192.168.1.50:9696 PROWLARR_API_KEY=your-api-key \
MCP_AUTH_TOKEN=your-shared-secret python server.py

Testing

pip install -r requirements-dev.txt
python -m pytest tests/ -v
  • tests/test_tools.py — each tool's logic against a mocked Prowlarr (httpx.MockTransport, no extra mocking library needed).

  • tests/test_http.py/health, /ready, and the bearer-auth middleware, via server.build_app() (the exact app __main__ runs) through Starlette's TestClient.

  • tests/test_live_prowlarr.pyopt-in contract tests against a real Prowlarr instance, to catch drift if a Prowlarr upgrade renames/removes a field these tools depend on (id, name, enable, indexerId, title, ...). Skipped by default (no Prowlarr in CI); run with:

    RUN_LIVE_PROWLARR_TESTS=1 PROWLARR_URL=https://prowlarr.example.com \
    PROWLARR_API_KEY=<real key> python -m pytest tests/test_live_prowlarr.py -v

CI (.github/workflows/ci.yml) runs the mocked suite on every push/PR; the GHCR build only runs after it passes.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    MCP server exposing Profilarr's v1 REST API as tools, enabling an LLM to inspect and manage linked databases, connected Radarr/Sonarr instances, backups, jobs, announcements, and system status.
    20
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    MCP server exposing Prowlarr's API v1 as tools for managing indexers, applications, download clients, and more, including cross-indexer searches and system commands.
    10
    MIT