Jellyseerr MCP Server
# ArrChestra

ArrChestra connects your MCP client to **Seerr/Jellyseerr, Sonarr, Radarr, NZBGet, SABnzbd and NZBHydra2**. You can request movies and shows, check upcoming episodes, change monitoring, and pause or retry downloads.
Each service is optional. Configure the ones you use; ArrChestra starts in read-only mode.
## What you can do
| Service | Supported operations |
| --- | --- |
| Seerr / Jellyseerr | Media search, requests and request status |
| Radarr | Movie lookup and additions, monitoring, release calendars, release searches, queue/history and import diagnostics |
| Sonarr | Series and episode lookup, season monitoring, missing episodes, upcoming airings, searches, queue/history and individual queue removal |
| NZBGet / SABnzbd | Download status, queue/history, categories, pause/resume and targeted retries |
| NZBHydra2 | Release searches and submission of a selected result to either downloader |
There are no tools for deleting library titles, purging whole queues, restarting services, changing service configuration or running scripts.
## Quick start
Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/). From a checkout:
```bash
cp .env.example .env
# Uncomment and fill in the services you want to connect.
chmod 600 .env
uv run arrchestra-mcp
```
A minimal `.env` for Sonarr:
```dotenv
SONARR_URL=https://sonarr.example.com
SONARR_API_KEY=replace-me
MCP_READ_ONLY=true
```
The server uses **stdio**. Your MCP client launches the process and owns stdin/stdout; logs go to stderr. If you use an existing Python environment, `pip install .` installs the same `arrchestra-mcp` command.
Both module entrypoints work:
```bash
python -m arrchestra_mcp
python -m jellyseerr_mcp # compatibility with earlier installations
```
## Configuration
Environment variables override `.env` in the working directory. Restart after changing settings. There is no YAML loader.
| Service | Required settings |
| --- | --- |
| Seerr / Jellyseerr | `JELLYSEERR_URL`, `JELLYSEERR_API_KEY` |
| Radarr | `RADARR_URL`, `RADARR_API_KEY` |
| Sonarr | `SONARR_URL`, `SONARR_API_KEY` |
| NZBGet | `NZBGET_URL`, `NZBGET_USERNAME`, `NZBGET_PASSWORD` |
| SABnzbd | `SABNZBD_URL`, `SABNZBD_API_KEY` |
| NZBHydra2 | `NZBHYDRA_URL`, `NZBHYDRA_API_KEY` |
Only configured services appear in your client's tool list. Configure at least one service. If a service is missing required settings, startup fails and lists what's missing. You can connect one instance of each service, including both NZBGet and SABnzbd.
Use each service's base URL, without an API suffix, credentials or query string. Paths such as `https://media.example.com/sonarr` are supported. Use HTTPS where possible; certificate verification is always enabled. SABnzbd requires its full API key, not the add-only NZB key.
| Setting | Default | Purpose |
| --- | --- | --- |
| `MCP_REQUEST_TIMEOUT` | `15` | Per-request timeout in seconds, positive and at most 300 |
| `<SERVICE>_TIMEOUT` | Shared timeout | Override for one service, including `JELLYSEERR_TIMEOUT` |
| `MCP_SEARCH_TIMEOUT` | `120` | Timeout for release/target search, which waits on every enabled indexer |
| `<SERVICE>_SEARCH_TIMEOUT` | Search timeout | Override for Radarr or Sonarr; `0` uses the ordinary request timeout |
| `LOG_LEVEL` | `INFO` | Diagnostic log level |
| `MCP_READ_ONLY` | `true` | Block mutations |
| `<SERVICE>_ALLOW_WRITES` | `false` | Permit that service's operations when read-only mode is off |
| `MCP_ALLOW_RAW_READ` | `false` | Optional legacy `GET status` tool only |
| `NZBHYDRA_ALLOWED_REDIRECT_ORIGINS` | Empty | Trusted indexer origins for NZB retrieval redirects |
Store API keys and passwords in environment variables or a protected `.env` file. Don't put them in prompts or commit them. `.env` files are ignored by Git and excluded from Docker builds.
### Allowing changes
To allow changes, turn off read-only mode and enable writes for each service you want to control:
```dotenv
MCP_READ_ONLY=false
SONARR_ALLOW_WRITES=true
RADARR_ALLOW_WRITES=true
NZBGET_ALLOW_WRITES=true
```
For Seerr, use `JELLYSEERR_ALLOW_WRITES=true`; for SABnzbd, use `SABNZBD_ALLOW_WRITES=true`. Downloading a Hydra result requires write permission for the chosen downloader.
If a write times out, it may still have succeeded. ArrChestra won't retry it automatically. Check the queue, history or command status before trying again. If a submission was accepted but its status check failed, the response still includes the job ID.
## Connecting an MCP client
For clients that use `mcpServers`:
```json
{
"mcpServers": {
"arrchestra": {
"command": "uv",
"args": ["run", "--directory", "/path/to/arrchestra", "arrchestra-mcp"],
"env": {
"SONARR_URL": "https://sonarr.example.com",
"SONARR_API_KEY": "replace-me",
"MCP_READ_ONLY": "true"
}
}
}
}
```
### Hermes
[Hermes uses `mcp_servers` in `~/.hermes/config.yaml`](https://hermes-agent.nousresearch.com/docs/reference/mcp-config-reference):
```yaml
mcp_servers:
arrchestra:
command: uv
args: [run, --directory, /path/to/arrchestra, arrchestra-mcp]
supports_parallel_tool_calls: true
```
Keep the service settings in ArrChestra's `.env`. Hermes also supports per-server `tools.include` and `tools.exclude` filters.
### OpenClaw
[OpenClaw's MCP registry](https://docs.openclaw.ai/cli/mcp/registry) uses `mcp.servers`. Set the same stdio command and arguments in that entry. `openclaw mcp probe` checks a saved server's connection and tool discovery. Consult the documentation for your installed version: runtime adapters differ, and this registry is separate from mcporter.
Hermes and OpenClaw configuration follows their documentation; end-to-end sessions with those clients aren't covered by this project's tests.
## Using ArrChestra
| Ask your client | What it uses |
| --- | --- |
| "Find Arrival in Seerr and request it." | Seerr search and request tools |
| "Which episodes in my Sonarr library air this week?" | Sonarr calendar |
| "When is the next monitored episode of Severance?" | Sonarr library lookup and next-up episodes |
| "Stop monitoring season 1 of Severance. Leave the other seasons alone." | Sonarr season monitoring |
| "Why hasn't Arrival imported? Check Radarr's queue and history." | Radarr queue, history and import diagnostics |
| "Show the SABnzbd queue, then pause the download I choose." | Downloader queue and per-job pause |
Calendars, lookups and status checks work in read-only mode. Requests, monitoring changes, searches that trigger downloads, and download controls need [write permission](#allowing-changes). `get_services` shows which services are connected and whether writes are enabled.
<details>
<summary>Tool behavior and arguments</summary>
### Requesting and adding media
`request_media` creates a request in Seerr and uses its approval workflow. TV requests default to season 1; specify `seasons` to request others.
You can also add media directly with `radarr_add_movie` or `sonarr_add_series`. These bypass Seerr. They require a quality profile and root folder from `radarr_get_options` or `sonarr_get_options`, and don't start a search automatically.
IDs depend on the operation: Seerr requests and Radarr additions use TMDB IDs; Sonarr additions use TVDB IDs. Once a title is in your library, monitoring and search tools use its Sonarr or Radarr ID. Episode tools use Sonarr episode IDs and check that they belong to the selected series.
Sonarr monitoring changes only the selected seasons or episodes. Season and episode changes require separate calls.
### Upcoming episodes and movie releases
`sonarr_get_calendar` and `radarr_get_calendar` take `start` and `end` as ISO dates or timestamps, with a maximum window of 90 days. The Sonarr calendar includes series titles. Both calendars cover the library, not a single title; results include `hasFile` to show whether the episode or movie is already downloaded.
For one show's upcoming episodes, use `sonarr_get_next_up` with its Sonarr `series_id`. It lists monitored episodes in air-date order, starting from now. Set `since` to use a different date, or `include_unmonitored=true` to include episodes you aren't monitoring.
### Searches and downloads
`radarr_search_movie` and `sonarr_search_episodes` start a search in Radarr or Sonarr, which may download a matching release. Sonarr accepts up to 100 episode IDs from one series per call. The response includes a command ID; check it with `radarr_get_command` or `sonarr_get_command` to see whether the search finished. A finished search doesn't mean the download or import is complete.
To choose a release yourself, use `radarr_get_releases` or `sonarr_get_releases`, then the matching `grab_release` tool. Sonarr requires an episode or season. These searches use indexer quota, and grabs refuse releases that are stale or rejected by the service.
Downloader tools require `backend="nzbget"` or `backend="sabnzbd"`. Pause and resume take either a `job_id` from that downloader's queue or `whole_queue=true`. Omitting both is an error. Sonarr/Radarr queue and history entries include `downloadId` for matching a download to its job. NZBGet IDs are positive integer strings; SABnzbd IDs are opaque strings.
`downloads_retry` retries one failed history item. NZBGet downloads it again and may replace the failed partial files.
`sonarr_remove_queue_item` clears one Sonarr queue entry by its queue `item_id`. By default it leaves the downloader's files alone and doesn't blocklist the release. `remove_from_client=true` also requests removal from the downloader and may delete its files; `blocklist=true` marks the release failed. Queue removal requires Sonarr write permission.
### Downloading from NZBHydra2
`nzbhydra_search` searches your indexers through Hydra. `nzbhydra_get_capabilities` lists the supported filters and categories.
To send a result to a downloader, call `nzbhydra_download_result` with its `result_id`, a `backend` and a configured downloader `category`. ArrChestra fetches the NZB and submits it to that downloader. The category's existing post-processing settings still apply.
This bypasses Sonarr/Radarr release selection. It doesn't fulfill a Seerr request or guarantee that the download will be imported into your library.
If NZB retrieval redirects to an indexer, add that indexer's origin to `NZBHYDRA_ALLOWED_REDIRECT_ORIGINS`. ArrChestra doesn't forward Hydra credentials or cookies to a different origin, follow HTTPS-to-HTTP redirects, or follow same-origin redirects outside the configured service path.
</details>
<details>
<summary>Full tool list</summary>
| Group | Tools |
| --- | --- |
| Common | `ping`, `get_services` |
| Seerr | `search_media`, `request_media`, `get_request` |
| Radarr | `radarr_find_movies`, `radarr_get_movie`, `radarr_get_options`, `radarr_get_queue`, `radarr_get_history`, `radarr_get_health`, `radarr_get_calendar`, `radarr_get_releases`, `radarr_get_command`, `radarr_add_movie`, `radarr_set_monitored`, `radarr_search_movie`, `radarr_grab_release` |
| Sonarr | `sonarr_find_series`, `sonarr_get_series`, `sonarr_get_episodes`, `sonarr_get_next_up`, `sonarr_get_missing`, `sonarr_get_calendar`, `sonarr_get_options`, `sonarr_get_queue`, `sonarr_get_history`, `sonarr_get_health`, `sonarr_get_releases`, `sonarr_get_command`, `sonarr_add_series`, `sonarr_set_monitoring`, `sonarr_search_episodes`, `sonarr_grab_release`, `sonarr_remove_queue_item` |
| Downloaders | `downloads_get_status`, `downloads_get_queue`, `downloads_get_history`, `downloads_get_categories`, `downloads_pause`, `downloads_resume`, `downloads_retry` |
| NZBHydra | `nzbhydra_get_capabilities`, `nzbhydra_search`, `nzbhydra_download_result` |
The Hydra submission tool appears only when a downloader is configured. Downloader calls always require an explicit backend.
Your MCP client's `tools/list` response includes the argument schemas. Most lists default to 25 results, up to 100 per page. Sonarr/Radarr queue, history and Sonarr missing episodes use `page_number`; other paged lists use `offset`. APIs that return a whole list are paged locally.
</details>
## Docker
```bash
docker build -t arrchestra-mcp .
docker run --rm -i --env-file .env arrchestra-mcp
```
The image runs as a non-root user. Use `-i`, not `-t`, for stdio clients. Pass credentials when the container starts; no media-service credentials are needed to build the image.
GitHub Actions builds pull requests without publishing an image. Pushes to `main`, release tags matching `v*.*.*`, and manual workflow runs publish to `ghcr.io/aserper/arrchestra` using GitHub's job token.
## Limits and compatibility
- **Stdio only.** SSE and HTTP are disabled until server authentication is implemented. Don't expose the process through an unauthenticated network bridge.
- Responses are limited to 8 MiB. NZBs/XML must be UTF-8 and stay within 4 MiB, 50,000 elements and depth 32. DTD/entities and compressed responses are refused. Large replies fail with a limit error.
- Writes to the same service run one at a time within this process. Reads can continue while a write is running. Other processes and clients can still make concurrent changes.
- Treat release titles and backend messages as data, not instructions to the agent.
Earlier Jellyseerr installations can keep using `python -m jellyseerr_mcp`, `search_media`, `request_media` and `get_request`. Requests now require write permission. The optional legacy `raw_request` tool only accepts `GET status`, without parameters or a body. `ping` returns `arrchestra-mcp` as the service name.
## Development
```bash
uv run --extra test pytest -q
```
Tests use `httpx.MockTransport` for service API calls, MCP sessions for tool behavior, and subprocesses for stdio. They cover permissions, IDs, both Hydra downloaders, redirects, response limits, timeouts and concurrent calls.
API tests target Sonarr/Radarr `/api/v3`, NZBGet v18+, SABnzbd 5.x and NZBHydra's public Newznab API. They don't check compatibility with every server release. Use disposable media and queues when testing writes against a live service.
[Implementation notes](IMPLEMENTATION_PLAN.md) cover the code structure and follow-up work. Changes to API operations should include a mock contract test.
TDQS
Scored across 5 tools
Each tool targets a distinct operation: creating a request, health check, listing services, searching, and inspecting a single request. There is no meaningful overlap between any pair.
request_media, get_services, search_media, and get_request follow a clear verb_noun pattern. The lone 'ping' is a bare verb, a minor deviation but a conventional one for a liveness check.
Five tools is a well-scoped set for a Jellyseerr integration, covering the essential search/request/inspect flow plus diagnostics without redundant entries. Every tool earns its place.
The surface covers search, create, and single-request inspection, but notable operations are missing: listing/filtering requests, cancelling or deleting a request, and approving/declining pending requests. Agents hit dead ends for anything beyond one-at-a-time request handling.