Skip to main content
Glama
gbone001
by gbone001

media-stack-mcp

What This Is

media-stack-mcp is a Model Context Protocol server for operating a self-hosted media stack:

  • Radarr

  • Sonarr

  • Prowlarr

  • SABnzbd

  • qBittorrent

The primary deployment target is QNAP Docker. In Docker mode this runs as a persistent Streamable HTTP MCP endpoint:

http://<qnap-ip>:3000/mcp

For Claude remote MCP/custom connector usage, publish the same MCP server through Cloudflare Tunnel:

https://mcp.yourdomain.com/mcp

Clients must send:

Authorization: Bearer <MCP_HTTP_TOKEN>

The container exposes an unauthenticated health endpoint:

http://<qnap-ip>:3000/healthz

Related MCP server: Arr Suite MCP Server

Clone The Repo Locally And Build Docker

Use this path if you want to clone the repository on your workstation, build the Docker image locally, and optionally smoke-test the HTTP server before deploying it on the QNAP.

Clone the repo locally:

git clone https://github.com/gbone001/media-stack-mcp.git
cd media-stack-mcp

Create a local .env from the example:

cp .env.example .env

Edit .env and set at least:

MCP_TRANSPORT=http
MCP_HTTP_HOST=0.0.0.0
MCP_HTTP_PORT=3000
MCP_HTTP_TOKEN=use_a_long_random_secret_here

If you want to run the container locally, use LAN IP URLs for Radarr, Sonarr, Prowlarr, SABnzbd, and qBittorrent unless those services are also running on the same local Docker network:

RADARR_URL=http://<qnap-ip>:7878
SONARR_URL=http://<qnap-ip>:8989
PROWLARR_URL=http://<qnap-ip>:9696
SABNZBD_URL=http://<qnap-ip>:8080
QBITTORRENT_URL=http://<qnap-ip>:5080

Build the Docker image locally:

docker build -t media-stack-mcp .

This does not require Node.js or pnpm on your workstation. The Dockerfile installs and builds everything inside the image.

Optional local smoke test:

docker run --rm -p 3000:3000 --env-file .env -e MCP_TRANSPORT=http media-stack-mcp

In another terminal, check:

curl http://localhost:3000/healthz

Expected response:

{"ok":true,"name":"media-stack-mcp","transport":"http"}

When you are ready to run it on the QNAP, SSH into the QNAP and either clone the same repo there or pull the latest code in the existing QNAP checkout:

cd /share/Container/media-stack-mcp
git pull
docker compose up -d --build

The included docker-compose.yml is the preferred QNAP runtime path because it publishes port 3000, loads .env, and attaches the container to the expected media-stack Docker network.

For Claude, also configure Cloudflare Tunnel so the public HTTPS URL routes to the internal Docker service:

https://mcp.yourdomain.com -> http://media-stack-mcp:3000

QNAP Docker Quick Start

SSH into your QNAP and choose an install location:

mkdir -p /share/Container
cd /share/Container
git clone https://github.com/gbone001/media-stack-mcp.git
cd media-stack-mcp

Create your runtime config:

cp .env.example .env
vi .env

At minimum, change the token:

MCP_HTTP_TOKEN=use_a_long_random_secret_here

Build and start the MCP container:

docker compose up -d --build

Watch startup logs:

docker compose logs -f media-stack-mcp

Claude HTTPS Setup

Claude remote MCP/custom connectors need a trusted HTTPS URL. This project ships an optional cloudflared sidecar in Docker Compose so the MCP app can stay plain HTTP inside Docker while Cloudflare publishes HTTPS externally.

The sidecar is off by default and lives behind the cloudflare Compose profile. Enable it by adding --profile cloudflare to your compose commands (see below). If you reach the MCP another way (LAN only, your own reverse proxy, etc.), you can ignore this section entirely and leave CLOUDFLARED_TOKEN blank.

The important lesson from Claude setup is that Claude does not connect to the QNAP LAN URL directly. Use the Cloudflare HTTPS hostname in Claude, not http://<qnap-ip>:3000/mcp, not localhost, and not a self-signed HTTPS URL.

Claude needs:

  • a publicly reachable https:// URL with a trusted certificate

  • the full MCP path, for example https://mcp.yourdomain.com/mcp

  • the bearer token header: Authorization: Bearer <MCP_HTTP_TOKEN>

  • no extra browser-only login page in front of /mcp

