CA

ct4nk3r/arrstack-mcp

Developer tools
7 stars 7 forks Quality 86 Trend 35MIT

MCP server for Sonarr, Radarr, qBittorrent & Jellyfin — give your AI assistant control over your media stack

Overview

An MCP server that gives AI assistants configurable access to homelab media and game services. Works with , , , , and any other MCP-compatible client. You can connect arrstack-mcp to OpenClaw (docs) and chat with your media stack from Discord. In this demo, OpenClaw uses a model running locally on a homelab server (, served by Ollama). 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. The result, a few moments later: Radarr is grabbing all three movies, and qBittorrent is downloading them. run arrstack-mcp over HTTP and register it, as described in 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).

README

🎬 arrstack-mcp

An MCP 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 (docs) 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). 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.

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

Connect it to your own OpenClaw: run arrstack-mcp over HTTP and register it, as described in 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).

What the bot can and can’t do: arrstack-mcp itself only exposes the tools listed under 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:

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

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

With uv installed, no clone is needed:

uvx arrstack-mcp --setup

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

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

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):

{
  "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):

{
  "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:

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.

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

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.

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:

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

OpenClaw:

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

Other HTTP MCP clients: point them at http://: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 _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; _file reads a value from a file:

    [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, _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.

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

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:

⚙️  No services configured yet. Open the setup page to pick yours:
http://: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:

  ✅ 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
prowlarr_list_applications List all configured applications (Radarr, Sonarr, Lidarr)
prowlarr_sync_applications Trigger a full indexer sync to all connected Servarr apps
prowlarr_test_applications Test connection to configured Servarr applications
prowlarr_indexer_stats Get aggregated query and grab statistics for all indexers

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
qbt_set_category Set or change the category of a torrent
qbt_list_categories List all configured categories and save paths
qbt_create_category Create a new category with optional save path
qbt_torrent_files List all files inside a torrent with size and priority
qbt_torrent_trackers Inspect trackers, seeds, peers, and status
qbt_recheck Force recheck torrent data integrity
qbt_reannounce Force reannounce torrent to all trackers
qbt_list_tags List all configured tags
qbt_add_tags Add tags to torrents
qbt_remove_tags Remove tags from torrents

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 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, a fork of Readarr that uses 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

# 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 (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). 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 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:

    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) 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:

    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 (.github.io//) can’t host the did.json at the .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 .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 .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:///.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 and server.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 to this Pages site (served at https:///.well-known/ai-catalog.json), or keep hosting on github.io and add a DNS record on your domain: _catalog._agents. 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:

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/ 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

View the full README on GitHub

Install

uvx arrstack-mcp --setup
New to MCP? Start with the mcpskill.org guide →

Configuration

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