Two co-hosted services for a Plex + Radarr + Sonarr stack:
- MCP server (
seedbox-mcp) — exposes compact tools for Plex, Radarr, and Sonarr so your agent of choice can inspect media state and do basic actions. - Chat interface (
seedbox-chat) — a Plex-authenticated single-page chat UI powered by Claude Haiku, pre-wired to the MCP server. Designed for family members who have Plex access but not Claude.
Runs anywhere Python can reach your Plex and *arr services — seedbox slots (Whatbox, Ultraseedbox, etc.), homelabs, NAS, VPS. The Whatbox-specific examples below cover one well-tested deployment path; adapt them to your environment (systemd, Docker, reverse proxy of choice).
The service is intentionally narrow:
- Plex is read-only.
- Radarr and Sonarr writes are limited to add, re-search, queue actions, and exact-ID delete.
- Write tools dry-run by default with
confirm=false. - Delete tools preserve files by default with
delete_files=false. - There are no torrent, indexer, filesystem, shell, scheduler, database, or frontend capabilities.
Requires just and uv.
cp .env.example .env
# fill in required values — see Configuration below
just setup
just run # MCP server on :17432
just run-chat # chat interface on :17433 (separate terminal)The MCP endpoint is /mcp; a basic unauthenticated health check is available at /health.
Every /mcp request requires either a static bearer token or a valid OAuth access token:
Authorization: Bearer <MCP_BEARER_TOKEN>All settings are read from .env or environment variables.
| Variable | Notes |
|---|---|
MCP_BEARER_TOKEN |
Shared secret for bearer auth and the OAuth consent gate |
MCP_PUBLIC_BASE_URL |
Public HTTPS base URL (e.g. https://mcp.example.box.ca) — required for OAuth discovery metadata |
RADARR_URL |
Radarr base URL |
RADARR_API_KEY |
Radarr → Settings → General → Security |
RADARR_DEFAULT_ROOT_FOLDER |
Absolute path to movies root folder, as Radarr sees it |
RADARR_DEFAULT_QUALITY_PROFILE_ID |
Integer — see Finding quality profile IDs |
SONARR_URL |
Sonarr base URL |
SONARR_API_KEY |
Sonarr → Settings → General → Security |
SONARR_DEFAULT_ROOT_FOLDER |
Absolute path to TV root folder, as Sonarr sees it |
SONARR_DEFAULT_QUALITY_PROFILE_ID |
Integer — see Finding quality profile IDs |
PLEX_URL |
Plex base URL — must be reachable from where this server runs. On a seedbox slot use the public HTTPS URL; on the same host as Plex http://127.0.0.1:32400 is fine |
PLEX_TOKEN |
Plex auth token — see Finding your Plex token |
| Variable | Default | Notes |
|---|---|---|
MCP_HOST |
127.0.0.1 |
Bind address |
MCP_PORT |
17432 |
Bind port |
RADARR_DEFAULT_MIN_AVAILABILITY |
released |
announced, in_cinemas, or released |
SONARR_DEFAULT_LANGUAGE_PROFILE_ID |
(none) | Sonarr v3 language profile ID; omit for Sonarr v4+ |
SONARR_DEFAULT_SERIES_TYPE |
standard |
standard, daily, or anime |
PLEX_VERIFY_TLS |
true |
Verify Plex HTTPS certificates. Set false only for a trusted endpoint with a self-signed certificate |
PLEX_MOVIE_SECTION |
Movies |
Display name of the Plex movie library |
PLEX_TV_SECTION |
TV Shows |
Display name of the Plex TV library |
TAUTULLI_ENABLED |
false |
Set true to enable Tautulli watch-history enrichment |
TAUTULLI_URL |
(none) | Tautulli base URL |
TAUTULLI_API_KEY |
(none) | Tautulli API key |
OAUTH_ACCESS_TOKEN_TTL |
3600 |
OAuth access token lifetime in seconds |
Startup logs print a redacted config summary. API keys, Plex tokens, bearer tokens, and request headers are never logged.
A single-page chat UI backed by Claude Haiku. Anyone who has friend-level access to the Plex server can log in with their Plex account and chat with an assistant that has full access to the MCP tools.
Write actions (add, delete, queue) require an in-chat confirmation step — Haiku always calls tools with confirm=false first to show a preview, and only proceeds with confirm=true after the user says yes.
The chat server reads the same .env file as the MCP server and adds:
| Variable | Notes |
|---|---|
CHAT_PUBLIC_BASE_URL |
Public HTTPS base URL for the chat app (e.g. https://chat.example.com) — used as the Plex OAuth callback origin |
CHAT_SESSION_SECRET |
Long random string used to sign session cookies |
CHAT_PLEX_CLIENT_ID |
Stable UUID identifying this app to Plex — generate once with python3 -c "import uuid; print(uuid.uuid4())" |
ANTHROPIC_API_KEY |
Anthropic API key for Claude Haiku |
| Variable | Default | Notes |
|---|---|---|
CHAT_HOST |
127.0.0.1 |
Bind address |
CHAT_PORT |
17433 |
Bind port |
SYSTEM_PROMPT_PATH |
(none) | Path to a plain-text file that overrides the default system prompt |
- User visits the chat URL and is redirected to
plex.tvto sign in. - After signing in, Plex redirects back to
/auth/callback. - The server verifies the user is a friend (shared-access user) of the Plex server.
- A signed session cookie is set — no expiry, valid until the browser clears cookies.
Only users who already have Plex friend access can log in. There is no separate user management.
The chat server is launched alongside the MCP server by scripts/start.sh — see the Deployment section below. Front the chat port (17433) with HTTPS the same way you do for the MCP server, and set CHAT_PUBLIC_BASE_URL to the resulting public URL.
Open Plex Web, play any item, then choose "Get Info" → "View XML". The X-Plex-Token query parameter in the URL is your token. Full guide: https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
The integer ID is visible in the URL when you click Edit on a profile in Settings → Profiles (e.g. /settings/profiles/edit/2 → ID is 2). You can also query the API:
# Radarr
curl -s "https://<radarr-host>/api/v3/qualityprofile?apikey=<key>" | python3 -m json.tool
# Sonarr
curl -s "https://<sonarr-host>/api/v3/qualityprofile?apikey=<key>" | python3 -m json.toolThe pattern below is what the author runs on Whatbox. On a homelab with systemd, swap screen + @reboot for a systemd unit; on Docker, package the same python -m seedbox_mcp.server invocation. Whatever the runtime, the server expects to be fronted by HTTPS at MCP_PUBLIC_BASE_URL — on Whatbox that's a "managed HTTPS link"; elsewhere use Caddy, nginx, Tailscale Serve, or similar.
git clone <repo-url> ~/seedbox-mcp
cd ~/seedbox-mcp
cp .env.example .env
# fill in all required values
uv syncOnce it's cloned, push new code from your workstation with just deploy (or bash scripts/deploy.sh). It SSHes in, fast-forwards the checkout, runs uv sync, and restarts both servers. It expects a seedbox host in your ~/.ssh/config with key auth set up (ssh-copy-id seedbox); override the target/path/branch via SEEDBOX_SSH, SEEDBOX_DIR, SEEDBOX_BRANCH.
scripts/start.sh kills any existing sessions and launches both servers in detached screen sessions (media-mcp and media-chat), tailing their output to mcp.log and chat.log:
bash scripts/start.shReattach: screen -r media-mcp (or media-chat). Detach: Ctrl-A D.
crontab -eAdd:
@reboot sleep 30 && bash ~/seedbox-mcp/scripts/start.shThe sleep 30 gives the host time to bring up networking before the server tries to connect.
bash scripts/start.sh(The script kills the existing screen sessions before relaunching.)
Front http://127.0.0.1:17432 (MCP) and http://127.0.0.1:17433 (chat) with HTTPS — Whatbox managed links, a reverse proxy (Caddy / nginx), Tailscale Serve, or equivalent — and set MCP_PUBLIC_BASE_URL and CHAT_PUBLIC_BASE_URL to the resulting URLs. OAuth discovery metadata is derived from these values, so they must match what your clients actually use.
curl https://<your-host>/health
curl https://<your-host>/.well-known/oauth-authorization-serverBoth Claude.ai and ChatGPT MCP connectors require OAuth 2.0. The server implements Authorization Code + PKCE automatically — no extra setup beyond a running server with MCP_PUBLIC_BASE_URL set.
- In the agent of your choice, add a new MCP integration:
- MCP Server URL:
https://<your-host>/mcp - Client ID: any string, e.g.
claude-ai(the server accepts any value here) - Client Secret: leave blank or enter any dummy value — it is not validated (PKCE replaces client secrets)
- MCP Server URL:
- The agent will open
https://<your-host>/oauth/authorizein your browser. - Paste your
MCP_BEARER_TOKENinto the consent form and submit. - The agent stores the resulting access token (default 1-hour TTL) and refreshes it automatically via a 30-day refresh token.
Prefer read-only tools first. For add, delete, re-search, and queue operations, first identify the exact item with media_search or an overview tool. For ambiguous titles, return candidates or ask for disambiguation. Do not invent IDs. Queue actions require the queue_id from radarr_overview or sonarr_overview output. Do not claim to access torrent clients, indexers, shell, or the filesystem; this app intentionally does not expose those capabilities.
Consult the justfile:
just setup # install runtime and dev dependencies with uv
just run # run the MCP server (:17432)
just run-chat # run the chat interface (:17433)
just test # run MCP unit tests (no live services required)
just test-chat # run chat unit tests (no live services required)
just test-live # run MCP live integration tests (requires LIVE_TESTS=1)
just test-chat-live # run chat live tests (requires LIVE_TESTS=1 + running MCP server)
just test-smoke # call /health on the configured host/port
just format # format with ruff
just check # ruff lint + mypy type-checkRead-only tools:
media_status— Radarr, Sonarr, and Plex reachability, version, health, and disk spaceradarr_overview— movie list, download queue, and wanted/missingsonarr_overview— series list, download queue, and wanted/missing episodesplex_overview— active sessions, recently added, and unwatched staleness candidatesmedia_search— fuzzy search across Radarr, Sonarr, and Plex with external TMDb/TVDb lookupstaleness_report— cross-references Plex watch history against Radarr/Sonarr to surface old unwatched, unmanaged, and queue-stuck items
Write tools:
radarr_add_movie— add a movie by TMDb ID (dry-run by default)radarr_research_movie— trigger a search, refresh, or downloaded-scan command on a Radarr movieradarr_delete_movie— remove a movie from Radarr by internal ID (preserves files by default)radarr_queue_action—removeorblocklista stuck queue item by queue IDsonarr_add_series— add a series by TVDb ID (dry-run by default)sonarr_research_series— trigger a series search, refresh, or missing-episode searchsonarr_delete_series— remove a series from Sonarr by internal ID (preserves files by default)sonarr_queue_action—removeorblocklista stuck queue item by queue ID
Write tools require explicit identifiers. Add operations require TMDb or TVDb IDs. Delete, re-search, and queue operations require exact Radarr or Sonarr internal IDs. Queue actions never touch the torrent client (removeFromClient=false).
What is currently playing on Plex?
Are Radarr or Sonarr stuck on anything?
Show movies added to Plex more than 90 days ago that no one has watched.
Is Heat already on the server? If not, prepare to add the 1995 Michael Mann film, but do not actually add it yet.
Remove the 2013 film The Heat from Radarr, but do not delete files.
Are there any stuck or import-blocked items in the Radarr or Sonarr queue? If so, clear them.
- Missing config: run
cp .env.example .env, then fill in all required values. - 401 from
/mcp: verifyAuthorization: Bearer <MCP_BEARER_TOKEN>. - OAuth consent form rejects your token: paste the exact value of
MCP_BEARER_TOKENfrom your.envwith no extra whitespace. - Claude.ai "Authorization failed" after consent: check that
MCP_PUBLIC_BASE_URLis set and matches your public hostname; verify withcurl https://<host>/.well-known/oauth-authorization-serverthattoken_endpointis a full HTTPS URL. - Radarr or Sonarr auth errors: verify API keys in each app.
- Plex errors:
PLEX_URLmust be reachable from where the server runs. On a seedbox slot that usually means the public HTTPS URL, not127.0.0.1:32400. VerifyPLEX_TOKENand section names (PLEX_MOVIE_SECTION,PLEX_TV_SECTION). - Partial status warnings: one upstream may be down while the MCP server itself is healthy.
Automated tests (just test) use mocked upstreams and do not require live services. Use just test-live to test access to external services.