In the Cloudflare Zero Trust dashboard:

  1. Create a Cloudflare Tunnel.

  2. Choose Docker as the connector environment.

  3. Add a public hostname, for example mcp.yourdomain.com.

  4. Set the tunnel service target to:

http://media-stack-mcp:3000

Copy the tunnel token into .env:

CLOUDFLARED_TOKEN=your_cloudflare_tunnel_token

Set the public MCP URL and allowed Host header:

MCP_PUBLIC_URL=https://mcp.yourdomain.com/mcp
MCP_ALLOWED_HOSTS=mcp.yourdomain.com

Start or update the stack with the tunnel enabled:

docker compose --profile cloudflare up -d --build

Check the Cloudflare Tunnel logs:

docker compose logs -f cloudflared

Use this URL in Claude:

https://mcp.yourdomain.com/mcp

Use this header:

Authorization: Bearer <MCP_HTTP_TOKEN>

Do not put Cloudflare Access in front of /mcp unless your Claude client can send the required Cloudflare Access headers. Keep MCP_HTTP_TOKEN as the MCP protection layer.

Claude connector values should look like this:

Name: media-stack
URL: https://mcp.yourdomain.com/mcp
Authorization header: Bearer your_long_random_mcp_token

If Claude asks for separate auth fields, use:

Header name: Authorization
Header value: Bearer your_long_random_mcp_token

The token must be the value from MCP_HTTP_TOKEN, not the Cloudflare tunnel token.

Do not paste CLOUDFLARED_TOKEN into Claude. That token is only for the cloudflared Docker sidecar.

Required .env Values

These MCP settings should be present for QNAP Docker mode:

MCP_SERVER_NAME=media-stack-mcp
MCP_TRANSPORT=http
MCP_HTTP_HOST=0.0.0.0
MCP_HTTP_PORT=3000
MCP_HTTP_PATH=/mcp
MCP_HTTP_TOKEN=use_a_long_random_secret_here
MCP_PUBLIC_URL=https://mcp.yourdomain.com/mcp
MCP_ALLOWED_HOSTS=mcp.yourdomain.com
CLOUDFLARED_TOKEN=your_cloudflare_tunnel_token

Configure whichever media apps you want enabled. A tool returns a clear configuration error if its matching app URL/key is missing.

RADARR_URL=http://radarr:7878
RADARR_API_KEY=your_radarr_api_key

SONARR_URL=http://sonarr:8989
SONARR_API_KEY=your_sonarr_api_key

PROWLARR_URL=http://prowlarr:9696
PROWLARR_API_KEY=your_prowlarr_api_key

SABNZBD_URL=http://sabnzbd:8080
SABNZBD_API_KEY=your_sabnzbd_api_key

QBITTORRENT_URL=http://vpn:5080
QBITTORRENT_USERNAME=admin
QBITTORRENT_PASSWORD=your_qbittorrent_password

For Radarr, Sonarr, and Prowlarr, get the API key from each app's UI under Settings -> General -> Security / API Key.

For SABnzbd, get the API key from Config -> General -> Security / API Key.

For qBittorrent, use the WebUI URL and WebUI username/password.

Docker Networking On QNAP

The included docker-compose.yml expects an existing external Docker network named media-stack:

networks:
  media-stack:
    external: true

List Docker networks on the QNAP:

docker network ls

Inspect which networks your existing media containers use:

docker inspect radarr --format '{{json .NetworkSettings.Networks}}'
docker inspect sonarr --format '{{json .NetworkSettings.Networks}}'
docker inspect prowlarr --format '{{json .NetworkSettings.Networks}}'
docker inspect sabnzbd --format '{{json .NetworkSettings.Networks}}'
docker inspect vpn --format '{{json .NetworkSettings.Networks}}'

If media-stack already exists and your media containers are on it, no compose change is needed.

If your media stack uses a different network name, update docker-compose.yml to use that network instead of media-stack.

If the network does not exist, create it:

docker network create media-stack

Then attach existing containers as needed:

docker network connect media-stack radarr
docker network connect media-stack sonarr
docker network connect media-stack prowlarr
docker network connect media-stack sabnzbd
docker network connect media-stack vpn

