MediaHub
Integrates with Plex to read library and watch history, show what is currently playing, and trigger rescans after deletes or undo operations.
Integrates with qBittorrent to monitor downloads and swarm health, recheck or resume stuck torrents, and swap dead torrents for better releases.
Integrates with Radarr to manage movie requests, inspect library and queue state, search and grab releases, remove and blocklist titles, and rescan or re-monitor movies.
Integrates with Sonarr to manage TV requests, inspect library and queue state, search and grab releases, remove and blocklist titles, and rescan or re-monitor seasons.
Uses Telegram as an optional alert channel for notifications and for sending one-time confirmation codes required by guarded delete operations.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
MediaHub
A home media stack you run by talking to an agent. One MCP server is the only interface: request a show, see what's downloading, fix a stuck torrent, get recommendations, free disk space, all by asking Claude Code, Codex, or a chat bot that drives one. A deterministic loop keeps downloads moving in the background, and an optional alert channel (Telegram or a webhook) tells you the few things it can't fix.
Agent (Claude Code / Codex / Pi / a chat bot) ── media skill
│ MCP over HTTP (bearer token), 127.0.0.1:8000/mcp
▼
mediahub container ── watch loop (every 2 min) ── alerts (optional)
▼
Plex · Sonarr · Radarr · Prowlarr · FlareSolverr · qBittorrent · BazarrInstall
Point your coding agent at this repo (https://github.com/bartholomewtj/mediahub-mcp) and say "install MediaHub". It follows
AGENTS.md and then INSTALL.md: checks your machine, asks you a
handful of questions in one go (media folder, timezone, VPN, alerts, delete mode, limits),
starts the stack, wires the apps together, connects your agents and verifies the result.
Secrets (Telegram token, VPN login) you paste into a file yourself, never into the chat.
Later: "update MediaHub" (UPDATE.md: backup, new release, settings check,
verify) and "uninstall MediaHub" (UNINSTALL.md: keeps your media and, unless
you say otherwise, your data).
Requirements
Linux with Docker Engine and Docker Compose 2.24.4 or newer, or Windows 10/11 with Docker Desktop (WSL 2 backend). No macOS in v0.1.
A disk for media with plenty of room: requests stop when free space would fall below 50 GB (
DISK_GUARD_GB).An MCP client: Claude Code, Codex, or any client that speaks MCP over HTTP with a bearer header. Optionally a chat bot that runs headless Claude Code on the same machine.
Your own indexers (you add them in Prowlarr; MediaHub ships none and names none).
Optional: a Plex account, a VPN provider supported by gluetun, a Telegram bot for alerts.
What you get
Full mode (default): MediaHub plus its own Sonarr, Radarr, Prowlarr, FlareSolverr, qBittorrent (optionally behind a gluetun VPN), Plex and Bazarr, wired to each other by
setup wire. Or core mode: MediaHub alone in front of apps you already run. An existing stack can be imported instead (the installer's REPLACE path keeps your library, history and Plex watch history, and stops but never deletes the old apps).19 MCP tools (below) and the
mediaskill that teaches an agent to use them.The watch loop: swaps dead torrents, rechecks nearly-done ones, retries requests that never grabbed, and alerts you about the rest.
A read-only status page, health checks, backup and verify scripts for both OSes.
Safety model
Local only. Everything listens on
127.0.0.1(Plex also on your LAN, port 32400), and the MCP needs a bearer token. Remote access is your decision, never the installer's:docs/connect-remote.md.Secrets stay in files in
MEDIAHUB_HOME(default~/.mediahub), outside the repo. Setup generates and harvests them and prints key names only.Stranger-written text is fenced. Release names and error text are data, never instructions or parameters.
Deletes are gated. A confirm form in Claude Code; elsewhere a bounded mode (cleanup candidates only for deletes). Deleting anything else (
delete_any) needs a one-time code sent to your alert channel unless you opt out. Every delete can be undone for 7 days.Bounded actions. Disk guard, requests per hour, grabs per title.
VPN is optional. Without it your internet provider can see the torrent traffic; the installer says so plainly.
Docs
Entry point and hard rules for any agent | |
The playbooks agents follow | |
Reaching MediaHub from a phone or another machine (your choice, your risk) | |
What changed in each version |
Related MCP server: Arr Suite MCP Server
Design
MCP is the only interface. Agents act on your media through the tools the server registers; nothing else (and no direct *arr, Plex or qBittorrent API call) is part of the contract.
The watch loop is deterministic. Every 2 minutes, no LLM: it retries, swaps and flags stuck downloads by fixed rules, and alerts you only for what it cannot fix.
Untrusted text is fenced. Release names and *arr error text arrive inside an
untrustedobject, cleaned and truncated, and never go into alerts or tool parameters.Ids are server-issued. Actions take ids the server handed out (like
t:radarr:17), re-checked against live state on every call.Destructive actions are bounded. Elicitation form in Claude Code; elsewhere cleanup candidates only for deletes, 5 remove+blocklists a day, and a 7-day recycle bin you can undo from.
Safe defaults. Everything binds to
127.0.0.1, secrets stay in files outside the repo, and guards (disk, request and grab limits, confirmation codes) are on unless you turn them off.
Using it
Ask the agent. The media skill (skill/SKILL.md; the installer links it into your agent's
skills folder) tells it how. Examples:
"Get me season 1 of Night Shift Nine."
"What's downloading? Anything stuck?"
"Recommend something — poll me on what I've watched first."
"I need 100 GB back."
The tools
Tool | What it does | Gate |
| Services, media disk free, downloads with swarm health, playing now, watch-loop actions | — |
| What needs attention, with the actions that apply | — |
|
| — |
| Look up by title or | — |
| Add to Radarr/Sonarr and search. TV needs explicit seasons | disk guard, 5/hour |
| Filtered, scored candidates for a stuck item | — |
| Download one of them (replacing the stuck one) | 2 per item, disk guard |
| Everything on the stack and in Plex: size, watched state, taste rating, seasons, part-way items, downloads, stalled ones; | — |
| Watch history, the taste ledger (+ recent rating changes), record a poll answer |
|
| Change a rating ( | only a title the owner names; every change logged in |
| What could go: watched, not in 14 days, not loved, nothing part-way | — |
| Destructive | Claude Code: the owner confirms in a form. Elsewhere: bounded (below) |
| Delete any title or named seasons, candidate or not | a reason, not while playing, a one-time code by default (see below) |
| Every delete in 30 days, each with its | — |
| Put a delete back: files out of the recycle bin, title back in Sonarr/Radarr, rescan | within 7 days, once |
Trust rules (enforced in the server)
Stranger-written text is fenced. Release names and *arr error text come back inside
untrusted, cleaned and truncated, and are never accepted as parameters.Server-issued ids only. Actions take ids like
q:sonarr:<hash>ort:radarr:17, re-checked against live state on every call.Destructive actions. In interactive Claude Code,
deleteandremove_blocklistuse MCP elicitation: the owner ticks a Confirm form the model can't answer. Everywhere else — a chat bot running headless Claude Code, Codex, Pi — they run in bounded mode: deletes only for cleanup candidates (watched, 14 days, not loved, nothing part-way), at most 5 remove+blocklists per 24 hours, deleted files kept 7 days in the recycle bin (deleted()explains undo), and an alert for each. A form the owner declines always stops it.delete_any(anything). The way to delete a non-candidate (and any title).item_idfromlibrary(), areasonof at least 3 words (logged), refused while the title is playing or if Plex can't be read, same recycle bin andundo_delete. It has three states, shown bystatus()asalerts.delete_any:code(the default,DELETE_ANY_REQUIRE_CODE=true): the first call sends the owner a six-digit code through the alert channel and returnscode_sent; the second call includes the code (single-use, bound to the target, 10 minutes, 3 tries then a 10-minute lockout, stored only as a salted hash).disabled: a code is required but there is no alert channel, sodelete_anyrefuses up front. Boundeddeleteof cleanup candidates still works. Set up a channel, or choose no-code mode.no_code(DELETE_ANY_REQUIRE_CODE=false, an explicit opt-in): one call, and works with no channel. The only guards against a wrong delete are the prompt rule "only a title the owner named" and the 7-day undo.
undo_delete. Every delete records the files it removes. Undo moves them from/media/.recyclebinback to their old paths (never outside/media, never over an existing file), re-adds the title at its old folder if it was removed (no search), re-monitors the seasons and rescans. The mediahub container therefore mounts/mediaread-write. Seeding torrents are not brought back.set_rating. Overwrites a rating, logs old and new indata/rating_changes.json(last 200;taste()shows the latest 10), drops the oldwhyunless a new one is given. The title text must match the ledger row for thattmdb_id. "Only a title the owner named" is enforced by the skill and the bot prompt, not the server.Bounded safe actions. Requests stop below 50 GB free (
DISK_GUARD_GB) and after 5 an hour; agent grabs stop after 2 per item.Alerts carry no stranger text. Every channel gets fixed wording and titles from Sonarr/Radarr metadata only. See Alerts below.
Connect a client
The token is MCP_TOKEN in <MEDIAHUB_HOME>/secrets.env (default ~/.mediahub). One script per
OS registers the server with a client without ever printing the token
(compose/scripts/register-mcp.ps1 on Windows, register-mcp.sh on Linux; --url / -Url
changes the default http://127.0.0.1:8000/mcp):
Client | Command | What it does |
Claude Code |
|
|
Codex CLI |
| adds |
Anything else |
| writes |
.\compose\scripts\register-mcp.ps1 claudeInstaller helpers
python -m app.setup <command>, run in the stack as mh run --rm setup <command>:
Command | What it does |
| The |
|
|
| After an update: |
| For an existing Sonarr/Radarr whose series/movies use another container path (for example |
| Prints |
Alerts
Alerts and delete_any codes go out over one optional channel, chosen by NOTIFIER
in secrets.env (TELEGRAM_* and WEBHOOK_* in compose/secrets.env.example):
| What happens |
| Nothing is sent. |
| One-way messages from your own bot ( |
| A JSON POST with a bearer key ( |
Left blank, NOTIFIER is chosen from what is set: webhook, else telegram, else none.
The watch loop
Every 2 minutes, without an LLM:
Groups the Sonarr/Radarr queues by torrent (a season pack is one torrent).
A download that hasn't moved for 40 minutes (30 min grace), or whose swarm is dead (no availability, no seeds for 10 minutes), is swapped for a healthier release — a season search for packs. At most 2 swaps. Nearly-done stalls get one resume + recheck first.
Requests made through the MCP that never grabbed anything are searched again with backoff (1h, 6h, 24h) per movie / season / episode.
Releases are filtered (no CAM/TS, no archives or executables, size sane for the runtime, English) and scored by seeders × the indexer's import track record squared, from 90 days of Sonarr/Radarr history.
What it can't fix becomes a problem for the agent and one alert on the configured channel.
If qBittorrent or a queue can't be read, it judges nothing that pass.
Pause it with WATCH_ENABLED=false in stack.env (then mh up -d mediahub).
It also sends two weekly messages through the alert channel (local time, set in
stack.env): WEEKLY_CLEANUP=SUN 10:00 lists cleanup candidates for the owner to
pick from, and WEEKLY_PICKS=FRI 18:00 asks for 3 recommendations. With no channel
nothing is sent.
New requests use the HD-720p profile (DEFAULT_PROFILE) to save space;
titles already on the stack keep their profile.
Read-only page
http://localhost:8000 — services, media disk, downloads, problems, watch
loop, playing now. No buttons; actions go through the agent.
Production (Docker Desktop, or Linux)
compose/docker-compose.yml (the core), project name mediahub, always run through
compose\mh.ps1 (or compose/mh.sh on Linux, WSL, Git Bash), which adds
--env-file <MEDIAHUB_HOME>\stack.env and picks the compose files from it:
| Files | You get |
|
| MediaHub plus bundled Sonarr, Radarr, Prowlarr, FlareSolverr, qBittorrent, Plex, Bazarr |
|
| MediaHub only, on |
| + | qBittorrent runs inside a gluetun VPN container. Provider: |
Dashboard links (/api/config) show the URLs MediaHub itself uses, so with core
URLs or the VPN they may not open from a browser.
Media in MEDIA_ROOT (/media in every container). App state in named volumes. Every app shares
the net container's network namespace, so services reach each other on
127.0.0.1. Ports publish on 127.0.0.1 only, except Plex (32400, LAN).
Settings |
|
Secrets |
|
Backups |
|
Health |
|
Config as code |
|
Linux scripts |
|
Plex on the LAN |
|
cd <repo>\compose
.\mh.ps1 ps
.\mh.ps1 up -d --build mediahub # after changing app/
.\mh.ps1 logs -f --tail 50 mediahub
.\scripts\verify-stack.ps1 # -AllowLowDisk while the disk is below the guard; -Fresh on a new install
# (unclaimed Plex, no indexers: PENDING instead of FAIL)
.\mh.ps1 run --rm -T setup mcp-ping # checks /mcp with no client; never prints the tokenUpgrade an app: bump its tag in stack.yml, .\mh.ps1 up -d <service>. Never lower a *arr or Plex tag (their databases migrate forward
only). After a fresh Bazarr volume: .\scripts\sync-runtime-secrets.ps1 (runs
mh run --rm setup secrets, then recreates the dashboard container).
First run (MEDIAHUB_STACK=full)
A new install is wired by commands alone: no Python on the host, and no key or
password passes through the person or agent running them (setup prints key
names only). setup is a compose service in the setup profile, so up never
starts it; it runs the mediahub image as PUID:PGID with the app config volumes
mounted (read-only, except qBittorrent's) and MEDIAHUB_HOME at /mh-home.
Create
<MEDIAHUB_HOME>/stack.envfromcompose/stack.env.exampleand an emptysecrets.envnext to it. On Linux, that folder, both files andMEDIA_ROOTmust be writable byPUID(sudo chown -R $PUID:$PGID ~/.mediahub <MEDIA_ROOT>). Optional:PLEX_CLAIM=from https://plex.tv/claim (valid 4 minutes, so do step 2 straight away; seecompose/plex-claim.yml).Run the stack; first start writes each app's config and API key:
.\mh.ps1 up -d # ./mh.sh up -d
.\mh.ps1 stop qbittorrent # ./mh.sh stop qbittorrent
.\mh.ps1 run --rm setup secrets # ./mh.sh run --rm setup secrets
.\mh.ps1 start qbittorrent # ./mh.sh start qbittorrent
.\mh.ps1 up -d --force-recreate mediahub # ./mh.sh up -d --force-recreate mediahub
.\mh.ps1 run --rm setup wire # ./mh.sh run --rm setup wire
.\mh.ps1 run --rm setup check # ./mh.sh run --rm setup checksetup secretscopies the Sonarr, Radarr, Prowlarr and Bazarr API keys into secrets.env (the running app's key wins),PLEX_TOKENonce Plex is claimed (a token you set yourself is kept), and generatesMCP_TOKENand the qBittorrent login (QBIT_USER=admin, a randomQBIT_PASS, its PBKDF2 hash written to qBittorrent.conf) only when they are empty. qBittorrent rewrites its config on exit, so it must be stopped while its password is set;secretsrefuses with aSTOPline if it is running. It waits up to 180 s for the apps' keys (--wait N). Every other line of secrets.env is kept as it was. Exit 1 if something is missing.setup wireonly adds what is missing, and never an indexer: a root folder (/media/TV,/media/Movies) if the app has none, a qBittorrent download client in Sonarr (tv-sonarr) and Radarr (radarr) if there is none (hostlocalhost, orgluetunwithMEDIAHUB_VPN=gluetun), qBittorrent's save path/media/Downloadsif it is outside/media, Prowlarr's Sonarr and Radarr apps (full sync), a FlareSolverr proxy for theflaretag, Plex libraries "Movies" and "TV Shows" if Plex has no library of that type (needsPLEX_TOKEN), plus the config-as-code steps. Anything that exists in another form is left alone. It printsFIXED/OK/NOTE/FAILlines; run it again any time.setup checkiswirewithout writes:DRIFTlines, exit 1 on drift or failure.Plex unclaimed:
wireprints aNOTE. Claim it (PLEX_CLAIM, or on Linux Plex's web page on port 32400 from a browser on the same network), thensetup secrets,up -d --force-recreate mediahub,setup wire.Then add indexers in Prowlarr (http://localhost:9696); it pushes them to Sonarr and Radarr.
Backup / restore
.\scripts\backup.ps1 # stops the stack ~1 min, tars every volume, restarts
.\scripts\backup.ps1 -NoStop # hot copyRestore one volume:
./mh.ps1 stop sonarr
docker run --rm -v mediahub_sonarr-config:/dst -v "$HOME\.mediahub\backups\<stamp>:/src" alpine:3.20 `
sh -c "rm -rf /dst/* && tar -xzf /src/mediahub_sonarr-config.tgz -C /dst"
./mh.ps1 start sonarrRebuild from nothing
Docker (Windows: Docker Desktop with WSL 2;
~/.wslconfigwith[experimental] autoMemoryReclaim=gradualkeeps its memory in check).Clone this repo (https://github.com/bartholomewtj/mediahub-mcp); put your saved
stack.envandsecrets.envback inMEDIAHUB_HOME(or create them from the templates).mh up --no-start(creates the volumes), restore each volume from the backup (above), thenmh up -d,scripts/sync-runtime-secrets(runssetup secrets, recreates MediaHub),mh run --rm setup wire,scripts/verify-stack. Windows only:scripts\plex-lan.ps1.
Development
python -m venv .venv; .venv\Scripts\pip install -r requirements.txt pytest pytest-asyncio pyyaml
.venv\Scripts\python -m pytest -qpyyaml is dev-only: tests/test_compose.py uses it to check the compose
files (skipped when it is missing).
A local run (python -m uvicorn app.main:app --port 8001) reads the same
secrets file. qBittorrent only answers from inside the stack, so the loop and
status swarm data need the container.
Path | What |
| MCP tools (descriptions are the agent's manual) |
| What each tool does; trust rules |
| The loop |
| Release filters, scoring, dead-swarm check |
| API wrappers (*arr, Prowlarr, qBittorrent, Plex) |
| The cached status snapshot |
| The taste ledger ( |
|
|
| Alerts and codes: telegram, webhook or none |
| Deep health checks |
| First-run setup and config as code ( |
| Installer helpers: |
|
|
| Release metadata (MIT) |
| The |
| Production stack |
| The agent playbooks ( |
This server cannot be deployed
Maintenance
Related MCP Connectors
The media memory layer for AI agents and their humans. Your AI client can search your collection, add and log what you've finished, update ratings and lists, and find patterns across everything you've read, watched, played, and listened to.
Media memory for AI agents: books, movies, music, shows, anime, podcasts, games, manga.
Build agents to automate any background task. Works with your ChatGPT/Claude subscription.
Manage a family's Plannie calendar, tasks, lists, files and meal plan as an agent.
Related MCP Servers
- AlicenseBqualityCmaintenanceEnables unified control of \*arr media management applications (Sonarr, Radarr, Lidarr, Readarr, Prowlarr) through natural language queries. Manage TV shows, movies, music, books, search for content, monitor downloads, and check upcoming releases across all services.661,003 npm237MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to manage media automation services like Sonarr, Radarr, Prowlarr, Bazarr, Overseerr, and Plex through natural language commands.7MIT
- AlicenseBqualityDmaintenanceEnables control and management of a self-hosted media stack (Radarr, Sonarr, Prowlarr, SABnzbd, qBittorrent) through natural language via MCP.15GPL 3.0
- AlicenseAqualityAmaintenanceEnables Claude Desktop to troubleshoot and manage a self-hosted Plex + Servarr media stack, including diagnosing missing media, monitoring download queues and indexer health, and coordinating operational tasks across Radarr, Sonarr, SABnzbd, qBittorrent, Tautulli, TMDb, Prowlarr, and remote hosts.26MIT