Command Palette

Search for a command to run...

UnylyUnyly
Browse all

LLMarr

FreeNot checked

An MCP server that replicates Sonarr/Radarr/Lidarr functionality driven by an LLM, enabling automated torrent management for TV shows and movies.

GitHubEmbed

About

An MCP server that replicates Sonarr/Radarr/Lidarr functionality driven by an LLM, enabling automated torrent management for TV shows and movies.

README

License: MIT Python

An MCP server that replicates the core of Sonarr/Radarr/Lidarr — but driven entirely by an LLM. It pulls metadata from a configurable provider (TMDB by default), finds torrents through Prowlarr, grabs them with a configured torrent client (qBittorrent first), notifies Plex when a download lands, and runs a background loop that auto-grabs new episodes of monitored series.

The whole point is flexibility: everything — providers, credentials, quality rules, path mappings, monitoring — is configurable through MCP tools, and a user can always hand it a raw magnet/torrent URL to grab manually.

Why an MCP server

Sonarr/Radarr are great but rigid. Exposing these primitives as MCP tools lets an LLM do the fuzzy parts (disambiguating a show, deciding which release looks right, reacting to "just grab this link") while LLMarr does the mechanical parts (search, grab, track, import).

Single-host by default, container-aware when you need it

Most people (including the author) run LLMarr, qBittorrent, and Plex on one host where they all see the same paths. That's the default — single_host: true — and it needs no path mappings: paths pass through untranslated.

For a split-container deployment, set single_host: false (via configure_server(single_host=false)) and describe how each container sees the same volume with path mappings — entries sharing a group are the same physical directory. When a download completes, the qBittorrent save path is translated into Plex's namespace before the targeted scan. In this mode an unmapped path raises instead of silently passing through.

add_path_mapping("dl", "qbittorrent", "/downloads")
add_path_mapping("dl", "plex",        "/data/torrents")
add_path_mapping("dl", "local",       "/mnt/media/dl")

Install

pip install -e .

Run

Stdio (what most MCP clients expect):

llmarr

HTTP (run it once as a persistent service a client logs into):

LLMARR_TRANSPORT=streamable-http LLMARR_HOST=0.0.0.0 LLMARR_PORT=8000 llmarr

Config is stored at $LLMARR_CONFIG (default ~/.config/llmarr/config.yaml) and the library/history/RSS state at $LLMARR_DB (default ~/.local/share/llmarr/llmarr.db).

Register with an MCP client