When this MCP container shares a Docker network with your media apps, use container DNS names:

RADARR_URL=http://radarr:7878
SONARR_URL=http://sonarr:8989
PROWLARR_URL=http://prowlarr:9696
SABNZBD_URL=http://sabnzbd:8080
QBITTORRENT_URL=http://vpn:5080

If it does not share a Docker network, use your QNAP or LAN IP instead:

RADARR_URL=http://<qnap-ip>:7878
SONARR_URL=http://<qnap-ip>:8989
PROWLARR_URL=http://<qnap-ip>:9696
SABNZBD_URL=http://<qnap-ip>:8080
QBITTORRENT_URL=http://<qnap-ip>:5080

If qBittorrent runs behind Gluetun and the WebUI is exposed by the VPN/Gluetun container, use the container that exposes the WebUI. Commonly:

QBITTORRENT_URL=http://vpn:5080

Validate The Deployment

Check container status:

docker compose ps

Check logs:

docker compose logs -f media-stack-mcp

Check the health endpoint:

curl http://<qnap-ip>:3000/healthz

Expected response:

{"ok":true,"name":"media-stack-mcp","transport":"http"}

Check the Cloudflare HTTPS health endpoint:

curl https://mcp.yourdomain.com/healthz

Expected response:

{"ok":true,"name":"media-stack-mcp","transport":"http"}

Check that /mcp rejects unauthenticated requests:

curl -i -X POST http://<qnap-ip>:3000/mcp \
  -H 'Content-Type: application/json' \
  -d '{}'

Expected status:

401 Unauthorized

Check that the public HTTPS /mcp endpoint also rejects unauthenticated requests:

curl -i -X POST https://mcp.yourdomain.com/mcp \
  -H 'Content-Type: application/json' \
  -d '{}'

Expected status:

401 Unauthorized

MCP clients must call /mcp with:

Authorization: Bearer <MCP_HTTP_TOKEN>

MCP Client Config

Use this shape for clients that support remote Streamable HTTP MCP servers:

{
  "mcpServers": {
    "media-stack": {
      "url": "https://mcp.yourdomain.com/mcp",
      "headers": {
        "Authorization": "Bearer your_long_random_token"
      }
    }
  }
}

Replace mcp.yourdomain.com with your Cloudflare Tunnel hostname and your_long_random_token with MCP_HTTP_TOKEN from .env.

HTTP MCP sessions are stored in memory. Restarting the container resets active MCP sessions, which is expected.

Operations

Start or update after config changes:

docker compose up -d --build

Stop:

docker compose down

Restart:

docker compose restart media-stack-mcp

Follow logs:

docker compose logs -f media-stack-mcp

Follow Cloudflare Tunnel logs:

docker compose logs -f cloudflared

Pull the latest code and rebuild:

git pull && docker compose up -d --build

Troubleshooting

.env Not Found

Create the runtime env file:

cp .env.example .env
vi .env

network media-stack declared as external, but could not be found

Create the expected network:

docker network create media-stack

Or edit docker-compose.yml to use the Docker network your media containers already use.

MCP_HTTP_TOKEN is required when MCP_TRANSPORT=http

Set a token in .env:

MCP_HTTP_TOKEN=use_a_long_random_secret_here

Then restart:

docker compose up -d --build

CLOUDFLARED_TOKEN Is Missing Or Invalid

Create a Cloudflare Tunnel in the Cloudflare Zero Trust dashboard, copy the Docker tunnel token, and set:

CLOUDFLARED_TOKEN=your_cloudflare_tunnel_token

Then restart with the cloudflare profile and check logs:

docker compose --profile cloudflare up -d --build
docker compose logs -f cloudflared

Cloudflare Tunnel Shows 502 Or Cannot Reach The Service

In the Cloudflare Tunnel public hostname settings, the service target should be:

http://media-stack-mcp:3000

Confirm both containers are on the same Docker network:

docker compose ps
docker inspect media-stack-mcp --format '{{json .NetworkSettings.Networks}}'
docker inspect media-stack-mcp-cloudflared --format '{{json .NetworkSettings.Networks}}'

Claude Cannot Connect To The MCP URL

Use the public Cloudflare URL in Claude:

