Skip to main content
Glama
CT4nk3r

arrstack-mcp

by CT4nk3r
README.md
# 🎬 arrstack-mcp

An [MCP](https://modelcontextprotocol.io/) server that gives AI assistants configurable access to homelab media and game services.

Works with **Claude Desktop**, **Cursor**, **VS Code Copilot**, **OpenClaw**, and any other MCP-compatible client.

## Demo

### OpenClaw + a local model, from Discord

You can connect arrstack-mcp to [OpenClaw](https://github.com/openclaw/openclaw) ([docs](https://docs.openclaw.ai)) and
chat with your media stack from Discord. In this demo, OpenClaw uses a model running locally on a homelab server
(**Gemma 4 E4B**, served by [Ollama](https://ollama.com)). In the chat below, the bot finds the three Hobbit movies in Radarr and asks which ones to
download; after a short follow-up ("grab all three") they are added and the downloads start.

![Asking an OpenClaw Discord bot, backed by a local Gemma 4 E4B, to download The Hobbit trilogy](screenshots/DemoOpenClawDiscordScreenshot.png)

The result, a few moments later: Radarr is grabbing all three movies, and qBittorrent is downloading them.

![Radarr showing the three Hobbit movies as downloading](screenshots/DemoOpenClawRadarrGrabbingScreenshot.png)

![qBittorrent showing the three Hobbit torrents, tagged radarr, downloading](screenshots/DemoOpenClawTorrentsScreenshot.png)

**Connect it to your own OpenClaw:** run arrstack-mcp over HTTP and register it, as described in
[Quick Start](#quick-start) (the `OpenClaw` snippet). The demo setup runs arrstack-mcp with `--transport sse` in
Docker, on the same Docker network as OpenClaw, and registers `http://arrstack-mcp:8000/sse` with `"transport":"sse"`.
With the default Streamable HTTP transport the URL ends in `/mcp` instead (see [Transport Options](#transport-options)).

**What the bot can and can't do:** arrstack-mcp itself only exposes the tools listed under
[Available Tools](#available-tools) (search, add, queue and similar actions for your configured services). It has no
shell, file or system access, so it is not a way to run commands on your machine. What the *rest* of the bot can do
depends on your OpenClaw setup, which has its own tools. In the demo setup OpenClaw runs in a Docker container,
so it cannot reach the host's services. Asked which services are running on the machine, it tried `systemctl` and the command failed:

![The bot trying systemctl and failing](screenshots/DemoOpenClawSystemctlScreenshot.png)

This is a container boundary, not a restriction on commands: OpenClaw's own tools (for example shell commands inside
its container) are configured separately from arrstack-mcp. Review those settings before you expose a bot to anyone
else.

### Adding a movie with natural language

![Adding a movie with natural language](screenshots/DemoAddMovieScreenshot.png)

## Features

| Service | Tools |
|---------|-------|
| **Sonarr** | List series, search & add shows, upcoming episodes, download queue |
| **Radarr** | List movies, search & add movies, download queue |
| **Lidarr** | List artists, search & add artists/albums, queue, missing search |
| **Prowlarr** | List/test indexers, search releases, health check |
| **qBittorrent** | List/pause/resume/delete torrents, add magnets / .torrent files / URLs, transfer stats |
| **SABnzbd** | Queue, history, status, pause/resume, add NZB url, speed limit |
| **RDTClient** | Real-Debrid downloader: list/pause/resume/delete torrents, add magnets, provider status |
| **Jellyfin** | List libraries, recent additions, system info |
| **RomM** | System info, list platforms, list/search ROMs, game details |
| **GameVault** | List/search PC games, game details, random game, reindex library |
| **Bookshelf** | List/search authors & books, queue, missing, profiles, health |
| **Seerr** | Search movies/TV, request media (with season selection), list/check requests |

Only configure the services you use β€” unconfigured services are gracefully skipped.

## Choose Your Services

To avoid flooding an MCP client's context with tools it does not need, the
advertised tool catalog is configurable:

- `ENABLED_SERVICES=auto` (default) advertises only services with a configured URL.
- A comma-separated list such as `sonarr,radarr,romm` advertises exactly that subset.
- `ENABLED_SERVICES=all` advertises every available tool.
- Run `python server.py --list-services` to inspect configured/enabled services.

Valid service names are `sonarr`, `radarr`, `lidarr`, `prowlarr`,
`qbittorrent`, `rdtclient`, `sabnzbd`, `jellyfin`, `romm`, `gamevault`,
`bookshelf`, and `seerr`. Aliases `qbt`, `rdt`, and `sab` are also accepted.

### Agent behaviour

- `ALLOW_DESTRUCTIVE=false` hides every tool that deletes downloads or library
  files (`*_delete_queue_item`, `radarr_delete_movie_file`,
  `sonarr_delete_episode_file`, `qbt_delete`, `rdt_delete`, `sab_delete_job`).
  Default `true`.
- `INSTRUCTION_PRESETS` appends opt-in workflow guidance to the server
  instructions. Available: `hun-ncore` (Hungarian dual-audio releases from nCore:
  quality-size limits, checking the queue for HuN releases).
- `EXTRA_INSTRUCTIONS` appends your own free text to the server instructions.

## Quick Start

### 1. Guided setup (recommended)

With [uv](https://docs.astral.sh/uv/) installed, no clone is needed:

```bash
uvx arrstack-mcp --setup
```

(`pipx run arrstack-mcp --setup` or `pip install arrstack-mcp` work too.) From a
source checkout instead:

```bash
git clone https://github.com/ct4nk3r/arrstack-mcp.git
cd arrstack-mcp
python -m venv .venv && .venv/bin/pip install -e .
.venv/bin/python server.py --setup
```

The wizard asks for each service's URL and API key (keys are typed without
echo), **tests every connection as you go**, writes `arrstack.toml` (mode
600, an existing file is backed up first) and prints the exact snippet for
Claude Desktop, Cursor, VS Code, Claude Code and Docker.

- `--setup --discover` first looks for services at their usual container names
  (`sonarr`, `radarr`, `qbittorrent`, ...) and on `localhost`, including URL
  bases such as `/sonarr`, and offers what it finds as defaults. It only probes
  that fixed list, never your whole network.
- `--arr-config /path/to/sonarr-config` (repeatable) reads the URL base, port and
  API key from a Sonarr/Radarr/Lidarr/Prowlarr `config.xml`, so you don't have to
  copy keys by hand.
- `--format env --output .env` writes a Docker `env_file` instead of TOML.
- Afterwards, `python server.py --check --config arrstack.toml` re-tests
  everything (see [Check your setup](#check-your-setup)).

### 2. Connect your MCP client

With a config file, every client needs just the command and `--config`. With uv
the command is `uvx` and the arguments `["arrstack-mcp", "--config", "/path/to/arrstack.toml"]`;
from a source checkout use the venv's Python and `server.py`, as below:

**Claude Desktop / Cursor** (`claude_desktop_config.json` / `~/.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "arrstack": {
      "command": "/path/to/arrstack-mcp/.venv/bin/python",
      "args": ["/path/to/arrstack-mcp/server.py", "--config", "/path/to/arrstack-mcp/arrstack.toml"]
    }
  }
}
```

**VS Code** (`.vscode/mcp.json`):

```json
{
  "servers": {
    "arrstack": {
      "type": "stdio",
      "command": "/path/to/arrstack-mcp/.venv/bin/python",
      "args": ["/path/to/arrstack-mcp/server.py", "--config", "/path/to/arrstack-mcp/arrstack.toml"]
    }
  }
}
```

**Claude Code:**

```bash
claude mcp add arrstack -- /path/to/arrstack-mcp/.venv/bin/python /path/to/arrstack-mcp/server.py --config /path/to/arrstack-mcp/arrstack.toml
```

You can still pass settings as environment variables instead (an `"env": {...}`
block with `SONARR_URL`, `SONARR_API_KEY`, ...); see [Configuration](#configuration).

### 3. Or run it in Docker (HTTP transport)

For remote clients, or to sit next to your *arr containers:

No clone needed: the image is published at `ghcr.io/ct4nk3r/arrstack-mcp`
(amd64 + arm64).

```bash
mkdir -p arrstack-mcp/config && cd arrstack-mcp
curl -fsSLO https://raw.githubusercontent.com/ct4nk3r/arrstack-mcp/main/docker-compose.yml
docker compose up -d
docker compose logs arrstack-mcp | grep setup   # prints the setup link
```

Open the link (`http://localhost:8000/setup?token=…`): tick the services you
use, fill in their addresses and API keys, press **Test connection** on each,
then **Save & restart**. Your choices are saved to `./config/arrstack.toml`
and only the ticked services become tools. See [Setup page](#setup-page).

Prefer environment variables? `curl -fsSL …/main/.env.example -o .env`, fill it
in and `docker compose up -d`; the compose file loads `.env` when present.

**Updating:** `docker compose pull && docker compose up -d`. Image tags:
`latest` (newest release), `0.1` / `0.1.0` (pin a release line or
version), `edge` (every commit to `main`, may break). To hear about new
versions, use GitHub's **Watch β†’ Custom β†’ Releases** on this repo and read the
release notes before updating; tools like Watchtower or Diun can also watch
the image for you. The server listens on port `8000` with
Streamable HTTP at `/mcp`; start it with `--transport sse` for legacy clients
that only speak SSE (endpoint `/sse`). Inside Docker, use container names in
URLs (`http://sonarr:8989`), not `localhost`, and put `arrstack-mcp` on the same
network as your services:

```yaml
services:
  arrstack-mcp:
    # ...as in docker-compose.yml
    networks:
      - your-media-network
networks:
  your-media-network:
    external: true
```

**OpenClaw:**

```bash
openclaw mcp set arrstack '{"url":"http://arrstack-mcp:8000/mcp","transport":"streamable-http"}'
```

**Other HTTP MCP clients:** point them at `http://<host>:8000/mcp` (Streamable HTTP).

## Configuration

Every setting is an environment variable (table below). Two optional
conveniences sit on top:

- **Secret files:** any variable can be read from a file by setting
  `<NAME>_FILE`, e.g. `SONARR_API_KEY_FILE=/run/secrets/sonarr_api_key`
  (Docker/Podman secrets).
- **Config file:** put settings in a TOML file and point the server at it with
  `--config /path/arrstack.toml` or `ARRSTACK_CONFIG=/path/arrstack.toml`. One
  section per service, keys in lowercase; `<key>_file` reads a value from a file:

  ```toml
  [server]
  enabled_services = "sonarr,radarr,qbittorrent"

  [sonarr]
  url = "http://sonarr:8989"
  api_key = "your-api-key"

  [qbittorrent]
  url = "http://qbittorrent:8080"
  username = "admin"
  password_file = "/run/secrets/qbt_password"
  ```

Precedence, highest first: a non-empty environment variable, `<NAME>_FILE`, the
config file, the default. Empty variables count as unset, so a compose file
listing `SONARR_URL=` doesn't hide the config file's value. Invalid values
(unknown sections or keys, URLs without `http://`, unreadable secret files)
stop the server at startup with a list of every problem.

<!-- BEGIN CONFIG TABLE (generated from arrstack_mcp/config.py; run tests to check) -->
| Variable | Required | Description |
|----------|----------|-------------|
| `ENABLED_SERVICES` | No | `auto` (default), `all`, or a comma-separated service subset |
| `ALLOW_DESTRUCTIVE` | No | `false` hides tools that delete downloads or library files (default `true`) |
| `INSTRUCTION_PRESETS` | No | Comma-separated opt-in instruction presets (e.g. `hun-ncore`) |
| `EXTRA_INSTRUCTIONS` | No | Free text appended to the server instructions |
| `MCP_ALLOWED_HOSTS` | For HTTP/SSE | Comma-separated accepted Host headers; supports wildcard ports such as `arrstack-mcp:*` |
| `SETUP_UI` | No | Web setup page at `/setup` on the HTTP transports: `auto` (default, on) or `false` |
| `LOG_LEVEL` | No | Logging level (default: `INFO`; credentials are masked) |
| `SONARR_URL` | No | Sonarr base URL (e.g. `http://localhost:8989`) |
| `SONARR_API_KEY` | If Sonarr | Sonarr API key (Settings β†’ General) |
| `RADARR_URL` | No | Radarr base URL (e.g. `http://localhost:7878`) |
| `RADARR_API_KEY` | If Radarr | Radarr API key (Settings β†’ General) |
| `LIDARR_URL` | No | Lidarr base URL (e.g. `http://localhost:8686`) |
| `LIDARR_API_KEY` | If Lidarr | Lidarr API key (Settings β†’ General) |
| `PROWLARR_URL` | No | Prowlarr base URL (e.g. `http://localhost:9696`) |
| `PROWLARR_API_KEY` | If Prowlarr | Prowlarr API key (Settings β†’ General) |
| `QBT_URL` | No | qBittorrent Web UI URL (e.g. `http://localhost:8080`) |
| `QBT_USER` | If qBt | qBittorrent username (default: `admin`) |
| `QBT_PASS` | If qBt | qBittorrent password |
| `RDT_URL` | No | RDTClient base URL (e.g. `http://localhost:6500`) |
| `RDT_USER` | If RDT login | RDTClient username (default: `admin`) |
| `RDT_PASS` | If RDT login | RDTClient password |
| `SAB_URL` | No | SABnzbd base URL (e.g. `http://localhost:8080`) |
| `SAB_API_KEY` | If SABnzbd | SABnzbd API key (Config β†’ General β†’ API Key) |
| `JELLYFIN_URL` | No | Jellyfin base URL (e.g. `http://localhost:8096`) |
| `JELLYFIN_API_KEY` | No | Jellyfin API key (needed for libraries, recent items and library scans) |
| `ROMM_URL` | No | RomM base URL (e.g. `http://localhost:8081`) |
| `ROMM_API_TOKEN` | If RomM | RomM bearer token; alternatively use `ROMM_USER` and `ROMM_PASS` |
| `ROMM_USER` | If RomM basic auth | RomM username |
| `ROMM_PASS` | If RomM basic auth | RomM password |
| `GAMEVAULT_URL` | No | GameVault server URL (e.g. `http://localhost:8082`) |
| `GAMEVAULT_API_KEY` | If GameVault | GameVault API key (Admin panel β†’ API Keys) |
| `BOOKSHELF_URL` | No | Bookshelf base URL (e.g. `http://localhost:8787`) |
| `BOOKSHELF_API_KEY` | If Bookshelf | Bookshelf API key (Settings β†’ General) |
| `SEERR_URL` | No | Seerr / Overseerr / Jellyseerr base URL (e.g. `http://localhost:5055`) |
| `SEERR_API_KEY` | If Seerr | Seerr API key (Settings β†’ General) |
| `ARD_ENABLED` | No | Agentic Resource Discovery publishing: `auto` (default; on for HTTP transports), `true`, or `false` |
| `ARD_PUBLIC_URL` | No | Public base URL clients reach this server at (e.g. `https://arrstack.example.com`); advertises an absolute connection endpoint inside the card. Leave blank if the server is private. |
| `ARD_DOMAIN` | No | Publisher domain for the `urn:air` logical identifier (defaults to the host of `ARD_PUBLIC_URL`, else `localhost`) |
| `ARD_HOST_NAME` | No | Human-readable catalog host name (default: `arrstack-mcp`) |
| `ARD_EMBED_CARD` | No | `auto` (embed the server card inline only when `ARD_PUBLIC_URL` is unset), `true` (always embed β€” best for static hosting), or `false` (always reference it by URL) |
| `ARD_DID_WEB` | No | Opt-in `did:web` host identity (e.g. `arrstack.example.com`). Only set for a domain whose root you control and where you host a `/.well-known/did.json`; otherwise left off |
<!-- END CONFIG TABLE -->

### Setup page

On the HTTP transports the server serves a small web page at `/setup` for
picking services and entering their URLs and API keys. On first start with
nothing configured it runs in setup mode (no tools yet) and prints the link:

```text
βš™οΈ  No services configured yet. Open the setup page to pick yours:
http://<this-server>:8000/setup?token=oaeGKQZJm9N0vZxB4JbIYDq-
```

- **Find my services** looks for the usual container names (`sonarr`, `radarr`,
  `qbittorrent`, …) and `localhost`, and fills in what it finds.
- **Test connection** runs the same checks as `--check` (βœ… with the version, or
  what's wrong).
- **Save & restart** writes the config file (mode 600, the previous one kept
  as a backup) and restarts the server with the new tool list.

The link contains a one-time token that changes on every restart: only people
who can read the server log can open the page. Saved API keys are never sent
back to the browser. Values set through environment variables are shown as
locked (they would override the page). On a headless server whose port is
published on `127.0.0.1` only, open it through an SSH tunnel:
`ssh -L 8000:localhost:8000 your-server`. Turn the page off with
`SETUP_UI=false`. Without Docker, `python server.py --setup` asks the same
questions in the terminal.

### Check your setup

`python server.py --check` tests every service with the same settings the server
would use and tells you what to fix:

```text
  βœ… sonarr       Sonarr 4.0.9.2244 at http://sonarr:8989/sonarr
  ❌ radarr       credentials rejected (HTTP 401): check RADARR_API_KEY (Settings β†’ General β†’ API Key)
  ⚠️ jellyfin     Jellyfin 10.11.8 at http://jellyfin:8096; JELLYFIN_API_KEY is empty; ...
  ⏸️ lidarr       configured but excluded by ENABLED_SERVICES
  Β·  sabnzbd      not configured (set SAB_URL)
```

It catches wrong URL bases (HTTP 404), login pages in front of the API,
rejected keys, a URL pointing at the wrong app, and unreachable hosts
(`localhost` inside a container). It only makes read-only requests (plus the
qBittorrent/RDTClient login), never prints credentials, and exits with status 1
when an enabled service is broken. Add `--json` for scripts. In Docker:
`docker compose exec arrstack-mcp python server.py --check`.

## Available Tools

### Sonarr (TV Shows)

| Tool | Description |
|------|-------------|
| `sonarr_list_series` | List all series with episode counts and disk usage |
| `sonarr_get_series` | Get detailed info about a specific series |
| `sonarr_search` | Search for new shows to add |
| `sonarr_add_series` | Add a show by TVDB ID |
| `sonarr_upcoming` | Show upcoming episodes |
| `sonarr_queue` | Show current download queue |
| `sonarr_delete_queue_item` | Remove an item from the queue (optionally blocklist) |
| `sonarr_list_episode_files` | List downloaded episode files with their file IDs |
| `sonarr_delete_episode_file` | Delete an episode file so it can be re-downloaded |
| `sonarr_search_missing` | Search missing episodes for one series or all |
| `sonarr_update_series` | Change a series' quality profile or monitored status |
| `sonarr_list_quality_profiles` | List quality profiles |
| `sonarr_get_quality_definitions` | Show size limits (MB/min) per quality tier |
| `sonarr_set_quality_definition` | Change size limits for a quality tier |
| `sonarr_list_custom_formats` | List custom formats |
| `sonarr_set_quality_profile_qualities` | Enable/disable qualities or quality groups in a profile |
| `sonarr_list_tags` | List tags with how many series use each |
| `sonarr_list_series_by_tag` | List series carrying a tag (e.g. a Seerr requester tag) |
| `sonarr_set_series_tags` | Add, remove or set tags (missing tags are created) |

### Radarr (Movies)

| Tool | Description |
|------|-------------|
| `radarr_list_movies` | List all movies with download status |
| `radarr_get_movie` | Get detailed info about a specific movie |
| `radarr_search` | Search for new movies to add |
| `radarr_add_movie` | Add a movie by TMDB ID |
| `radarr_queue` | Show current download queue |
| `radarr_delete_queue_item` | Remove an item from the queue (optionally blocklist) |
| `radarr_delete_movie_file` | Delete a movie's file so it can be re-downloaded |
| `radarr_search_missing` | Search specific movies, or all monitored missing movies |
| `radarr_releases` | Interactive search: list the releases Radarr finds for a movie (filterable) |
| `radarr_grab_release` | Grab one of those releases; Radarr downloads and imports it itself |
| `radarr_update_movie` | Change a movie's quality profile or monitored status |
| `radarr_list_quality_profiles` | List quality profiles |
| `radarr_get_quality_definitions` | Show size limits (MB/min) per quality tier |
| `radarr_set_quality_definition` | Change size limits for a quality tier |
| `radarr_list_custom_formats` | List custom formats |
| `radarr_set_quality_profile_qualities` | Enable/disable qualities or quality groups in a profile |
| `radarr_list_tags` | List tags with how many movies use each |
| `radarr_list_movies_by_tag` | List movies carrying a tag (e.g. a Seerr requester tag) |
| `radarr_set_movie_tags` | Add, remove or set tags (missing tags are created) |

### Lidarr (Music)

| Tool | Description |
|------|-------------|
| `lidarr_list_artists` | List all artists with album/track counts and disk usage |
| `lidarr_get_artist` | Get detailed info about a specific artist |
| `lidarr_search` | Search for artists to add |
| `lidarr_search_album` | Search for albums in metadata |
| `lidarr_add_artist` | Add an artist by name (requires quality + metadata profile + root folder) |
| `lidarr_list_quality_profiles` | List quality profiles |
| `lidarr_list_metadata_profiles` | List metadata profiles |
| `lidarr_list_root_folders` | List root folders with free space |
| `lidarr_queue` | Show current download queue |
| `lidarr_delete_queue_item` | Remove an item from the queue (optionally blocklist) |
| `lidarr_search_missing` | Trigger search for all missing albums |

### Prowlarr (Indexers)

| Tool | Description |
|------|-------------|
| `prowlarr_list_indexers` | List all indexers with status |
| `prowlarr_test_indexer` | Test a specific indexer connection |
| `prowlarr_test_all_indexers` | Test all enabled indexers |
| `prowlarr_search` | Search across indexers for releases |
| `prowlarr_grab` | Send a result from the last search to qBittorrent |
| `prowlarr_health` | Check system health warnings |

### qBittorrent (Downloads)

| Tool | Description |
|------|-------------|
| `qbt_list_torrents` | List torrents with progress and speed |
| `qbt_torrent_details` | Get detailed torrent info |
| `qbt_add` | Add anything β€” magnet, .torrent URL, local .torrent path, or base64 β€” and start it |
| `qbt_add_magnet` | Add a magnet link (optional category / save path / paused) |
| `qbt_add_torrent_file` | Add a .torrent from a local path, http(s) URL, or base64 content |
| `qbt_pause` | Pause a torrent |
| `qbt_resume` | Resume a torrent |
| `qbt_delete` | Delete a torrent (optionally with files) |
| `qbt_transfer_info` | Global transfer statistics |

### SABnzbd (Usenet Downloads)

| Tool | Description |
|------|-------------|
| `sab_queue` | Show the current download queue |
| `sab_history` | Show download history |
| `sab_status` | Show full server status (disk, speed, etc.) |
| `sab_pause` | Pause the entire queue |
| `sab_resume` | Resume the entire queue |
| `sab_pause_job` | Pause a specific queue item by NZO id |
| `sab_resume_job` | Resume a specific queue item by NZO id |
| `sab_delete_job` | Delete a queue item (optionally with files) |
| `sab_add_url` | Add an NZB by URL (with optional category/priority) |
| `sab_speed_limit` | Set the global speed limit (0..100% of configured max) |

### RDTClient (Real-Debrid Downloader)

[RDTClient](https://github.com/rogerfar/rdt-client) is a Real-Debrid /
AllDebrid / Premiumize download manager that exposes a qBittorrent-compatible
API, so it slots into Sonarr/Radarr just like qBt.

| Tool | Description |
|------|-------------|
| `rdt_list_torrents` | List torrents with progress and speed |
| `rdt_torrent_details` | Get detailed torrent info |
| `rdt_add_magnet` | Add a magnet link to your debrid provider |
| `rdt_pause` | Pause one or more torrents |
| `rdt_resume` | Resume one or more torrents |
| `rdt_delete` | Delete one or more torrents (optionally with files) |
| `rdt_provider_status` | Show configured debrid provider (Real-Debrid / AllDebrid / etc.) |

### Jellyfin (Media Server)

| Tool | Description |
|------|-------------|
| `jellyfin_libraries` | List media libraries |
| `jellyfin_recent` | Recently added items |
| `jellyfin_system_info` | Server version and system info |
| `jellyfin_scan_library` | Trigger a library scan (needs `JELLYFIN_API_KEY`) |

### Seerr (Media Requests)

Seerr (and its predecessors Overseerr /
Jellyseerr) lets household members request media; approved requests flow into
Sonarr/Radarr.

| Tool | Description |
|------|-------------|
| `seerr_search` | Search movies and TV shows, returns TMDB IDs |
| `seerr_request_movie` | Request a movie by TMDB ID |
| `seerr_request_tv` | Request a TV show by TMDB ID, with optional season selection |
| `seerr_list_requests` | List requests, filterable by status |
| `seerr_request_status` | Get the status of a specific request |

### RomM (ROM Library)

| Tool | Description |
|------|-------------|
| `romm_system_info` | Show version, detected platforms, and metadata sources |
| `romm_list_platforms` | List platforms, ROM counts, and library sizes |
| `romm_list_games` | List or search indexed ROMs |
| `romm_get_game` | Show details for one indexed ROM |

### GameVault (PC Game Library)

| Tool | Description |
|------|-------------|
| `gamevault_list_games` | List or search PC games and installers |
| `gamevault_get_game` | Show details for one game |
| `gamevault_random_game` | Pick a random indexed game |
| `gamevault_reindex` | Scan the game-files directory for changes |

### Bookshelf (Books β€” Hardcover-flavored Readarr fork)

Bookshelf is [pennydreadful/bookshelf](https://github.com/pennydreadful/bookshelf),
a fork of Readarr that uses [hardcover.app](https://hardcover.app) as its
metadata provider. It exposes the standard Readarr v1 API, so these tools
behave like the Sonarr/Radarr/Lidarr equivalents.

| Tool | Description |
|------|-------------|
| `bookshelf_health` | Version + active health-check issues |
| `bookshelf_list_authors` | List monitored authors with book counts and disk usage |
| `bookshelf_get_author` | Detailed info for an author by ID |
| `bookshelf_search_author` | Search Hardcover for an author |
| `bookshelf_search_book` | Search Hardcover for a book |
| `bookshelf_list_books` | List all tracked books |
| `bookshelf_queue` | Current download queue |
| `bookshelf_wanted_missing` | Books flagged as missing |
| `bookshelf_list_quality_profiles` | Quality profiles |
| `bookshelf_list_metadata_profiles` | Metadata profiles |
| `bookshelf_list_root_folders` | Root folders with free space |
| `bookshelf_search_missing` | Trigger a search for all missing books |

## Transport Options

```bash
# stdio (default) β€” for Claude Desktop, Cursor, VS Code
python server.py

# Streamable HTTP β€” for Docker / remote
python server.py --transport streamable-http --port 8000

# SSE β€” legacy HTTP transport
python server.py --transport sse --port 8000
```

## Agentic Resource Discovery (ARD)

arrstack-mcp implements the **publisher** side of [Agentic Resource Discovery](https://agenticresourcediscovery.org/)
([spec](https://github.com/ards-project/ard-spec)) β€” an open standard for
publishing and discovering AI capabilities across the web. This lets ARD
registries and agents find your server and learn how to connect to it, instead
of every client needing a hand-written config.

> [!IMPORTANT]
> ARD publishes **discoverable metadata only**. It does *not* add authentication
> to this server or make it safe to expose to the internet β€” the catalog just
> describes what tools exist. The MCP endpoint still controls your media stack
> with your credentials, so keep it behind Tailscale or an authenticated proxy
> (see [Security](#security)). Discovery is not a substitute for verifying who
> you connect to.

When running an HTTP transport, the server publishes two documents, generated
live from whatever `ENABLED_SERVICES` advertises:

| Endpoint | Description |
|----------|-------------|
| `/.well-known/ai-catalog.json` | The ARD **capability manifest**. Advertises this server as a single `application/mcp-server-card+json` entry with `capabilities` (the enabled tool names), `representativeQueries` for semantic search, and a domain-anchored `urn:air` identifier (plus an optional `did:web` identity, see below). |
| `/.well-known/mcp-server-card.json` | The **MCP server card** the catalog references: every advertised tool with its `inputSchema`, plus the MCP endpoint and transport. |

Both are served with `Content-Type: application/json`. When `ARD_PUBLIC_URL` is
set (i.e. you've opted into public discovery), they also send
`Access-Control-Allow-Origin: *` so browser-based crawlers can fetch them from
any origin; on a private deployment that wildcard CORS header is omitted.
Server-side crawlers ignore CORS and work either way.

### Identity & trust

The catalog entry's `identifier` is a **domain-anchored `urn:air` URN** β€” a
stable *logical* name (e.g. `urn:air:arrstack.example.com:server:arrstack`); it
does not need to resolve. A `did:web` host identity is **opt-in**: it's only
emitted when you set `ARD_DID_WEB` to a domain whose root you control and where
you host a [DID document](https://w3c-ccg.github.io/did-method-web/) at
`/.well-known/did.json`. It is never inferred from `ARD_PUBLIC_URL`/`ARD_DOMAIN`,
so the catalog never advertises an identity that can't be verified. This
implementation does **not** include a cryptographic `trustManifest` (signing /
attestations) β€” that's an optional, enterprise-grade layer of the spec.

### Publishing

1. Set `ARD_PUBLIC_URL` to the URL clients reach this server at, e.g.
   `https://arrstack.example.com`. The catalog then advertises an absolute
   connection endpoint (override the URN namespace with `ARD_DOMAIN`).
2. Serve it over **HTTPS** on your public domain (a reverse proxy / Tailscale
   Funnel / Cloudflare Tunnel in front of port `8000`).
3. Check discovery works:

   ```bash
   curl https://arrstack.example.com/.well-known/ai-catalog.json
   ```

Startup logs print the discovery URL when ARD is enabled. Set `ARD_ENABLED=false`
to turn the endpoints off.

### Static hosting on GitHub Pages (no domain, no public server)

You don't need a custom domain **or** a publicly exposed MCP server to publish a
discoverable catalog β€” host it as a static file on GitHub Pages, anchored to the
`github.io` domain you already control. This repo ships a workflow
([`.github/workflows/ard-pages.yml`](.github/workflows/ard-pages.yml)) that
regenerates the catalog from `server.py` on every change and deploys it, so it
never goes stale.

**Setup (one-time, ~2 minutes):**

1. **Enable Pages:** repo **Settings β†’ Pages β†’ Build and deployment β†’ Source:
   "GitHub Actions"**.
2. **Flip the deploy gate on:** repo **Settings β†’ Secrets and variables β†’
   Actions β†’ Variables**, add `ARD_PAGES_ENABLED` = `true`. (The deploy job is
   skipped until you do this, so the workflow stays green beforehand β€” GitHub's
   token can't enable Pages for you, so this is an explicit opt-in.)
3. **Run it:** push to `main` (or **Actions β†’ Publish ARD catalog β†’ Run
   workflow**). The catalog goes live at:

   ```
   https://ct4nk3r.github.io/arrstack-mcp/.well-known/ai-catalog.json
   ```

4. **Verify:**

   ```bash
   curl -L https://ct4nk3r.github.io/arrstack-mcp/.well-known/ai-catalog.json
   ```

Before you complete steps 1–2, the workflow still runs and stays green: the
**Build ARD catalog** job generates and validates the catalog, and the deploy
job is simply skipped.

With no configuration, the publisher identity defaults to your Pages domain β€”
`urn:air:ct4nk3r.github.io:server:arrstack`. This matches the ARD spec's "solo
developer" examples (which use identifiers like `urn:air:github.com:alice-dev:...`).
GitHub Pages serves the file over HTTPS with `Content-Type: application/json` and
`Access-Control-Allow-Origin: *`, satisfying the ARD hosting requirements, and
the manifest embeds the full server card inline so it's self-contained.

> [!NOTE]
> The default identity is the `urn:air` URN only β€” a stable logical name, not a
> resolvable address. A `did:web` identity is **not** advertised by default,
> because a project Pages site (`<owner>.github.io/<repo>/`) can't host the
> `did.json` at the `<owner>.github.io` root that `did:web` resolution expects.
> Only set `ARD_DID_WEB` if you serve a DID document at that domain's root (e.g.
> via a `<owner>.github.io` user site or a custom domain).

**Optional repo variables** (Settings β†’ Secrets and variables β†’ Actions β†’
Variables) let you override the defaults:

| Variable | Default | Purpose |
|----------|---------|---------|
| `ARD_DOMAIN` | `<owner>.github.io` | `urn:air` publisher namespace (a logical name). Set this if you later get a custom domain. |
| `ARD_PUBLIC_URL` | _(none)_ | Where your MCP server actually runs, advertised inside the card. Leave blank if it's private (e.g. Tailscale-only). |
| `ARD_HOST_NAME` | `arrstack-mcp` | Friendly catalog host name. |
| `ARD_DID_WEB` | _(none)_ | Opt-in `did:web` identity; only set for a domain whose root serves a `/.well-known/did.json`. |

### Getting it indexed by registries

Hosting makes the catalog _reachable_; registries still have to find it. Without
a custom domain you have two routes:

- **Direct fetch / manual submission (works now).** Any agent or registry you
  give the URL above can fetch and index it immediately β€” the spec explicitly
  supports bypassing search and fetching a known catalog directly. Many
  registries also let you submit a catalog URL for crawling.
- **Automatic `.well-known` discovery (optional, still no purchase).** Crawlers
  that probe `https://<domain>/.well-known/ai-catalog.json` expect it at a domain
  _root_. To get that for free, create a GitHub **user site** β€” a repo named
  `ct4nk3r.github.io` β€” and host the same files there, so the catalog sits at
  `https://ct4nk3r.github.io/.well-known/ai-catalog.json` (root, not a
  subpath). Copy [`.github/workflows/ard-pages.yml`](.github/workflows/ard-pages.yml)
  and [`server.py`](server.py)/[`ard.py`](ard.py) into that repo, or just commit
  the generated `ai-catalog.json` from the manual command below.

> [!TIP]
> If you _do_ get a domain later, you can either add a [custom domain](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site)
> to this Pages site (served at `https://<your-domain>/.well-known/ai-catalog.json`),
> or keep hosting on `github.io` and add a DNS record on your domain:
> `_catalog._agents.<your-domain>  TXT  "url=https://ct4nk3r.github.io/arrstack-mcp/.well-known/ai-catalog.json"`.

### Generating the files manually

To host elsewhere (another repo, S3, a CDN, your own server), generate the
documents yourself:

```bash
ARD_DOMAIN=ct4nk3r.github.io ARD_EMBED_CARD=true \
  python server.py --print-catalog > ai-catalog.json
ARD_DOMAIN=ct4nk3r.github.io \
  python server.py --print-server-card > mcp-server-card.json
```

`ARD_EMBED_CARD=true` makes `--print-catalog` embed the server card inline so the
manifest is self-contained (the default `auto` only embeds when `ARD_PUBLIC_URL`
is unset). See [`examples/`](examples/) for sample output.

## Finding Your API Keys

- **Sonarr**: Settings β†’ General β†’ API Key
- **Radarr**: Settings β†’ General β†’ API Key
- **Lidarr**: Settings β†’ General β†’ API Key
- **Prowlarr**: Settings β†’ General β†’ API Key
- **qBittorrent**: Settings β†’ Web UI β†’ Authentication
- **SABnzbd**: Config β†’ General β†’ API Key
- **RDTClient**: Settings β†’ General β†’ Authentication (or set
  `Authentication: None` to allow open access on a trusted network)
- **Jellyfin**: Dashboard β†’ API Keys β†’ Add
- **RomM**: User profile β†’ API Tokens, or configure `ROMM_USER` and `ROMM_PASS`
- **GameVault**: Admin panel β†’ API Keys
- **Bookshelf**: Settings β†’ General β†’ API Key (same as Readarr)
- **Seerr**: Settings β†’ General β†’ API Key

## Security

The HTTP/SSE transports listen on `0.0.0.0:8000` by default, and MCP does not
provide authentication by itself. Anyone who can reach that port can invoke
tools using the configured service credentials.

- Prefer stdio for same-machine clients.
- The bundled `docker-compose.yml` publishes port `8000` on `127.0.0.1` only.
  Other containers on the same Docker network can still reach it by name.
- `qbt_add` / `qbt_add_torrent_file` only read local files whose names end in
  `.torrent`. They will still fetch any http(s) URL you (or the model) give them,
  including hosts on your LAN, because Prowlarr download links live there.
- For remote access, restrict port `8000` to Tailscale or place it behind an
  authenticated reverse proxy.
- DNS-rebinding protection is enabled. Set `MCP_ALLOWED_HOSTS` to the exact
  hostnames or IP addresses clients use, with optional wildcard ports:
  `localhost:*,127.0.0.1:*,arrstack-mcp:*,100.64.0.1:*`.
- The Docker image runs as non-root user `appuser` with UID `1000`.
- Credentials are kept out of logs and tool output: request URLs are not
  logged (some APIs put the key in the query string), and `apikey=`-style
  values plus every configured key/password (6+ characters) are masked as `***`.

## License

MIT