jdownloader-mcp
# jdownloader-mcp
An MCP server that lets an agent drive a **local JDownloader 2** through its
entire download lifecycle — add links / containers, watch the linkgrabber
resolve them, move them to the queue, set priority, force-start, track progress,
inspect download/extract directories, and clean up.
It exists for the one download workload the shell can't already do: **premium
hosters, one-click hosters, `.dlc`/`.crypt` containers, and captcha-gated folder
links.** JDownloader stays the moat for that case — but plain video/social
URLs (TikTok, YouTube, X, Instagram, Reddit, ...) are often where JD's
plugins fall behind (anti-bot walls, "plugin outdated"). For those, the
server also carries a **yt-dlp backend** and a `smart_download` router that
tries JDownloader first and auto-falls-back to yt-dlp when a link breaks, so
the agent can finish the download either way instead of stopping at "left to
the shell."
Thin wrapper over [`myjdapi`](https://github.com/mmarquezs/My.Jdownloader-API-Python-Library)
and [`yt-dlp`](https://github.com/yt-dlp/yt-dlp); built on the `mcp` 1.x FastMCP API.
> **Status: v0.6.0 — 62 tools.** `monitor()` is the tool for watching a
> queue over time: a live snapshot for polling — overall state + speed,
> download counts bucketed by category, active-download progress, and
> blocked hosts, in one call. The full v0.3 tool surface has now been
> exercised live against a real JDownloader 2: 24 safe tools (config,
> cleanup/update, variants/organize, extensions, dialogs, basic-auth/account
> creds reads, plus the original 34 v0.2.0 lifecycle/query/account tools) are
> live-verified, on top of 91+ mocked unit tests and the full
> `smoke_test.py` lifecycle. That live pass found and fixed two bugs:
> `get_download_urls` (now returns each link's `url` via `query_links`) and
> `get_variants` (now handles non-variant links gracefully) — both are now
> live-verified working, as is `set_variant`. Destructive tools
> (`clear_linkgrabber`, `update_jd`, `restart_jd`, `reconnect`) remain
> unit-tested only by design — not exercised live.
## Lifecycle
```
add_links / add_container ─▶ LINKGRABBER ─(move_to_downloads)─▶ DOWNLOADS ─▶ extract ─▶ remove
list_linkgrabber list_downloads / force_start / wait_for
```
Item IDs (`uuid`) come from the `list_*` tools; most tools take
`package_ids` / `link_ids` plus a `stage` (`linkgrabber` | `downloads`).
See **[Agent lifecycle](#agent-lifecycle)** below for worked tool-call flows,
and the full **[tool reference](#tools-62)** for every parameter.
## Tools (62)
Every tool returns `{"ok": true, "result": ...}` on success or
`{"ok": false, "error": "..."}` on failure — no tool raises.
### Lifecycle (16)
| Tool | Parameters | Description |
|---|---|---|
| `add_links` | `urls: list[str]`, `package: str \| None`, `destination_folder: str \| None`, `priority: str = "DEFAULT"`, `extract_password: str \| None`, `download_password: str \| None`, `autostart: bool = False` | Add URLs to the linkgrabber. `autostart=True` moves them straight to the download queue once resolved. `priority`: HIGHEST..LOWEST. |
| `add_container` | `container_type: str`, `content: str` | Add a link container (`container_type`: DLC \| RSDF \| CCF) by its content. |
| `list_linkgrabber` | — | List linkgrabber packages+links (online/offline status) and whether JD is still collecting. Use the `uuid` fields to act on items. |
| `list_downloads` | — | List download-queue packages with `progress_pct`, speed, ETA, status, and save path (`saveTo`). The workhorse for polling. |
| `status` | — | Overall dashboard: state, running/paused, aggregate speed (bytes/s), premium, speed-limit state, bytes downloaded, and `update_available`. |
| `wait_for` | `package_id: str`, `timeout_s: int = 60` | Bounded poll of one download package (cap 120s). Returns finished / failed / stalled / still_running / not_found — finished/failed/stalled are terminal, re-call only on still_running. |
| `move_to_downloads` | `package_ids: list[str] \| None`, `link_ids: list[str] \| None` | Promote resolved linkgrabber items to the download queue. |
| `control_downloads` | `action: str` | Global queue control. `action`: start \| stop \| pause \| resume. |
| `force_start` | `package_ids: list[str] \| None`, `link_ids: list[str] \| None` | Force the given items to download now, bypassing queue/connection limits. |
| `set_priority` | `priority: str`, `stage: str = "linkgrabber"`, `package_ids: list[str] \| None`, `link_ids: list[str] \| None` | Set priority (HIGHEST..LOWEST) on items in the given stage (linkgrabber \| downloads). |
| `set_enabled` | `enabled: bool`, `stage: str = "downloads"`, `package_ids: list[str] \| None`, `link_ids: list[str] \| None` | Enable/disable items in the given stage (linkgrabber \| downloads). |
| `get_directories` | `package_ids: list[str] \| None` | Per-package save paths + the default download folder + extraction settings (extension enabled, custom extract path). |
| `set_download_dir` | `directory: str`, `package_ids: list[str]` | Set the download directory for the given download-queue packages. |
| `set_extraction` | `enabled: bool \| None`, `custom_path: str \| None` | Toggle the extraction extension and/or set a custom extract path. |
| `remove` | `stage: str = "downloads"`, `package_ids: list[str] \| None`, `link_ids: list[str] \| None`, `from_disk: bool = False` | Remove items from a list. `from_disk=True` (downloads only) also deletes the files from disk. |
| `reset` | `package_ids: list[str] \| None`, `link_ids: list[str] \| None` | Reset download-queue items so they re-download from scratch. |
### Ergonomic / composite (3)
| Tool | Parameters | Description |
|---|---|---|
| `download` | `urls: list[str]`, `dest: str \| None`, `package: str \| None`, `priority: str = "DEFAULT"` | One-shot download: add URLs, auto-move to the queue, start, and return the resulting download package. The common case in a single call. |
| `check_availability` | `urls: list[str]` | Check whether URLs are online (and their size) WITHOUT queueing them. Resolves in the linkgrabber, reports per-link availability, then cleans up. |
| `find` | `query: str` | Find packages by name substring across both lists -> uuid + stage. |
### Accounts / premium hosters (6)
| Tool | Parameters | Description |
|---|---|---|
| `list_accounts` | — | List premium/hoster accounts: hostname, username, valid, traffic left, expiry (-1 = unlimited/none), enabled, uuid. |
| `list_premium_hosters` | `filter: str \| None`, `limit: int = 100` | List hosters JDownloader supports (optionally filtered by substring). |
| `add_account` | `hoster: str`, `username: str`, `password: str` | Add a premium/hoster account. The password is a hoster credential supplied by the user; it travels through the My.JDownloader relay (end-to-end encrypted) to your JDownloader instance and is never stored by this server. |
| `remove_accounts` | `account_ids: list[str]` | Remove hoster accounts by uuid. Irreversible — re-adding requires the credentials again. |
| `set_account_enabled` | `enabled: bool`, `account_ids: list[str]` | Enable or disable hoster accounts by uuid. Disabling can stop in-flight premium downloads that rely on the account. |
| `refresh_accounts` | `account_ids: list[str] \| None` | Refresh hoster account status/traffic (all accounts if none given). |
### System / control (5)
| Tool | Parameters | Description |
|---|---|---|
| `get_storage_info` | — | Free/total disk per drive (with `free_gb` and `used_pct`) — check before a large download. |
| `restart_jd` | — | Restart the JDownloader application (recovery). |
| `get_speed_limit` | — | Current download speed-limit state: enabled + limit in bytes/s. |
| `set_speed_limit` | `enabled: bool`, `bytes_per_sec: int \| None` | Enable/disable the download speed limit and optionally set it (bytes/s). |
| `reconnect` | — | Trigger a router reconnect to get a new IP (resets some hoster limits). |
### Captcha — human-in-the-loop (3)
| Tool | Parameters | Description |
|---|---|---|
| `list_captchas` | — | List captchas JDownloader is currently waiting on. |
| `get_captcha` | `captcha_id: str` | Fetch a pending captcha (image data) for a human to solve. |
| `answer_captcha` | `captcha_id: str`, `solution: str` | Submit a human-provided solution for a pending captcha. |
### Config (generic settings) (3)
| Tool | Parameters | Description |
|---|---|---|
| `get_config` | `interface: str`, `key: str` | Read one JDownloader config value. `interface` is a settings interface name (e.g. `org.jdownloader.settings.GeneralSettings`); find keys with `list_config`. |
| `list_config` | `filter: str \| None`, `limit: int = 200` | List JD config entries (interface/key/value), optionally filtered by substring. JD exposes ~2200 keys — always pass a filter and expect `total` to exceed `limit`. |
| `set_config` | `interface: str`, `key: str`, `value: str \| int \| bool` | Write one JDownloader config value; returns the re-read value. Use `list_config`/`get_config` first to confirm the exact interface + key. |
### Cleanup, update & diagnostics (8)
| Tool | Parameters | Description |
|---|---|---|
| `clear_linkgrabber` | `backup: bool = True` | Clear the ENTIRE linkgrabber list. With `backup=True` (default) it first saves every link URL to a backup file (returned as `backup_file`) so the wipe is recoverable via `restore_linkgrabber`. |
| `restore_linkgrabber` | `backup_file: str` | Re-add links from a backup file written by `clear_linkgrabber`. |
| `clean_downloads` | `what: str = "finished"` | Remove a category of links from the download list (files stay on disk): `what` = finished \| offline \| failed \| disabled \| all. Use `offline` to purge dead "File not found" links clogging a stuck queue. |
| `diagnose_queue` | — | One-shot health summary of the download queue: totals, unfinished links bucketed by status and host, and pending captcha/dialog counts. The fast way to understand WHY a queue is stuck without dumping every link. |
| `recommend` | — | Turn the current queue state into prioritized, actionable fixes (offline links, anti-bot/plugin hosts, waiting captchas, stopped-with-pending, low disk, bad accounts). Priority 1 = most urgent. The "what should I do?" tool. |
| `monitor` | — | Live monitoring snapshot for polling: overall state + speed, download counts bucketed by category (downloading \| pending \| done \| stopped \| error \| blocked), the currently-active downloads with progress/speed/eta, and which hosts are blocked. |
| `wait_for_change` | `since: str \| None = None`, `timeout_s: int = 600`, `poll_s: int = 15` | Block (server-side) until the queue status changes, then return the new `monitor` snapshot — or `{changed: false, timed_out: true}` after `timeout_s`. The MCP polls internally, so you spend no tokens while waiting and only get woken on a real change. Pass `since` = the last signature you saw; omit to baseline on the current state. Call again after a timeout to keep waiting. |
| `update_jd` | — | Apply an available JDownloader update and restart it. Disruptive — downloads pause during the restart. |
### Variants & organize (5)
| Tool | Parameters | Description |
|---|---|---|
| `get_variants` | `link_id: str` | List the variants (audio/video/quality) of a multi-variant link. |
| `set_variant` | `link_id: str`, `variant_id: str` | Select a variant (from `get_variants`) for a multi-variant link. |
| `rename` | `new_name: str`, `package_id: str \| None`, `link_id: str \| None` | Rename a linkgrabber package (`package_id`) or link (`link_id`). |
| `move_to_new_package` | `new_name: str`, `package_ids: list[str] \| None`, `link_ids: list[str] \| None`, `dest: str \| None`, `stage: str = "linkgrabber"` | Move the given items into a new package named `new_name` in the given stage (linkgrabber \| downloads). |
| `get_download_urls` | `package_ids: list[str] \| None`, `link_ids: list[str] \| None` | Get the resolved original download URLs for the given linkgrabber items. |
### Extensions (2)
| Tool | Parameters | Description |
|---|---|---|
| `list_extensions` | — | List JDownloader extensions (id, name, installed, enabled). |
| `set_extension_enabled` | `extension_id: str`, `enabled: bool` | Enable or disable a JDownloader extension by id (from `list_extensions`). |
### Dialogs (3)
| Tool | Parameters | Description |
|---|---|---|
| `list_dialogs` | — | List interactive dialogs JDownloader is currently waiting on. |
| `get_dialog` | `dialog_id: str` | Fetch a pending dialog's details/options. |
| `answer_dialog` | `dialog_id: str`, `data: dict` | Answer a pending dialog with a data payload (shape per the dialog type). |
### Basic-auth & account credentials (5)
| Tool | Parameters | Description |
|---|---|---|
| `list_basic_auth` | — | List stored FTP/HTTP basic-auth entries. |
| `add_basic_auth` | `auth_type: str`, `hostmask: str`, `username: str`, `password: str` | Add an FTP/HTTP basic-auth entry (`auth_type`: FTP \| HTTP). The password is a user-supplied credential; it travels the My.JDownloader relay (end-to-end encrypted) and is never stored by this server. |
| `remove_basic_auth` | `auth_ids: list[str]` | Remove FTP/HTTP basic-auth entries by id. |
| `set_account_credentials` | `account_id: str`, `username: str`, `password: str` | Update a hoster account's username/password. The password is a user-supplied credential; it travels the My.JDownloader relay (end-to-end encrypted) and is never stored by this server. |
| `get_premium_hoster_url` | `hoster: str` | Get the signup/premium URL for a hoster. |
### Escape hatch (1)
| Tool | Parameters | Description |
|---|---|---|
| `raw_action` | `path: str`, `params: list \| None`, `http_action: str = "POST"` | Call any JD API endpoint not wrapped above (e.g. premium accounts, captcha, reconnect, speed-limit). `path` like `/downloadsV2/setPriority`; `params` is the positional argument list that endpoint expects. |
### yt-dlp backend (2)
| Tool | Parameters | Description |
|---|---|---|
| `ytdlp_download` | `url: str`, `dest: str \| None`, `audio_only: bool = False`, `cookies_from_browser: str \| None`, `cookies_file: str \| None` | Download a URL directly via yt-dlp (thousands of video/social sites); bypasses JDownloader. For private/login-gated content pass `cookies_from_browser` (chrome \| firefox \| edge \| ... — browser must be CLOSED on Windows) or `cookies_file` (a Netscape cookies.txt path — works while the browser is open). |
| `smart_download` | `url: str`, `dest: str \| None`, `audio_only: bool = False`, `backend: str \| None`, `cookies_from_browser: str \| None`, `cookies_file: str \| None`, `jd_wait_s: int = 45` | Known video/social sites (tiktok, youtube, x, instagram, reddit, ...) go straight to yt-dlp. Everything else is tried on JDownloader **first**; if the link breaks within `jd_wait_s` (offline / error / plugin-outdated / stalled) it auto-falls-back to yt-dlp. Override with `backend='ytdlp'\|'jd'` to force one engine (strict, no fallback). |
16 + 3 + 6 + 5 + 3 + 3 + 8 + 5 + 2 + 3 + 5 + 1 + 2 = **62 tools**.
## Agent lifecycle
JDownloader keeps **two separate lists**. Understanding this is the key to
using the tools correctly:
1. **Linkgrabber** — a staging area. `add_links` / `add_container` drop URLs
here; JD resolves them (checks online/offline, size, host) in the
background. Inspect with `list_linkgrabber`.
2. **Downloads queue** — the actual download engine. Nothing downloads until
an item is here. Promote resolved linkgrabber items with
`move_to_downloads`, or add with `autostart=True` / use the `download`
tool to skip straight there. Inspect and poll with `list_downloads` /
`status` / `wait_for`.
`uuid` values returned by any `list_*` tool are the IDs (`package_ids` /
`link_ids`) that every other tool expects — there is no other way to get an
ID.
**Config surface:** JD exposes its full ~2,200-key settings tree via
`get_config` / `list_config` / `set_config`. Don't try to enumerate it —
start with `list_config(filter=...)` to find the interface/key you need, then
`get_config`/`set_config` on that exact pair. **List cleanup:** once you're
done with a package, `clear_linkgrabber` wipes the whole staging list —
by default it backs up every link URL first (`backup=True`), so the wipe is
recoverable via `restore_linkgrabber(backup_file)` — and `clean_downloads`
(what = finished \| offline \| failed \| disabled \| all) removes a category
of entries from the download queue (files stay on disk) — both are separate
from the per-item `remove` tool above. **Stuck queue?** Call `diagnose_queue`
first — it buckets unfinished links by status/host and surfaces pending
captcha/dialog counts in one call, before you reach for
`list_downloads`/`list_captchas` individually. Then call `recommend()` for
the quickest path to an actionable next step — it turns that same queue
state (offline links, anti-bot/plugin hosts, waiting captchas,
stopped-with-pending, low disk, bad accounts) into a prioritized fix list. **Watching a queue over
time?** `monitor` gives the same status/host buckets plus per-download
progress/speed/eta for active items — it's the one to poll on an interval
instead of re-calling `diagnose_queue`/`list_downloads` separately. **Token-free
waiting:** instead of calling `monitor()` on a timer, call `wait_for_change`
(optionally with `since` = the last snapshot's signature) — it blocks
server-side and only returns when the queue actually changes or the timeout
elapses, so you don't burn tokens on unchanged snapshots.
```mermaid
stateDiagram-v2
[*] --> Linkgrabber: add_links / add_container
Linkgrabber --> Linkgrabber: list_linkgrabber while resolving
Linkgrabber --> Downloads: move_to_downloads
Linkgrabber --> [*]: remove to discard
Downloads --> Downloads: control_downloads / force_start / set_priority
Downloads --> Downloads: list_downloads / status / wait_for to poll
Downloads --> Finished: wait_for returns finished
Downloads --> Failed: wait_for returns failed or stalled
Finished --> [*]: remove / get_directories
Failed --> Downloads: reset to retry
Failed --> [*]: remove
```
**Polling pattern:** `list_downloads` / `status` are the workhorses — call
them as often as you like. `wait_for` is a bounded convenience (hard-capped
at 120s server-side); for anything longer, loop: call `wait_for`, and if it
returns `still_running`, call it again. A `stalled` result means the item is
idle with no progress (captcha / reconnect needed / all links offline) —
don't re-poll blindly, investigate first (e.g. `list_captchas`).
### "Just download this" (fire and forget)
```
1. download(urls=[...]) # add + auto-queue + start in one call
2. wait_for(package_id) # poll until finished/failed/stalled
3. get_directories(package_ids) # confirm where the files landed
```
For a fast preflight check without committing to a download, use
`check_availability(urls)` first — it reports online/offline + size and
leaves nothing queued (`timed_out` flags if the check itself ran out of time).
### "Careful lifecycle" (inspect before committing)
```
1. add_links(urls, package="my-set")
2. list_linkgrabber() # confirm links resolved online, get uuid
3. move_to_downloads(package_ids=[uuid])
4. force_start(package_ids=[uuid]) # or control_downloads("start")
5. wait_for(package_id=uuid)
6. remove(stage="downloads", package_ids=[uuid]) # cleanup once finished
```
### "Manage premium accounts"
```
1. list_accounts() # see current hosters, validity, traffic
2. add_account(hoster, username, password) # add a new one, or:
set_account_enabled(enabled, account_ids) # temporarily disable one, or:
refresh_accounts(account_ids) # re-check traffic/validity
```
### "Precondition checks" (before a large or unattended download)
```
1. get_storage_info() # confirm free_gb on the target drive
2. list_accounts() # confirm the needed hoster account is valid/enabled
3. download(urls, dest=...)
```
## Downloading video / social (yt-dlp)
`smart_download(url)` is JDownloader-first with an auto-fallback to yt-dlp.
Known video/social hosts (tiktok, youtube, x/twitter, instagram, facebook,
reddit, vimeo, twitch, soundcloud, ...) route straight to `ytdlp_download`.
Everything else is tried on JDownloader's `download` first; if that link
breaks within `jd_wait_s` seconds (default 45 — offline / error /
plugin-outdated / stalled), the failed JD entry is auto-removed and the URL
falls back to `ytdlp_download`. The result carries `"routed_to": "jd"` when
JD is handling it, or `"routed_to": "jd->ytdlp"` plus `jd_outcome`/`jd_reason`
when a fallback happened. Override with `backend='ytdlp'|'jd'` to force one
engine (strict, no fallback), or call `ytdlp_download` directly to skip the
router.
**Private/login-gated content** (your own TikTok videos, members-only posts)
needs cookies — pick one:
- `cookies_from_browser='chrome'|'firefox'|'edge'|...` — reads that browser's
cookie store directly. On Windows the browser must be **closed** first (it
locks the cookie DB while running).
- `cookies_file='path/to/cookies.txt'` — a Netscape-format cookies file
exported via a "Get cookies.txt" browser extension. Works while the browser
stays open.
**ffmpeg must be on PATH** for `audio_only=True` (audio extraction) and for
merging separate video+audio streams on formats that need it — yt-dlp will
otherwise fail or fall back to a lower-quality single-stream format.
## Connection modes
Set via `MYJD_CONNECT_MODE` in `.env` (default `relay`):
- **`relay`** (default) — connects via My.JDownloader, which works through
NAT with no port-forwarding. `myjdapi` automatically upgrades data calls to
a direct LAN connection when JD advertises one, so only the initial
handshake actually touches the cloud. Uses `MYJD_EMAIL` / `MYJD_PASSWORD` /
`MYJD_DEVICE`.
- **`direct`** — pure-local via JDownloader's built-in RemoteAPI, no cloud
account involved. Set `MYJD_CONNECT_MODE=direct` plus `MYJD_DIRECT_IP`
(default `127.0.0.1`) / `MYJD_DIRECT_PORT` (default `3128`). Requires
enabling RemoteAPI in JDownloader first (**Settings → Advanced Settings**,
search "RemoteAPI" / "Deprecated"). It's a deprecated, more limited API
that may not support every tool here — `relay` is the full-featured
default; only use `direct` if you specifically need to avoid the cloud
account.
## Setup
1. In JDownloader: pick a connection mode (see above). For the default
`relay` mode, go to **Settings → My.JDownloader**, sign in / link the
device, note its **device name**. For `direct` mode, instead enable
RemoteAPI (**Settings → Advanced Settings**, search "RemoteAPI").
2. Configure creds:
```bash
cp .env.example .env # then edit .env
```
3. Install deps (already done if you used `uv`):
```bash
uv sync
```
Deps: `mcp`, `myjdapi`, `requests`, `yt-dlp`. For the yt-dlp backend, also
put `ffmpeg` on PATH (needed for `audio_only` extraction and video+audio
merges) — JDownloader-only usage doesn't need it.
## Verify
```bash
uv run pytest # mocked unit tests (no JD needed)
uv run python smoke_test.py # live end-to-end against your JD2 (uses .env)
```
The smoke test walks the whole lifecycle with a tiny file and cleans up. It
SKIPs cleanly if the env vars aren't set.
## Register with Claude Code
```bash
claude mcp add jdownloader -- uv run --directory E:/SWE-Pioneers/projects/jdownloader-mcp jdownloader-mcp
```
or add to your MCP client config (stdio):
```json
{
"mcpServers": {
"jdownloader": {
"command": "uv",
"args": ["run", "--directory", "E:/SWE-Pioneers/projects/jdownloader-mcp", "jdownloader-mcp"]
}
}
}
```
The server reads `MYJD_CONNECT_MODE` plus either the relay creds
(`MYJD_EMAIL` / `MYJD_PASSWORD` / `MYJD_DEVICE`) or the direct-mode settings
(`MYJD_DIRECT_IP` / `MYJD_DIRECT_PORT`) from `.env` (or the environment). See
[Connection modes](#connection-modes).
## Docs
- `docs/2026-08-01-jdownloader-mcp-design.md` — design spec.
- `docs/myjdapi-api-reference.md` — exact installed-API reference the code targets.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 17 tools
Each tool targets a distinct operation or stage (linkgrabber vs downloads vs global controller). The only adjacent tools, force_start and control_downloads, have clearly different scopes (specific items vs global state).
Most tools follow a consistent verb_noun pattern (add_links, list_downloads, set_priority). Minor deviations: status, wait_for, remove, and reset are shorter but still intuitive; no mixing of styles.
At 17 tools, the set is on the heavier side but each tool covers a meaningful function in the download lifecycle. The count is reasonable for a full-featured download manager.
The toolkit covers the full workflow: adding, collecting, moving, controlling, prioritizing, configuring directories/extraction, and removing/resetting. The raw_action fallback ensures no endpoint is unreachable, making the surface effectively complete.