Skip to main content

qbt-kit

CI status Latest PyPI version Supported Python versions MIT licence

qbt-kit

A complete, typed Python client for the qBittorrent WebUI API — with a CLI that covers every endpoint one-to-one, and an MCP server so coding agents can drive it without deleting your data.

$ qbt coverage

qbt-kit coverage — target release-5.2.3

  scope               n   spec  progress
  torrents           61     61  ████████████████████  61/61
  app                16     16  ████████████████████  16/16
  rss                14     14  ████████████████████  14/14
  search             11     11  ████████████████████  11/11
  transfer            9      9  ████████████████████  9/9
  torrentcreator      4      4  ████████████████████  4/4
  auth                2      2  ████████████████████  2/2
  clientdata          2      2  ████████████████████  2/2
  log                 2      2  ████████████████████  2/2
  sync                2      2  ████████████████████  2/2
                   ----
  TOTAL             123    123  ████████████████████  100.0%

The ledger is generated from qBittorrent's own source, and CI fails if it slips.

Highlights

  • All 123 endpoints of qBittorrent 5.2.3, each one a spec entry that generates the client method, the CLI command and the docs together.
  • A verifiable coverage claim. The endpoint list is extracted from the qBittorrent C++ source per release tag and committed, so CI fails both when the spec misses an endpoint and when it invents one.
  • An MCP server with 15 tools — 13 task-shaped, plus qbt_call and list_endpoints — and safety gates that hold even through the escape hatch.
  • Compatibility measured, not assumed — tested against real 4.6, 5.0, 5.1 and 5.2 containers, with a rename table so torrents/stop resolves to pause on servers that predate the rename.
  • Typed throughout, no runtime dependency beyond httpx and typer.

That coverage claim is worth a sentence of context. The wiki page most clients are written against documents 78 endpoints. qBittorrent 5.2.3 ships 123, and two whole scopes — torrentcreator and clientdata — appear in no wiki page at all.

Installation

PyPI's qbt name belongs to an unrelated package, so the CLI is run via --from:

uvx --from qbt-kit qbt doctor
uvx --from "qbt-kit[mcp]" qbt-mcp --help   # the server lives in the `mcp` extra

Both run via --from because the qbt name on PyPI belongs to an unrelated package, and qbt-mcp is a script inside this one rather than a package of its own — uvx qbt-mcp resolves to nothing.

Or add it to a project:

uv add qbt-kit              # library and CLI
uv add "qbt-kit[mcp]"       # with the MCP server

As a Claude Code plugin

This repository is also a plugin marketplace, so one line installs the MCP server and the skill together:

/plugin install qbt-kit --marketplace manueltarouca/qbt-kit

The bundled MCP server runs from the plugin's own checkout, so it needs uv but not a published package, and it always matches the version you installed. Point it at your server with QBT_URL, and see Configuration for credentials.

Quickstart

qbt doctor                          # version, auth mode, endpoint availability
qbt torrents info --category tv     # the API calls this `info`, not `list`
qbt torrents add --urls <magnet> --category tv --auto-tmm
qbt health                          # swarm health across the queue
qbt coverage                        # the endpoint coverage ledger
qbt call torrents/pieceStates -p hash=<h>    # raw, reaches all 123

Scope commands mirror the API exactly — torrents info, not a friendlier list — so anything you read in qBittorrent's own docs translates directly. --json works everywhere, so every command composes with jq.

qbt ready is also there, but it inspects the local filesystem to decide whether a download has really settled, so it only means anything when the CLI runs on the same machine as qBittorrent.

MCP server

{
  "mcpServers": {
    "qbittorrent": {
      "command": "uvx",
      "args": ["--from", "qbt-kit[mcp]", "qbt-mcp", "--url", "http://127.0.0.1:8080"]
    }
  }
}

Fifteen tools rather than 123 — a tool list that long costs context on every turn and measurably worsens which tool a model picks. qbt_call keeps every endpoint reachable and list_endpoints lets a model discover them, so the small surface stays complete.

Configuration

Flags first, then the environment, then the macOS Keychain:

variable meaning
QBT_URL base URL, default http://127.0.0.1:8080
QBT_API_KEY API key (qBittorrent 5.2+); preferred when present
QBT_USERNAME WebUI username, for cookie login on any version
QBT_PASSWORD WebUI password
QBT_READ_ONLY 1/true/yes to refuse everything that changes state

Keychain fallback uses service qbittorrent, account api-key for a key and webui for the password:

security add-generic-password -s qbittorrent -a api-key -w '<key>'

qBittorrent has no OAuth, so there is no token flow to delegate and the secret is long-lived. The reference MCP servers put credentials straight into the client's env block — GITHUB_PERSONAL_ACCESS_TOKEN is the canonical example — but that file is plaintext and frequently committed. Prefer the Keychain, and if you do use env, reference a variable instead of pasting the secret:

"env": {
  "QBT_URL": "${QBT_URL:-http://127.0.0.1:8080}",
  "QBT_API_KEY": "${QBT_API_KEY:-}"      // empty falls through to the Keychain
}

