Skip to main content
Glama
Barrow1990

bazarr-mcp-server

by Barrow1990

bazarr-mcp-server

A minimal Model Context Protocol server that connects to Bazarr (the subtitle-manager companion to Sonarr/Radarr), 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_movies

List movies Bazarr is tracking, optionally filtered by title

list_series

List TV series Bazarr is tracking, optionally filtered by title

missing_subtitles_movies

Movies with at least one wanted subtitle language missing

missing_subtitles_episodes

Episodes with at least one wanted subtitle language missing

search_movie_subtitles

Search for and download one subtitle language for a movie

search_episode_subtitles

Search for and download one subtitle language for an episode

run_wanted_search

Trigger Bazarr's background job to search all wanted subtitles for movies or series

system_status

Bazarr version/environment info and current health issues

search_movie_subtitles, search_episode_subtitles, and run_wanted_search are the only tools that change state in Bazarr (they trigger real subtitle searches/downloads). Everything else is read-only.

Related MCP server: media-stack-mcp

Note on API accuracy

Bazarr was not reachable while building this server, so the tools were written against Bazarr's own source (bazarr/api/) rather than a live instance — endpoint paths, the X-API-KEY auth header, and response shapes ({"data": ..., "total": ...} envelopes, field names like radarrId/sonarrSeriesId/missing_subtitles) are read directly out of the current master branch's Flask-RESTX route definitions. Run GET /ready (see below) and the mocked test suite is not a substitute for this — confirm against your real instance, and if a field is missing or an endpoint 404s, that's real API drift to report, not a bug in guesswork.

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 Bazarr.

200 {"status": "ok"}

(doesn't respond)

GET /ready

BAZARR_URL is reachable and BAZARR_API_KEY is accepted (via Bazarr's /system/status).

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

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

Unlike the Servarr-family servers (Sonarr/Radarr/Prowlarr), there's no API- version compatibility check here — Bazarr isn't a Servarr app and has no equivalent unauthenticated version-discovery endpoint, and its /api/... routes are unversioned.

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 the subtitle-download and wanted-search ones. 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:8933:8933) 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

BAZARR_URL

yes

e.g. http://192.168.1.50:6767

BAZARR_API_KEY

yes

Bazarr > Settings > General > Security > API Key

MCP_HOST

no

0.0.0.0

Interface the server binds to inside the container

MCP_PORT

no

8933

Port the server listens on

MCP_AUTH_TOKEN

no

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

Image

Built and pushed to ghcr.io/barrow1990/bazarr-mcp-server by .github/workflows/ci.yml on every push to main that passes tests, tagged :latest and :<commit-sha>. docker-compose.yml pulls :latest by default; swap in build: . there instead if you'd rather build locally from the Dockerfile.

The image is the same three-stage build used across this family of MCP servers: builder compiles dependencies into --target=/deps; prep starts fresh from python:3.12-alpine, drops pip/setuptools/wheel, strips stdlib pieces this headless server never touches, 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 — the step that actually drops the stripped bytes from what gets pushed, rather than just hiding them in a layered image. Expect roughly the same ~98MB floor as the other servers in this family (mcp.server.request_state unconditionally imports cryptography's AES-GCM/HKDF, so that ~15MB native extension ships regardless). Dependencies in requirements.txt are pinned to exact versions.

Running with Docker Compose

cp .env.example .env   # fill in BAZARR_URL / BAZARR_API_KEY
docker compose up -d --pull always

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

Managing with Dockhand

Point Dockhand at ghcr.io/barrow1990/bazarr-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 separately if you want to alert on Bazarr connectivity specifically rather than container liveness.

Environment variables in Dockhand: docker-compose.yml loads BAZARR_URL/BAZARR_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.

Connecting a client

Claude Code

claude mcp add bazarr -s user --transport http http://<docker-host>:8933/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": {
    "bazarr": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "http://<docker-host>:8933/mcp",
        "--header", "Authorization: Bearer <MCP_AUTH_TOKEN>"
      ]
    }
  }
}

Running without Docker

pip install -r requirements.txt
BAZARR_URL=http://192.168.1.50:6767 BAZARR_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 Bazarr (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_bazarr.pyopt-in contract tests against a real Bazarr instance, to catch drift if a Bazarr upgrade renames/removes a field these tools depend on (radarrId, sonarrSeriesId, missing_subtitles, bazarr_version, ...). Skipped by default (no Bazarr in CI); run with:

    RUN_LIVE_BAZARR_TESTS=1 BAZARR_URL=https://bazarr.example.com \
    BAZARR_API_KEY=<real key> python -m pytest tests/test_live_bazarr.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
    C
    maintenance
    Enables interaction with the *arr media management suite (Sonarr, Radarr, Lidarr, Prowlarr, SABnzbd) and TRaSH Guides through MCP tools, allowing media library management, searching, and configuration via natural language.
    70
    6 npm
    1
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP clients to interact with the arr media stack (Sonarr, Radarr, Prowlarr, Jellyseerr, Bazarr) via clean, typed tools over each app's REST API.
    1
    -
  • A
    license
    C
    quality
    A
    maintenance
    Enables management of Bazarr subtitles through all 89 API operations, including blacklists, wanted lists, provider configuration, language profiles, and Plex/Jellyfin integrations.
    89
    MIT