https://mcp.yourdomain.com/mcp

Do not use these in Claude remote MCP/custom connector setup:

http://<qnap-ip>:3000/mcp
http://localhost:3000/mcp
https://<qnap-ip>:3000/mcp

Then validate from outside your LAN if possible:

curl https://mcp.yourdomain.com/healthz

If health works but Claude still fails, check that Claude has the bearer token header exactly:

Authorization: Bearer <MCP_HTTP_TOKEN>

Also confirm Cloudflare is not presenting an Access login page or other browser challenge in front of /mcp.

Public HTTPS URL Returns Host Header Errors

Set MCP_ALLOWED_HOSTS to your Cloudflare Tunnel hostname:

MCP_ALLOWED_HOSTS=mcp.yourdomain.com

Then restart:

docker compose restart media-stack-mcp

401 Unauthorized

Your MCP client is missing the bearer token or using the wrong token. Configure:

Authorization: Bearer <MCP_HTTP_TOKEN>

Media App Connection Failures

Check that each URL works from inside the MCP container's network. If container names do not resolve, either attach the MCP container to the same Docker network or use LAN IP URLs.

Common same-network URLs:

RADARR_URL=http://radarr:7878
SONARR_URL=http://sonarr:8989
PROWLARR_URL=http://prowlarr:9696
SABNZBD_URL=http://sabnzbd:8080

Common LAN URLs:

RADARR_URL=http://<qnap-ip>:7878
SONARR_URL=http://<qnap-ip>:8989
PROWLARR_URL=http://<qnap-ip>:9696
SABNZBD_URL=http://<qnap-ip>:8080

Also confirm the API key is copied from the matching app, not another service.

qBittorrent Auth Failure

Confirm QBITTORRENT_URL points to the WebUI endpoint and that the WebUI credentials are correct:

QBITTORRENT_URL=http://vpn:5080
QBITTORRENT_USERNAME=admin
QBITTORRENT_PASSWORD=your_qbittorrent_password

If you use Gluetun, the correct host is often the VPN/Gluetun container rather than a separate qbittorrent container.

Current MCP Tools

Health

  • media_health_check

Radarr

  • radarr_lookup_movie

  • radarr_add_movie

  • radarr_get_queue

Sonarr

  • sonarr_lookup_series

  • sonarr_add_series

  • sonarr_get_queue

SABnzbd

  • sabnzbd_get_queue

  • sabnzbd_pause

  • sabnzbd_resume

Prowlarr

  • prowlarr_search

  • prowlarr_test_indexers

qBittorrent

  • qbittorrent_list_torrents

  • qbittorrent_pause_all

  • qbittorrent_resume_all

Local Stdio Mode

For local process-spawned MCP clients, set:

MCP_TRANSPORT=stdio

Install and build locally:

corepack enable
pnpm install
pnpm run build

Example local stdio MCP config:

{
  "mcpServers": {
    "media-stack": {
      "command": "node",
      "args": ["/path/to/media-stack-mcp/dist/index.js"],
      "env": {
        "MCP_TRANSPORT": "stdio",
        "RADARR_URL": "http://<qnap-ip>:7878",
        "RADARR_API_KEY": "your_key",
        "SONARR_URL": "http://<qnap-ip>:8989",
        "SONARR_API_KEY": "your_key",
        "SABNZBD_URL": "http://<qnap-ip>:8080",
        "SABNZBD_API_KEY": "your_key",
        "PROWLARR_URL": "http://<qnap-ip>:9696",
        "PROWLARR_API_KEY": "your_key",
        "QBITTORRENT_URL": "http://<qnap-ip>:5080",
        "QBITTORRENT_USERNAME": "admin",
        "QBITTORRENT_PASSWORD": "your_password"
      }
    }
  }
}

Security

Do not expose port 3000 directly to the public internet.

Use Cloudflare Tunnel for Claude HTTPS connectivity instead of opening inbound ports on the QNAP. Keep bearer-token auth enabled, use a long random MCP_HTTP_TOKEN, set MCP_ALLOWED_HOSTS to your public tunnel hostname, and rotate both MCP_HTTP_TOKEN and CLOUDFLARED_TOKEN if either value is shared or exposed.

Install Server
A
license - permissive license
B
quality
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gbone001/media-stack-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server