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.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues