downloader-mcp
by CarlDog
README.md
# downloader-mcp
<!-- fleet-confidence -->
 <sub>· `claude-opus-4-8[1m]` · 2026-07-07 · [details](https://github.com/CarlDog/downloader-mcp/issues/1)</sub>
<!-- /fleet-confidence -->
An [MCP](https://modelcontextprotocol.io) server for download clients —
**SABnzbd** (usenet) and **qBittorrent** (torrents) — packaged as a
Docker container. Companion to media-management MCPs like
[`servarr-mcp`](https://github.com/CarlDog/servarr-mcp).
Each client is optional: configure only the ones you actually run, and
only those tools register.
## Tools
### SABnzbd (usenet)
| Tool | Description |
| --- | --- |
| `sabnzbd_queue` | Current download queue with speeds and ETAs |
| `sabnzbd_history` | Recent history (newest first) |
| `sabnzbd_categories` | Configured categories |
| `sabnzbd_version` | SABnzbd version info |
### qBittorrent (torrents)
| Tool | Description |
| --- | --- |
| `qbittorrent_list_torrents` | List torrents, optional status filter |
| `qbittorrent_get_torrent` | Torrent details by info-hash |
| `qbittorrent_torrent_files` | Files inside a torrent |
| `qbittorrent_transfer_info` | Global transfer stats |
| `qbittorrent_categories` | Configured categories |
| `qbittorrent_version` | qBittorrent application version |
## Configuration
Each client requires its full config block to enable; partial config
silently disables the client.
| Client | Required env vars | Default port |
| --- | --- | --- |
| SABnzbd | `SABNZBD_URL`, `SABNZBD_API_KEY` | 8080 |
| qBittorrent | `QBITTORRENT_URL`, `QBITTORRENT_API_KEY` | 8080 |
API keys are found in each app's settings:
- SABnzbd: *Config → General → API Key*
- qBittorrent: *Tools → Options → Web UI → API Key* (requires
qBittorrent >= v5.2.0 / WebAPI >= v2.14.1)
> **Note:** SABnzbd and qBittorrent both default to port 8080. If you
> run both on the same host, remap one of them in its own config.
At least one client must be configured or the server exits with an error.
### HTTP transport hardening
When running in HTTP mode (`MCP_PORT` set), you can enable bearer-token
auth and DNS-rebinding protection with:
| Env var | Meaning |
| --- | --- |
| `MCP_AUTH_TOKEN` | Shared secret. When set, every `/mcp` request must carry `Authorization: Bearer <token>`; `/health` stays open for the docker healthcheck. |
| `MCP_ALLOWED_HOSTS` | Comma-separated bare-hostname Host/Origin allowlist (DNS-rebinding defense; port-independent, bracketed IPv6 like `[::1]` supported). A present `Origin` header must independently match too. A `host:port` entry, scheme, or wildcard now throws at startup. Unset falls back to `localhost,127.0.0.1,[::1],host.docker.internal` (safe default, not open). |
The provided Compose deployment requires `MCP_ALLOWED_HOSTS` so a missing
stack variable cannot silently expose the MCP endpoint to whatever the safe
default admits. `MCP_AUTH_TOKEN` stays optional even in Compose: if unset,
the server logs a startup warning and leaves `/mcp` unauthenticated.
Recommended `MCP_AUTH_TOKEN`: a random secret, e.g. `openssl rand -hex 32`,
passed by clients as `Authorization: Bearer <token>`.
Recommended `MCP_ALLOWED_HOSTS`: the host names/IPs clients actually use to
reach the server — fleet-canonical form is
`MCP_ALLOWED_HOSTS=localhost,127.0.0.1,[::1],192.168.1.50,host.docker.internal`
(bare hostnames only — a mapped `HOST_PORT` doesn't need to appear here, and
a `host:port` entry is now rejected at startup rather than silently ignored).
## Run with Docker
```bash
docker build -t downloader-mcp .
docker run -i --rm \
-e SABNZBD_URL=http://192.168.1.50:8080 -e SABNZBD_API_KEY=... \
-e QBITTORRENT_URL=http://192.168.1.50:8081 \
-e QBITTORRENT_API_KEY=... \
downloader-mcp
```
## Published image
After each push to `main` (docs-only changes excluded), GitHub Actions
builds and pushes an image to GHCR:
`ghcr.io/carldog/downloader-mcp:latest` (linux/amd64 only — every
deployment target is x86-64; see `docker-publish.yml` for the ARM
tradeoff if that ever changes)
Pull instead of building locally:
```bash
docker pull ghcr.io/carldog/downloader-mcp:latest
docker run -i --rm \
-e SABNZBD_URL=... -e SABNZBD_API_KEY=... \
ghcr.io/carldog/downloader-mcp:latest
```
## Run with Docker Compose (HTTP, long-lived)
The compose file runs the server in HTTP mode (Streamable HTTP) for
long-lived deployment via Portainer or Compose. It pulls the published
image from `ghcr.io/carldog/downloader-mcp:latest`.
```bash
# Set whichever client credentials apply:
export SABNZBD_URL=http://192.168.1.50:8080; export SABNZBD_API_KEY=...
export QBITTORRENT_URL=http://192.168.1.50:8081
export QBITTORRENT_API_KEY=...
export MCP_AUTH_TOKEN="$(openssl rand -hex 32)"
export MCP_ALLOWED_HOSTS="192.168.1.50"
export HOST_PORT=3003 # optional, defaults to 3003
docker compose up
```
The MCP endpoint will be at `http://<host>:${HOST_PORT}/mcp`.
## Deploy via Portainer (Stack from Git)
1. In Portainer, *Stacks → Add Stack → Repository*.
2. Repository URL: `https://github.com/CarlDog/downloader-mcp`
3. Compose path: `docker-compose.yml`
4. Environment variables: set whichever client credentials apply, plus
`MCP_AUTH_TOKEN` and `MCP_ALLOWED_HOSTS`. Optionally set `HOST_PORT`.
5. Deploy. Healthcheck reaches green within ~10 seconds.
## Use with Claude Desktop
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"downloader": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "SABNZBD_URL", "-e", "SABNZBD_API_KEY",
"-e", "QBITTORRENT_URL",
"-e", "QBITTORRENT_API_KEY",
"downloader-mcp"
],
"env": {
"SABNZBD_URL": "http://192.168.1.50:8080",
"SABNZBD_API_KEY": "...",
"QBITTORRENT_URL": "http://192.168.1.50:8081",
"QBITTORRENT_API_KEY": "..."
}
}
}
}
```
Drop the `-e`/`env` entries for whichever client you don't run.
## Local development
```bash
npm install
cp .env.example .env # then edit
SABNZBD_URL=... SABNZBD_API_KEY=... npm run dev
```
## Security
- Container runs as a non-root user (`mcp`).
- Credentials passed via env vars — never baked into the image.
- A `.githooks/pre-commit` runs gitleaks (secrets) and a PII pattern
check (user-home paths, personal-domain emails). Activate it once
per clone: `git config core.hooksPath .githooks`.
TDQS
A3.7/5.0
Scored across 10 tools
Disambiguation5/5
Every tool is clearly distinguished by its client prefix (qbittorrent_ vs sabnzbd_) and specific action, with no overlap in functionality between tools.
Naming Consistency5/5
All tools use a consistent snake_case client_verb_noun pattern, making it easy to predict tool names and purposes.
Tool Count5/5
10 tools is well-scoped for a server supporting two download clients, covering essential query operations without unnecessary bloat.
Completeness3/5
While query operations are well-covered (list, get, categories, etc.), the server lacks mutation commands like add, remove, or pause torrents/jobs, which are expected for a downloader tool.
Maintenance
ActivityActive
ResponsivenessResponsive