Stdio — the client spawns the process (no auth needed, it's local):

{
  "mcpServers": {
    "llmarr": { "command": "llmarr" }
  }
}

HTTP — point the client at the URL with the bearer token (see below):

{
  "mcpServers": {
    "llmarr": {
      "url": "http://your-host:8000/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}

Authentication (single persistent login)

stdio needs no auth — the MCP client launches LLMarr directly. The HTTP transport is protected by one static bearer token, deliberately simple for a single-user homelab service: a persistent login rather than a per-session OAuth dance.

  • On first HTTP start, if no token is set, LLMarr generates one, saves it to config, and prints it (with the URL) to stderr. The same token is reused on every restart.
  • Clients send Authorization: Bearer <token> on every request; anything else gets 401.
  • Manage it with the auth_token tool (auth_token("get"|"set"|"rotate")), or set your own value in config.yaml. Disable auth entirely (e.g. behind your own reverse proxy) with configure_server(require_auth=false).

OAuth mode (for claude.ai custom connectors / mobile apps)

claude.ai custom connectors — and therefore the Claude iOS/Android apps — authenticate with OAuth 2.1, not a static header. LLMarr can act as its own OAuth authorization server for this:

configure_server(auth_mode="oauth", public_url="https://arr.example.com")
# then restart the HTTP server

The flow keeps the single-login idea: when Claude sends you to the authorize page, you enter the same LLMarr token to approve. Under the hood LLMarr implements dynamic client registration (RFC 7591), discovery metadata (RFC 8414 + 9728), authorization-code + PKCE (S256), and refresh tokens — all signed JWTs, no external identity provider. The static token still works as a direct bearer header too, so Claude Code keeps connecting unchanged.

To add it on mobile: claude.ai → Settings → Connectors → Add custom connector, URL https://arr.example.com/mcp. Claude discovers the OAuth endpoints automatically; approve with your token. oauth_info prints the exact URLs. public_url must be set (or derivable from the request) so the issued endpoint URLs are correct.

Remote access via Cloudflare Tunnel

The HTTP server's MCP endpoint is at /mcp. Cloudflare terminates TLS and the bearer token authenticates the client, so no inbound ports are opened to the internet. What cloudflared's ingress points at depends on where it runs.

Bind address matters. The default LLMARR_HOST=127.0.0.1 only accepts connections from the same machine. Set the host to something the cloudflared process can reach.

cloudflared on the same machine

LLMARR_TRANSPORT=streamable-http llmarr            # binds 127.0.0.1:8000
cloudflared tunnel --url http://localhost:8000     # quick tunnel, or a named tunnel

cloudflared on a different host / VM (common)

Bind an interface the other host can reach, and point ingress at this host's IP — localhost in the tunnel config would resolve to the cloudflared box, not LLMarr:

LLMARR_TRANSPORT=streamable-http LLMARR_HOST=0.0.0.0 LLMARR_PORT=8000 llmarr
# cloudflared ingress (on the other VM). Replace with this host's LAN IP.
ingress:
  - hostname: llmarr.example.com
    service: http://10.0.0.10:8000
  - service: http_status:404

0.0.0.0 exposes port 8000 to the LAN — the bearer token is the protection. Tighten it by binding the specific NIC (LLMARR_HOST=10.0.0.10) and/or a firewall rule allowing only the cloudflared host, e.g. ufw allow from <cloudflared-ip> to any port 8000.

Client config

Whatever the topology, the URL is the tunnel hostname + /mcp, with the token from the server's startup banner:

{
  "mcpServers": {
    "llmarr": {
      "url": "https://llmarr.example.com/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}

First-run setup (all via tools)

The fastest path is to let the LLM drive it: setup_status returns an ordered checklist (each step done/pending with the exact next tool to call), enumerates the available metadata providers / download-client types / auth & import modes, and — once Plex is linked — lists your detected libraries with ready-to-run configure_root_folder commands. The server also ships these instructions to the client on connect, so a capable LLM will start there on its own.

  1. configure_metadata(tmdb_api_key="…")
  2. configure_prowlarr(url="http://localhost:9696", api_key="…")
  3. configure_download_client("qbit", url="http://localhost:8080", username="…", password="…", save_path="/data/downloads")
  4. Plex — either paste a token with configure_plex(url=…, token=…), or sign in via the browser: plex_login_start → open https://plex.tv/link, enter the code → plex_login_poll. Then plex_discover_libraries shows your sections + paths so you can set the right section names and root folders.
  5. configure_root_folder("tv-main", "/data/media/tv") (and a movie one)
  6. Single host? You're done — skip path mappings. Split containers? configure_server(single_host=false) then add_path_mapping(...) per namespace.
  7. test_connections() to confirm everything is reachable

Typical flow

search_series("your show")            -> pick a metadata id
add_series("<id>", seasons=[1,2])     -> library + episode list, monitored
search_releases("your show S02E01")   -> ranked torrents (quality rules applied)
grab_release(grab_url=…, series_id=1, episode_id=42)
refresh_downloads()                   -> marks completed, scans Plex

The background poller (configure_rss) does the search → grab → import loop automatically for monitored, still-missing episodes. Trigger it on demand with rss_poll_now() and inspect it with rss_status().

Import / hardlink

When a download completes, LLMarr organises it into a Sonarr/Radarr-style library instead of just scanning the download folder:

<root>/Series Title (Year)/Season 01/Series Title - S01E01 - Ep Title.mkv
<root>/Movie Title (Year)/Movie Title (Year).mkv

Files are hardlinked by default (falling back to a copy across filesystems), so the torrent keeps seeding while Plex sees a clean library. Configure with configure_import(mode="hardlink|copy|move", work_context="local", …).

Importing runs in work_context — the namespace LLMarr itself can read and write. The download's path and the destination root are both translated into that context first, so hardlinks require the download dir and the library root to be on the same filesystem there. After linking, the resulting library folder is translated into Plex's namespace and a targeted scan is fired. import_download re-runs the import for a completed grab (handy after fixing a mapping).

Movies

Movies work like series: search_moviesadd_movie (monitored) → the RSS poller auto-grabs by title+year while the movie is missing, or grab on demand with grab_movie / search_movie_releases. Completed movie downloads import and scan the Plex movie section.

Metadata providers (incl. anime)

The metadata source is pluggable per lookup:

  • tmdb (default) — TV + movies. Needs a free API key.
  • jikan — anime via MyAnimeList through a Jikan-compatible API. No API key. Per-episode titles included. Defaults to Tenrai (api.tenrai.org/v1), a 1:1 Jikan v4 mirror, as the original Jikan is being discontinued; override with configure_metadata(anime_api_url=…).

Pass provider="jikan" to search_series/add_series (or the movie variants) for a single anime lookup, or make it the default with configure_metadata(provider="jikan"). Each search result carries its own provider/provider_id, so mix sources freely in one library.

Anime episode numbering: MyAnimeList models each cour/season as one entry with absolute episode numbers, so LLMarr stores anime as season 1, episodes 1..N and flags the series as absolute-numbered. Release matching and import then understand absolute-numbered names ([Group] Show - 12 [1080p]), the Episode 12 / E12 forms, and batches/ranges ((01-28), [Batch]), so grab_episode, RSS auto-grab and hardlink import all work for anime. Absolute matching is applied only to anime series, so it can't cause false matches on ordinary TV.

Tool surface

Area Tools
Setup setup_status (guided checklist + enumerations — call first)
Config get_config, configure_metadata, configure_prowlarr, configure_download_client, remove_download_client, configure_plex, configure_root_folder, configure_quality, configure_rss, configure_import
Server / auth configure_server, auth_token, rotate_oauth_keys, oauth_info
Path maps add_path_mapping, list_path_mappings, remove_path_mapping, translate_path
Diagnostics test_connections
Series search_series, add_series, refresh_series, activate_series, bulk_activate_series, list_series, get_series, list_episodes, set_series_monitored, set_episode_monitored, remove_series
Movies search_movies, add_movie, list_movies, get_movie, set_movie_monitored, remove_movie, search_movie_releases, grab_movie
Releases search_releases, search_episode_releases, grab_release, grab_episode, grab_season
Downloads list_downloads, download_queue, get_download, refresh_downloads, import_download, remove_download (cancel)
Recovery reset_episode, reset_movie, mark_download_failed, retry_download, forget_release, clear_grab_history
Plex plex_login_start, plex_login_poll, plex_discover_libraries, import_plex_library, plex_scan
Root folders configure_root_folder, list_root_folders, remove_root_folder
RSS rss_status, rss_poll_now

Quality selection

Not full Sonarr custom formats — a lightweight, predictable heuristic (configure_quality): hard filters (ignored/required terms, min seeders, size bounds) then ranking by resolution preference, preferred terms, and seeders.

Development

pip install -e ".[dev]"
pytest

The suite is fully offline: metadata/Prowlarr HTTP is driven by httpx.MockTransport, qBittorrent/Plex are faked, and the DB, config, path mapping and importer (real hardlinks in a temp dir) are exercised directly. No credentials or running services are required.

Scope / status

Supports TV, movies, and anime via qBittorrent + Prowlarr + Plex, with metadata from TMDB (TV/movies) or a Jikan-compatible API (anime, no key). Features: hardlink/copy/move import into an organised library, season/batch packs, specials (season 0), RSS auto-grab, browser-based Plex login, importing an existing Plex library, download-queue progress + cancel, guided onboarding (setup_status), and OAuth 2.1 for claude.ai custom connectors / mobile. The provider/indexer/download-client/notifier interfaces are abstract so other clients (Transmission, Deluge) and metadata sources slot in without touching the core.

Not yet implemented: full Sonarr custom-format quality profiles, multi-episode-file (double-episode) parsing, download clients other than qBittorrent, and Lidarr-style music.

from github.com/himanshugoel2797/LLMarr

Installing LLMarr

This server has no published package — it is built from source. Open the repository and follow its README.

▸ github.com/himanshugoel2797/LLMarr

FAQ

Is LLMarr MCP free?

Yes, LLMarr MCP is free — one-click install via Unyly at no cost.

Does LLMarr need an API key?

No, LLMarr runs without API keys or environment variables.

Is LLMarr hosted or self-hosted?

Self-hosted: the server runs locally on your machine via the install command above.

How do I install LLMarr in Claude Desktop, Claude Code or Cursor?

Open LLMarr on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.

Related MCPs

Compare LLMarr with

Not sure what to pick?

Find your stack in 60 seconds

Author?

Embed badge for your README

Browse similar

All ai MCPs