LLMarr

LLMarr

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

Category
访问服务器

README

LLMarr

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选