Skip to main content
Glama
dannyj

sonarr-radarr-mcp

by dannyj
README.md
# sonarr-radarr-mcp

One [MCP](https://modelcontextprotocol.io) server that exposes both **Sonarr** (TV) and
**Radarr** (movies) to an AI assistant, built with [FastMCP](https://gofastmcp.com).
Look up a title, add it with your configured quality profile and root folder, kick off an
indexer search, and ask what the download queue is doing — in plain language.

Both services are optional. Each is enabled only when **both** its URL and its API key are
set; configuring exactly one of a pair is an error and the server refuses to start. At
least one service must be configured.

## Tools

### Radarr (movies)

| Tool | What it does |
| --- | --- |
| `search_movie` | Search TMDB for movies matching a title (use before adding). |
| `add_movie` | Add a movie to the library by TMDB id, optionally searching immediately. |
| `list_movies` | List library movies, optionally filtered. |
| `get_movie` | Details for one movie already in the library. |
| `search_movie_releases` | Ask Radarr to search indexers for a tracked movie. |
| `delete_movie` | Remove a movie, optionally deleting its files. |

### Sonarr (TV)

| Tool | What it does |
| --- | --- |
| `search_series` | Search TVDB for series matching a title (use before adding). |
| `add_series` | Add a series by TVDB id, optionally starting a search. |
| `list_series` | List library series, optionally filtered. |
| `get_series` | Details for one series, including per-season status. |
| `get_episodes` | List episodes for a series, optionally one season. |
| `search_season` | Search indexers for every missing episode of one season. |
| `search_episodes` | Search indexers for specific episode ids. |
| `delete_series` | Remove a series, optionally deleting its files. |

### Both (take a `service` argument: `"radarr"` or `"sonarr"`)

| Tool | What it does |
| --- | --- |
| `get_queue` | What is downloading now, and whether it is actually progressing. |
| `get_calendar` | Upcoming movie releases / episode air dates in a date window. |
| `get_history` | Recent grabs, imports, failures and upgrades. |
| `get_config` | Quality profiles and root folders the instance offers. |
| `system_status` | Reachability check plus health warnings. |

## Where to find your API keys

In each app: **Settings → General → Security → API Key**. Radarr and Sonarr have separate
keys.

## Quick start

### a) Docker Compose (homelab)

```bash
cp .env.example .env   # fill in the URLs and API keys
docker compose up -d
```

The server listens on `http://<host>:8000/mcp`.

If your *arr apps run in Docker too, put this container on their network and address them
by container name (`http://radarr:7878`) — see the comments in `docker-compose.yml`.
Otherwise use the host's LAN IP; `localhost` inside the container means the container.

Set `MCP_API_KEY` in `.env` before exposing the port beyond a trusted network — without it
the endpoint is unauthenticated, and these tools can delete media.

### b) Connect Claude Code (HTTP)

```bash
claude mcp add --transport http sonarr-radarr http://HOST:8000/mcp
```

With `MCP_API_KEY` set:

```bash
claude mcp add --transport http sonarr-radarr http://HOST:8000/mcp \
  --header "Authorization: Bearer YOUR_MCP_API_KEY"
```

### c) Local stdio (Claude Desktop)

Runs the server as a child process — no port, no bearer token needed.

```json
{
  "mcpServers": {
    "sonarr-radarr": {
      "command": "uv",
      "args": [
        "run",
        "--no-editable",
        "--directory", "/absolute/path/to/sonarr_radarr_mcp",
        "sonarr-radarr-mcp"
      ],
      "env": {
        "TRANSPORT": "stdio",
        "RADARR_URL": "http://192.168.1.10:7878",
        "RADARR_API_KEY": "...",
        "SONARR_URL": "http://192.168.1.10:8989",
        "SONARR_API_KEY": "..."
      }
    }
  }
}
```

`TRANSPORT=stdio` is required here — the default is `http`. The `env` block is also
required: **the server does not read `.env` itself.** For a manual local run, export it
first:

```bash
set -a; . ./.env; set +a
uv run --no-editable sonarr-radarr-mcp
```

## Configuration

| Variable | Required | Default | Notes |
| --- | --- | --- | --- |
| `RADARR_URL` | with `RADARR_API_KEY` | — | Base URL, no `/api/v3` suffix. Trailing `/` is stripped. |
| `RADARR_API_KEY` | with `RADARR_URL` | — | Settings → General → Security. |
| `RADARR_QUALITY_PROFILE` | no | first on instance | Profile **name**, e.g. `HD-1080p`. |
| `RADARR_ROOT_FOLDER` | no | first on instance | Path as Radarr sees it, e.g. `/movies`. |
| `SONARR_URL` | with `SONARR_API_KEY` | — | Base URL, no `/api/v3` suffix. |
| `SONARR_API_KEY` | with `SONARR_URL` | — | Settings → General → Security. |
| `SONARR_QUALITY_PROFILE` | no | first on instance | Profile **name**. |
| `SONARR_ROOT_FOLDER` | no | first on instance | Path as Sonarr sees it, e.g. `/tv`. |
| `TRANSPORT` | no | `http` | `http` (streamable HTTP at `/mcp`) or `stdio`. |
| `HOST` | no | `0.0.0.0` | HTTP transport only. |
| `PORT` | no | `8000` | HTTP transport only. |
| `MCP_API_KEY` | no | unset | When set, HTTP clients must send `Authorization: Bearer <key>`. Ignored for stdio. |

Quality profiles and root folders are configured by **name/path, not id** — ids differ
between instances. Names are resolved against the live instance at call time.

## Example prompts

- "Download the film Dune Part Three"
- "Is my download finished?"
- "Add Severance and start searching for season 2"
- "What's airing this week?"
- "Which movies in my library are still missing files?"

## Development

```bash
uv sync          # install, including dev dependencies
uv run pytest    # 105 tests
```

Tests are fully offline — HTTP is mocked with `respx`, so no live Sonarr or Radarr
instance is needed.

## CI/CD

Every push runs the test suite; pushes to `main` additionally build and publish
`ghcr.io/dannyj/sonarr-radarr-mcp:latest` (see `.github/workflows/ci.yml`). The Docker
host runs a systemd timer (`deploy/sonarr-radarr-mcp-deploy.timer`, every 10 minutes)
that pulls the image and restarts the stack only when the digest changed, then verifies
the MCP endpoint answers before declaring success (`deploy/pull-and-deploy.sh`). The
host pulls rather than CI pushing, so no inbound access or host credentials live in
GitHub. One-time host setup:

```bash
git clone https://github.com/dannyj/sonarr-radarr-mcp.git ~/Documents/repos/sonarr-radarr-mcp
cd ~/Documents/repos/sonarr-radarr-mcp && cp .env.example .env  # fill in URLs + keys
sudo cp deploy/sonarr-radarr-mcp-deploy.{service,timer} /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now sonarr-radarr-mcp-deploy.timer
```

> **macOS gotcha:** if `uv run sonarr-radarr-mcp` fails with
> `ModuleNotFoundError: No module named 'sonarr_radarr_mcp'` despite a clean `uv sync`,
> the repo is in an iCloud-synced folder (e.g. `~/Documents`): iCloud re-marks the
> editable-install `.pth` file hidden+dataless, and CPython silently skips hidden `.pth`
> files. `chflags nohidden` does not stick — iCloud re-applies the flag within a minute.
> Run with `uv run --no-editable sonarr-radarr-mcp` instead (real files, no `.pth`), as
> all examples above do. `pytest` is immune (`pythonpath = ["src"]` in `pyproject.toml`),
> and so is the Docker image.