The :- default matters. Claude Code passes an unset ${VAR} through as the literal text ${VAR} rather than as empty, which otherwise surfaces as a confusing 401 much later — so qbt-mcp detects that case at startup and says so plainly instead of starting.

On startup it reports, on stderr, where the credentials came from and never what they are:

qbt-mcp: http://127.0.0.1:8080 · WebAPI 2.15.1 · auth=bearer via macOS Keychain

Safety

An agent driving a torrent client can destroy data that took weeks to collect, so the gates are code rather than guidance:

  • Destructive endpoints require confirm; delete_files is never implied.
  • --read-only (or QBT_READ_ONLY=1) blocks mutations, including via qbt_call.
  • app/setPreferences refuses the WebUI, auth and network keys that would sever the connection or weaken security. There is no override.
  • Refusals return a structured cause and remedy, so an agent can correct itself instead of seeing an opaque tool error.

qbt-mcp speaks stdio by default, which keeps it a child process with nothing listening. The sse and streamable-http transports expose an unauthenticated control surface holding your qBittorrent credentials — bound to loopback, but do not forward those ports. See SECURITY.md.

Documentation

docs/TRACEABILITY.md how the coverage claim is built and gated
docs/TESTING.md the container harness, and behaviours that cost real time to learn
docs/PLAN.md the design and the reasoning behind it
docs/ROADMAP.md what is done and what is not
docs/REVIEW-1.md an independent review of the plan, with every finding dispositioned
docs/dependencies.md every dependency, and why it is justified
SECURITY.md threat model, credential handling, how to report a vulnerability

A Claude skill ships in skills/qbt-kit/ covering CLI usage and the swarm-health rule.

Field notes

Things that cost us time, documented so they cost you none.

  • Login success cannot be read from the status code. qBittorrent ≤5.1 answers a failed login with HTTP 200 and the body Fails.; 5.2 returns an empty 204. The only rule that holds across versions is whether a session cookie was issued.
  • app/webapiVersion requires authentication. Only auth/login is public, so you cannot read the version to decide how to authenticate. Probe instead.
  • The session cookie is not always SID. On 5.2 it is QBT_SID_<port>; on 5.0/5.1 it is user-configurable. Never match on the name.
  • A bare 401 is often Host-header validation, not bad credentials. It is on by default and rejects any request whose Host port differs from the port qBittorrent listens on — which is what happens behind mapped container ports, SSH tunnels and most reverse proxies.
  • API keys exist only from qBittorrent 5.2.0. Sending Authorization: Bearer to 5.0/5.1 gets a 403 that looks like a credentials problem.
  • pause/resume were removed in 5.0, not aliased; they 404 there.
  • progress: 1.0 does not mean every file is on disk. Progress is computed over the selected files, so a torrent with files at priority 0 reports 100% having never fetched them. size < total_size is the cheap flag, from the same listing call.
  • A category alone does not place a torrent. qBittorrent only applies a category's save path under automatic torrent management, so torrents/add with a category but without autoTMM files it in the global save path.
  • A stalled torrent is usually not slow but stuck. availability is how many complete copies exist across reachable peers; when it does not exceed progress, no peer has the missing pieces. But a queued torrent reports availability 0 simply for not being connected — judging those as dead is how you delete healthy downloads.

Contributing

See CONTRIBUTING.md. The short version: endpoints are added to the spec under src/qbtkit/spec/, never by hand to the CLI or MCP, and tools/build_coverage.py --strict has to stay green.

Acknowledgements

  • qBittorrent — and specifically its src/webui/api/*controller.cpp, which is the only complete and accurate description of this API that exists.
  • qbittorrent-api is the mature, well-maintained Python library for this API, and if a library is all you need, use it. qbt-kit exists for the CLI, the MCP server and the verifiable coverage claim, not because anything is wrong with it.

Licence

MIT — see LICENSE.

Metadata

Release files for qbt-kit 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for qbt-kit 0.1.2
File Size Uploaded
qbt_kit-0.1.2.tar.gz 47.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for qbt-kit 0.1.2
File Interpreter ABI Platform
qbt_kit-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 102.6 kB

Release files / qbt_kit-0.1.2.tar.gz

Download URL qbt_kit-0.1.2.tar.gz
Size 47.0 kB
Tags Source
SHA-256 checksum
How to use checksums
063a9e2d61a496749b0e1916b4c2587717351026450400c6893c563c616aa015
BLAKE2b-256 checksum
How to use checksums
04059319eb7bdc8513da52954f46ad4109d91326d778330f4a90915cb93f0e0f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / qbt_kit-0.1.2-py3-none-any.whl

Download URL qbt_kit-0.1.2-py3-none-any.whl
Size 55.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9644d939ac44f0f1f9a14720bb91c0bd0a2c870c1321ba2710e3c51a8208c637
BLAKE2b-256 checksum
How to use checksums
89b6ebb48ba4a510cf4dddae0570de2a6ec87bb82ecfd6a8318025a009f3b7b7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page