qbt-kit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qbt_kit-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
qbt_kit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,288 @@
1
+ Metadata-Version: 2.4
2
+ Name: qbt-kit
3
+ Version: 0.1.0
4
+ Summary: Spec-driven client, 1:1 CLI and MCP server for the qBittorrent WebUI API
5
+ Keywords: qbittorrent,torrent,cli,mcp,webui-api
6
+ Author: mt
7
+ Author-email: mt <manuel+git@matm.xyz>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Communications :: File Sharing
18
+ Requires-Dist: httpx>=0.28
19
+ Requires-Dist: typer>=0.27
20
+ Requires-Dist: mcp>=2.3 ; extra == 'mcp'
21
+ Requires-Dist: pydantic>=2.0 ; extra == 'mcp'
22
+ Requires-Python: >=3.11
23
+ Project-URL: Homepage, https://github.com/manueltarouca/qbt-kit
24
+ Project-URL: Issues, https://github.com/manueltarouca/qbt-kit/issues
25
+ Project-URL: Changelog, https://github.com/manueltarouca/qbt-kit/blob/main/CHANGELOG.md
26
+ Project-URL: Documentation, https://github.com/manueltarouca/qbt-kit#readme
27
+ Provides-Extra: mcp
28
+ Description-Content-Type: text/markdown
29
+
30
+ # qbt-kit
31
+
32
+ <a href="https://github.com/manueltarouca/qbt-kit/actions/workflows/ci.yaml"><img src="https://github.com/manueltarouca/qbt-kit/actions/workflows/ci.yaml/badge.svg" alt="CI status" /></a>
33
+ <a href="https://pypi.org/project/qbt-kit/"><img src="https://img.shields.io/pypi/v/qbt-kit.svg" alt="Latest PyPI version" /></a>
34
+ <a href="https://pypi.org/project/qbt-kit/"><img src="https://img.shields.io/pypi/pyversions/qbt-kit.svg" alt="Supported Python versions" /></a>
35
+ <a href="LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="MIT licence" /></a>
36
+
37
+ <p align="center">
38
+ <picture>
39
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo-dark.png">
40
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo.png">
41
+ <img alt="qbt-kit" src="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo.png" width="180">
42
+ </picture>
43
+ </p>
44
+
45
+ A complete, typed Python client for the qBittorrent WebUI API — with a CLI that
46
+ covers every endpoint one-to-one, and an MCP server so coding agents can drive
47
+ it without deleting your data.
48
+
49
+ ```
50
+ $ qbt coverage
51
+
52
+ qbt-kit coverage — target release-5.2.3
53
+
54
+ scope n spec progress
55
+ torrents 61 61 ████████████████████ 61/61
56
+ app 16 16 ████████████████████ 16/16
57
+ rss 14 14 ████████████████████ 14/14
58
+ search 11 11 ████████████████████ 11/11
59
+ transfer 9 9 ████████████████████ 9/9
60
+ torrentcreator 4 4 ████████████████████ 4/4
61
+ auth 2 2 ████████████████████ 2/2
62
+ clientdata 2 2 ████████████████████ 2/2
63
+ log 2 2 ████████████████████ 2/2
64
+ sync 2 2 ████████████████████ 2/2
65
+ ----
66
+ TOTAL 123 123 ████████████████████ 100.0%
67
+ ```
68
+
69
+ <p align="center">
70
+ <i>The ledger is generated from qBittorrent's own source, and CI fails if it slips.</i>
71
+ </p>
72
+
73
+ ## Highlights
74
+
75
+ - **All 123 endpoints** of qBittorrent 5.2.3, each one a spec entry that
76
+ generates the client method, the CLI command and the docs together.
77
+ - **A verifiable coverage claim.** The endpoint list is extracted from the
78
+ qBittorrent C++ source per release tag and committed, so CI fails both when
79
+ the spec misses an endpoint and when it invents one.
80
+ - **An MCP server** with 15 tools — 13 task-shaped, plus `qbt_call` and
81
+ `list_endpoints` — and safety gates that hold even through the escape hatch.
82
+ - **Compatibility measured, not assumed** — tested against real 4.6, 5.0, 5.1
83
+ and 5.2 containers, with a rename table so `torrents/stop` resolves to
84
+ `pause` on servers that predate the rename.
85
+ - **Typed throughout**, no runtime dependency beyond `httpx` and `typer`.
86
+
87
+ That coverage claim is worth a sentence of context. The wiki page most clients
88
+ are written against documents **78** endpoints. qBittorrent 5.2.3 ships
89
+ **123**, and two whole scopes — `torrentcreator` and `clientdata` — appear in
90
+ no wiki page at all.
91
+
92
+ ## Installation
93
+
94
+ PyPI's `qbt` name belongs to an unrelated package, so the CLI is run via
95
+ `--from`:
96
+
97
+ ```sh
98
+ uvx --from qbt-kit qbt doctor
99
+ uvx qbt-mcp --help # the MCP server ships under its own name
100
+ ```
101
+
102
+ Or add it to a project:
103
+
104
+ ```sh
105
+ uv add qbt-kit # library and CLI
106
+ uv add "qbt-kit[mcp]" # with the MCP server
107
+ ```
108
+
109
+ ### As a Claude Code plugin
110
+
111
+ This repository is also a plugin marketplace, so one line installs the MCP
112
+ server and the skill together:
113
+
114
+ ```
115
+ /plugin install qbt-kit --marketplace manueltarouca/qbt-kit
116
+ ```
117
+
118
+ The bundled MCP server runs from the plugin's own checkout, so it needs `uv`
119
+ but not a published package, and it always matches the version you installed.
120
+ Point it at your server with `QBT_URL`, and see [Configuration](#configuration)
121
+ for credentials.
122
+
123
+ ## Quickstart
124
+
125
+ ```sh
126
+ qbt doctor # version, auth mode, endpoint availability
127
+ qbt torrents info --category tv # the API calls this `info`, not `list`
128
+ qbt torrents add --urls <magnet> --category tv --auto-tmm
129
+ qbt health # swarm health across the queue
130
+ qbt coverage # the endpoint coverage ledger
131
+ qbt call torrents/pieceStates -p hash=<h> # raw, reaches all 123
132
+ ```
133
+
134
+ Scope commands mirror the API exactly — `torrents info`, not a friendlier
135
+ `list` — so anything you read in qBittorrent's own docs translates directly.
136
+ `--json` works everywhere, so every command composes with `jq`.
137
+
138
+ `qbt ready` is also there, but it inspects the local filesystem to decide
139
+ whether a download has really settled, so it only means anything when the CLI
140
+ runs on the same machine as qBittorrent.
141
+
142
+ ## MCP server
143
+
144
+ ```jsonc
145
+ {
146
+ "mcpServers": {
147
+ "qbittorrent": {
148
+ "command": "uvx",
149
+ "args": ["qbt-mcp", "--url", "http://127.0.0.1:8080"]
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ Fifteen tools rather than 123 — a tool list that long costs context
156
+ on every turn and measurably worsens which tool a model picks. `qbt_call` keeps
157
+ every endpoint reachable and `list_endpoints` lets a model discover them, so the
158
+ small surface stays complete.
159
+
160
+ ### Configuration
161
+
162
+ Flags first, then the environment, then the macOS Keychain:
163
+
164
+ | variable | meaning |
165
+ |---|---|
166
+ | `QBT_URL` | base URL, default `http://127.0.0.1:8080` |
167
+ | `QBT_API_KEY` | API key (qBittorrent 5.2+); preferred when present |
168
+ | `QBT_USERNAME` | WebUI username, for cookie login on any version |
169
+ | `QBT_PASSWORD` | WebUI password |
170
+ | `QBT_READ_ONLY` | `1`/`true`/`yes` to refuse everything that changes state |
171
+
172
+ Keychain fallback uses service `qbittorrent`, account `api-key` for a key and
173
+ `webui` for the password:
174
+
175
+ ```sh
176
+ security add-generic-password -s qbittorrent -a api-key -w '<key>'
177
+ ```
178
+
179
+ **qBittorrent has no OAuth**, so there is no token flow to delegate and the
180
+ secret is long-lived. The reference MCP servers put credentials straight into
181
+ the client's `env` block — `GITHUB_PERSONAL_ACCESS_TOKEN` is the canonical
182
+ example — but that file is plaintext and frequently committed. Prefer the
183
+ Keychain, and if you do use `env`, reference a variable instead of pasting the
184
+ secret:
185
+
186
+ ```jsonc
187
+ "env": {
188
+ "QBT_URL": "${QBT_URL:-http://127.0.0.1:8080}",
189
+ "QBT_API_KEY": "${QBT_API_KEY:-}" // empty falls through to the Keychain
190
+ }
191
+ ```
192
+
193
+ The `:-` default matters. Claude Code passes an unset `${VAR}` through as the
194
+ *literal text* `${VAR}` rather than as empty, which otherwise surfaces as a
195
+ confusing 401 much later — so `qbt-mcp` detects that case at startup and says
196
+ so plainly instead of starting.
197
+
198
+ On startup it reports, on stderr, where the credentials came from and never
199
+ what they are:
200
+
201
+ ```
202
+ qbt-mcp: http://127.0.0.1:8080 · WebAPI 2.15.1 · auth=bearer via macOS Keychain
203
+ ```
204
+
205
+ ### Safety
206
+
207
+ An agent driving a torrent client can destroy data that took weeks to collect,
208
+ so the gates are code rather than guidance:
209
+
210
+ - Destructive endpoints require `confirm`; `delete_files` is never implied.
211
+ - `--read-only` (or `QBT_READ_ONLY=1`) blocks mutations, **including via
212
+ `qbt_call`**.
213
+ - `app/setPreferences` refuses the WebUI, auth and network keys that would
214
+ sever the connection or weaken security. There is no override.
215
+ - Refusals return a structured cause and remedy, so an agent can correct itself
216
+ instead of seeing an opaque tool error.
217
+
218
+ `qbt-mcp` speaks stdio by default, which keeps it a child process with nothing
219
+ listening. The `sse` and `streamable-http` transports expose an
220
+ *unauthenticated* control surface holding your qBittorrent credentials — bound
221
+ to loopback, but do not forward those ports. See [SECURITY.md](https://github.com/manueltarouca/qbt-kit/blob/main/SECURITY.md).
222
+
223
+ ## Documentation
224
+
225
+ | | |
226
+ |---|---|
227
+ | [`docs/TRACEABILITY.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/TRACEABILITY.md) | how the coverage claim is built and gated |
228
+ | [`docs/TESTING.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/TESTING.md) | the container harness, and behaviours that cost real time to learn |
229
+ | [`docs/PLAN.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/PLAN.md) | the design and the reasoning behind it |
230
+ | [`docs/ROADMAP.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/ROADMAP.md) | what is done and what is not |
231
+ | [`docs/REVIEW-1.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/REVIEW-1.md) | an independent review of the plan, with every finding dispositioned |
232
+ | [`docs/dependencies.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/dependencies.md) | every dependency, and why it is justified |
233
+ | [`SECURITY.md`](https://github.com/manueltarouca/qbt-kit/blob/main/SECURITY.md) | threat model, credential handling, how to report a vulnerability |
234
+
235
+ A Claude skill ships in [`skills/qbt-kit/`](https://github.com/manueltarouca/qbt-kit/tree/main/skills/qbt-kit/) covering CLI usage
236
+ and the swarm-health rule.
237
+
238
+ ## Field notes
239
+
240
+ Things that cost us time, documented so they cost you none.
241
+
242
+ - **Login success cannot be read from the status code.** qBittorrent ≤5.1
243
+ answers a *failed* login with HTTP 200 and the body `Fails.`; 5.2 returns an
244
+ empty 204. The only rule that holds across versions is whether a session
245
+ cookie was issued.
246
+ - **`app/webapiVersion` requires authentication.** Only `auth/login` is public,
247
+ so you cannot read the version to decide how to authenticate. Probe instead.
248
+ - **The session cookie is not always `SID`.** On 5.2 it is `QBT_SID_<port>`;
249
+ on 5.0/5.1 it is user-configurable. Never match on the name.
250
+ - **A bare 401 is often Host-header validation, not bad credentials.** It is on
251
+ by default and rejects any request whose `Host` port differs from the port
252
+ qBittorrent listens on — which is what happens behind mapped container ports,
253
+ SSH tunnels and most reverse proxies.
254
+ - **API keys exist only from qBittorrent 5.2.0.** Sending `Authorization:
255
+ Bearer` to 5.0/5.1 gets a 403 that looks like a credentials problem.
256
+ - **`pause`/`resume` were removed in 5.0**, not aliased; they 404 there.
257
+ - **`progress: 1.0` does not mean every file is on disk.** Progress is computed
258
+ over the *selected* files, so a torrent with files at priority 0 reports 100%
259
+ having never fetched them. `size < total_size` is the cheap flag, from the
260
+ same listing call.
261
+ - **A category alone does not place a torrent.** qBittorrent only applies a
262
+ category's save path under automatic torrent management, so `torrents/add`
263
+ with a category but without `autoTMM` files it in the global save path.
264
+ - **A stalled torrent is usually not slow but stuck.** `availability` is how
265
+ many complete copies exist across reachable peers; when it does not exceed
266
+ `progress`, no peer has the missing pieces. But a *queued* torrent reports
267
+ availability 0 simply for not being connected — judging those as dead is how
268
+ you delete healthy downloads.
269
+
270
+ ## Contributing
271
+
272
+ See [CONTRIBUTING.md](https://github.com/manueltarouca/qbt-kit/blob/main/CONTRIBUTING.md). The short version: endpoints are added
273
+ to the spec under `src/qbtkit/spec/`, never by hand to the CLI or MCP, and
274
+ `tools/build_coverage.py --strict` has to stay green.
275
+
276
+ ## Acknowledgements
277
+
278
+ - [qBittorrent](https://github.com/qbittorrent/qBittorrent) — and specifically
279
+ its `src/webui/api/*controller.cpp`, which is the only complete and accurate
280
+ description of this API that exists.
281
+ - [`qbittorrent-api`](https://pypi.org/project/qbittorrent-api/) is the mature,
282
+ well-maintained Python library for this API, and if a library is all you
283
+ need, use it. qbt-kit exists for the CLI, the MCP server and the verifiable
284
+ coverage claim, not because anything is wrong with it.
285
+
286
+ ## Licence
287
+
288
+ MIT — see [LICENSE](https://github.com/manueltarouca/qbt-kit/blob/main/LICENSE).
@@ -0,0 +1,259 @@
1
+ # qbt-kit
2
+
3
+ <a href="https://github.com/manueltarouca/qbt-kit/actions/workflows/ci.yaml"><img src="https://github.com/manueltarouca/qbt-kit/actions/workflows/ci.yaml/badge.svg" alt="CI status" /></a>
4
+ <a href="https://pypi.org/project/qbt-kit/"><img src="https://img.shields.io/pypi/v/qbt-kit.svg" alt="Latest PyPI version" /></a>
5
+ <a href="https://pypi.org/project/qbt-kit/"><img src="https://img.shields.io/pypi/pyversions/qbt-kit.svg" alt="Supported Python versions" /></a>
6
+ <a href="LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="MIT licence" /></a>
7
+
8
+ <p align="center">
9
+ <picture>
10
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo-dark.png">
11
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo.png">
12
+ <img alt="qbt-kit" src="https://raw.githubusercontent.com/manueltarouca/qbt-kit/main/assets/logo.png" width="180">
13
+ </picture>
14
+ </p>
15
+
16
+ A complete, typed Python client for the qBittorrent WebUI API — with a CLI that
17
+ covers every endpoint one-to-one, and an MCP server so coding agents can drive
18
+ it without deleting your data.
19
+
20
+ ```
21
+ $ qbt coverage
22
+
23
+ qbt-kit coverage — target release-5.2.3
24
+
25
+ scope n spec progress
26
+ torrents 61 61 ████████████████████ 61/61
27
+ app 16 16 ████████████████████ 16/16
28
+ rss 14 14 ████████████████████ 14/14
29
+ search 11 11 ████████████████████ 11/11
30
+ transfer 9 9 ████████████████████ 9/9
31
+ torrentcreator 4 4 ████████████████████ 4/4
32
+ auth 2 2 ████████████████████ 2/2
33
+ clientdata 2 2 ████████████████████ 2/2
34
+ log 2 2 ████████████████████ 2/2
35
+ sync 2 2 ████████████████████ 2/2
36
+ ----
37
+ TOTAL 123 123 ████████████████████ 100.0%
38
+ ```
39
+
40
+ <p align="center">
41
+ <i>The ledger is generated from qBittorrent's own source, and CI fails if it slips.</i>
42
+ </p>
43
+
44
+ ## Highlights
45
+
46
+ - **All 123 endpoints** of qBittorrent 5.2.3, each one a spec entry that
47
+ generates the client method, the CLI command and the docs together.
48
+ - **A verifiable coverage claim.** The endpoint list is extracted from the
49
+ qBittorrent C++ source per release tag and committed, so CI fails both when
50
+ the spec misses an endpoint and when it invents one.
51
+ - **An MCP server** with 15 tools — 13 task-shaped, plus `qbt_call` and
52
+ `list_endpoints` — and safety gates that hold even through the escape hatch.
53
+ - **Compatibility measured, not assumed** — tested against real 4.6, 5.0, 5.1
54
+ and 5.2 containers, with a rename table so `torrents/stop` resolves to
55
+ `pause` on servers that predate the rename.
56
+ - **Typed throughout**, no runtime dependency beyond `httpx` and `typer`.
57
+
58
+ That coverage claim is worth a sentence of context. The wiki page most clients
59
+ are written against documents **78** endpoints. qBittorrent 5.2.3 ships
60
+ **123**, and two whole scopes — `torrentcreator` and `clientdata` — appear in
61
+ no wiki page at all.
62
+
63
+ ## Installation
64
+
65
+ PyPI's `qbt` name belongs to an unrelated package, so the CLI is run via
66
+ `--from`:
67
+
68
+ ```sh
69
+ uvx --from qbt-kit qbt doctor
70
+ uvx qbt-mcp --help # the MCP server ships under its own name
71
+ ```
72
+
73
+ Or add it to a project:
74
+
75
+ ```sh
76
+ uv add qbt-kit # library and CLI
77
+ uv add "qbt-kit[mcp]" # with the MCP server
78
+ ```
79
+
80
+ ### As a Claude Code plugin
81
+
82
+ This repository is also a plugin marketplace, so one line installs the MCP
83
+ server and the skill together:
84
+
85
+ ```
86
+ /plugin install qbt-kit --marketplace manueltarouca/qbt-kit
87
+ ```
88
+
89
+ The bundled MCP server runs from the plugin's own checkout, so it needs `uv`
90
+ but not a published package, and it always matches the version you installed.
91
+ Point it at your server with `QBT_URL`, and see [Configuration](#configuration)
92
+ for credentials.
93
+
94
+ ## Quickstart
95
+
96
+ ```sh
97
+ qbt doctor # version, auth mode, endpoint availability
98
+ qbt torrents info --category tv # the API calls this `info`, not `list`
99
+ qbt torrents add --urls <magnet> --category tv --auto-tmm
100
+ qbt health # swarm health across the queue
101
+ qbt coverage # the endpoint coverage ledger
102
+ qbt call torrents/pieceStates -p hash=<h> # raw, reaches all 123
103
+ ```
104
+
105
+ Scope commands mirror the API exactly — `torrents info`, not a friendlier
106
+ `list` — so anything you read in qBittorrent's own docs translates directly.
107
+ `--json` works everywhere, so every command composes with `jq`.
108
+
109
+ `qbt ready` is also there, but it inspects the local filesystem to decide
110
+ whether a download has really settled, so it only means anything when the CLI
111
+ runs on the same machine as qBittorrent.
112
+
113
+ ## MCP server
114
+
115
+ ```jsonc
116
+ {
117
+ "mcpServers": {
118
+ "qbittorrent": {
119
+ "command": "uvx",
120
+ "args": ["qbt-mcp", "--url", "http://127.0.0.1:8080"]
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ Fifteen tools rather than 123 — a tool list that long costs context
127
+ on every turn and measurably worsens which tool a model picks. `qbt_call` keeps
128
+ every endpoint reachable and `list_endpoints` lets a model discover them, so the
129
+ small surface stays complete.
130
+
131
+ ### Configuration
132
+
133
+ Flags first, then the environment, then the macOS Keychain:
134
+
135
+ | variable | meaning |
136
+ |---|---|
137
+ | `QBT_URL` | base URL, default `http://127.0.0.1:8080` |
138
+ | `QBT_API_KEY` | API key (qBittorrent 5.2+); preferred when present |
139
+ | `QBT_USERNAME` | WebUI username, for cookie login on any version |
140
+ | `QBT_PASSWORD` | WebUI password |
141
+ | `QBT_READ_ONLY` | `1`/`true`/`yes` to refuse everything that changes state |
142
+
143
+ Keychain fallback uses service `qbittorrent`, account `api-key` for a key and
144
+ `webui` for the password:
145
+
146
+ ```sh
147
+ security add-generic-password -s qbittorrent -a api-key -w '<key>'
148
+ ```
149
+
150
+ **qBittorrent has no OAuth**, so there is no token flow to delegate and the
151
+ secret is long-lived. The reference MCP servers put credentials straight into
152
+ the client's `env` block — `GITHUB_PERSONAL_ACCESS_TOKEN` is the canonical
153
+ example — but that file is plaintext and frequently committed. Prefer the
154
+ Keychain, and if you do use `env`, reference a variable instead of pasting the
155
+ secret:
156
+
157
+ ```jsonc
158
+ "env": {
159
+ "QBT_URL": "${QBT_URL:-http://127.0.0.1:8080}",
160
+ "QBT_API_KEY": "${QBT_API_KEY:-}" // empty falls through to the Keychain
161
+ }
162
+ ```
163
+
164
+ The `:-` default matters. Claude Code passes an unset `${VAR}` through as the
165
+ *literal text* `${VAR}` rather than as empty, which otherwise surfaces as a
166
+ confusing 401 much later — so `qbt-mcp` detects that case at startup and says
167
+ so plainly instead of starting.
168
+
169
+ On startup it reports, on stderr, where the credentials came from and never
170
+ what they are:
171
+
172
+ ```
173
+ qbt-mcp: http://127.0.0.1:8080 · WebAPI 2.15.1 · auth=bearer via macOS Keychain
174
+ ```
175
+
176
+ ### Safety
177
+
178
+ An agent driving a torrent client can destroy data that took weeks to collect,
179
+ so the gates are code rather than guidance:
180
+
181
+ - Destructive endpoints require `confirm`; `delete_files` is never implied.
182
+ - `--read-only` (or `QBT_READ_ONLY=1`) blocks mutations, **including via
183
+ `qbt_call`**.
184
+ - `app/setPreferences` refuses the WebUI, auth and network keys that would
185
+ sever the connection or weaken security. There is no override.
186
+ - Refusals return a structured cause and remedy, so an agent can correct itself
187
+ instead of seeing an opaque tool error.
188
+
189
+ `qbt-mcp` speaks stdio by default, which keeps it a child process with nothing
190
+ listening. The `sse` and `streamable-http` transports expose an
191
+ *unauthenticated* control surface holding your qBittorrent credentials — bound
192
+ to loopback, but do not forward those ports. See [SECURITY.md](https://github.com/manueltarouca/qbt-kit/blob/main/SECURITY.md).
193
+
194
+ ## Documentation
195
+
196
+ | | |
197
+ |---|---|
198
+ | [`docs/TRACEABILITY.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/TRACEABILITY.md) | how the coverage claim is built and gated |
199
+ | [`docs/TESTING.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/TESTING.md) | the container harness, and behaviours that cost real time to learn |
200
+ | [`docs/PLAN.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/PLAN.md) | the design and the reasoning behind it |
201
+ | [`docs/ROADMAP.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/ROADMAP.md) | what is done and what is not |
202
+ | [`docs/REVIEW-1.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/REVIEW-1.md) | an independent review of the plan, with every finding dispositioned |
203
+ | [`docs/dependencies.md`](https://github.com/manueltarouca/qbt-kit/blob/main/docs/dependencies.md) | every dependency, and why it is justified |
204
+ | [`SECURITY.md`](https://github.com/manueltarouca/qbt-kit/blob/main/SECURITY.md) | threat model, credential handling, how to report a vulnerability |
205
+
206
+ A Claude skill ships in [`skills/qbt-kit/`](https://github.com/manueltarouca/qbt-kit/tree/main/skills/qbt-kit/) covering CLI usage
207
+ and the swarm-health rule.
208
+
209
+ ## Field notes
210
+
211
+ Things that cost us time, documented so they cost you none.
212
+
213
+ - **Login success cannot be read from the status code.** qBittorrent ≤5.1
214
+ answers a *failed* login with HTTP 200 and the body `Fails.`; 5.2 returns an
215
+ empty 204. The only rule that holds across versions is whether a session
216
+ cookie was issued.
217
+ - **`app/webapiVersion` requires authentication.** Only `auth/login` is public,
218
+ so you cannot read the version to decide how to authenticate. Probe instead.
219
+ - **The session cookie is not always `SID`.** On 5.2 it is `QBT_SID_<port>`;
220
+ on 5.0/5.1 it is user-configurable. Never match on the name.
221
+ - **A bare 401 is often Host-header validation, not bad credentials.** It is on
222
+ by default and rejects any request whose `Host` port differs from the port
223
+ qBittorrent listens on — which is what happens behind mapped container ports,
224
+ SSH tunnels and most reverse proxies.
225
+ - **API keys exist only from qBittorrent 5.2.0.** Sending `Authorization:
226
+ Bearer` to 5.0/5.1 gets a 403 that looks like a credentials problem.
227
+ - **`pause`/`resume` were removed in 5.0**, not aliased; they 404 there.
228
+ - **`progress: 1.0` does not mean every file is on disk.** Progress is computed
229
+ over the *selected* files, so a torrent with files at priority 0 reports 100%
230
+ having never fetched them. `size < total_size` is the cheap flag, from the
231
+ same listing call.
232
+ - **A category alone does not place a torrent.** qBittorrent only applies a
233
+ category's save path under automatic torrent management, so `torrents/add`
234
+ with a category but without `autoTMM` files it in the global save path.
235
+ - **A stalled torrent is usually not slow but stuck.** `availability` is how
236
+ many complete copies exist across reachable peers; when it does not exceed
237
+ `progress`, no peer has the missing pieces. But a *queued* torrent reports
238
+ availability 0 simply for not being connected — judging those as dead is how
239
+ you delete healthy downloads.
240
+
241
+ ## Contributing
242
+
243
+ See [CONTRIBUTING.md](https://github.com/manueltarouca/qbt-kit/blob/main/CONTRIBUTING.md). The short version: endpoints are added
244
+ to the spec under `src/qbtkit/spec/`, never by hand to the CLI or MCP, and
245
+ `tools/build_coverage.py --strict` has to stay green.
246
+
247
+ ## Acknowledgements
248
+
249
+ - [qBittorrent](https://github.com/qbittorrent/qBittorrent) — and specifically
250
+ its `src/webui/api/*controller.cpp`, which is the only complete and accurate
251
+ description of this API that exists.
252
+ - [`qbittorrent-api`](https://pypi.org/project/qbittorrent-api/) is the mature,
253
+ well-maintained Python library for this API, and if a library is all you
254
+ need, use it. qbt-kit exists for the CLI, the MCP server and the verifiable
255
+ coverage claim, not because anything is wrong with it.
256
+
257
+ ## Licence
258
+
259
+ MIT — see [LICENSE](https://github.com/manueltarouca/qbt-kit/blob/main/LICENSE).
@@ -0,0 +1,117 @@
1
+ [project]
2
+ name = "qbt-kit"
3
+ version = "0.1.0"
4
+ description = "Spec-driven client, 1:1 CLI and MCP server for the qBittorrent WebUI API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ keywords = [
10
+ "qbittorrent",
11
+ "torrent",
12
+ "cli",
13
+ "mcp",
14
+ "webui-api",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: System Administrators",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Communications :: File Sharing",
25
+ ]
26
+ dependencies = [
27
+ "httpx>=0.28",
28
+ "typer>=0.27",
29
+ ]
30
+
31
+ [[project.authors]]
32
+ name = "mt"
33
+ email = "manuel+git@matm.xyz"
34
+
35
+ [project.optional-dependencies]
36
+ mcp = [
37
+ "mcp>=2.3",
38
+ "pydantic>=2.0",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/manueltarouca/qbt-kit"
43
+ Issues = "https://github.com/manueltarouca/qbt-kit/issues"
44
+ Changelog = "https://github.com/manueltarouca/qbt-kit/blob/main/CHANGELOG.md"
45
+ Documentation = "https://github.com/manueltarouca/qbt-kit#readme"
46
+
47
+ [project.scripts]
48
+ qbt = "qbtkit.cli:app"
49
+ qbt-mcp = "qbtkit.mcp:main"
50
+
51
+ [build-system]
52
+ requires = ["uv_build>=0.8,<0.14"]
53
+ build-backend = "uv_build"
54
+
55
+ [tool.uv]
56
+ required-version = ">=0.12"
57
+
58
+ [tool.uv.build-backend]
59
+ module-name = "qbtkit"
60
+
61
+ [tool.ruff]
62
+ line-length = 96
63
+ target-version = "py311"
64
+
65
+ [tool.ruff.format]
66
+ exclude = ["*.md"]
67
+
68
+ [tool.ruff.lint]
69
+ select = [
70
+ "E",
71
+ "F",
72
+ "W",
73
+ "I",
74
+ "UP",
75
+ "B",
76
+ "SIM",
77
+ "RUF",
78
+ "S",
79
+ "ANN",
80
+ ]
81
+ ignore = [
82
+ "ANN401",
83
+ "S105",
84
+ "S106",
85
+ ]
86
+
87
+ [tool.ruff.lint.per-file-ignores]
88
+ "tests/**" = [
89
+ "S101",
90
+ "ANN",
91
+ ]
92
+ "tools/**" = ["S310"]
93
+ "tests/contract/harness.py" = [
94
+ "S310",
95
+ "S603",
96
+ "S607",
97
+ ]
98
+ "tests/contract/**" = ["S310"]
99
+
100
+ [tool.ty.rules]
101
+ possibly-unresolved-reference = "warn"
102
+
103
+ [tool.pytest.ini_options]
104
+ testpaths = ["tests"]
105
+ markers = [
106
+ "contract: needs a qBittorrent container",
107
+ "realworld: needs outbound internet; opt-in only",
108
+ ]
109
+ addopts = "-m 'not contract and not realworld'"
110
+
111
+ [dependency-groups]
112
+ dev = [
113
+ "ruff>=0.17",
114
+ "ty>=0.0.89",
115
+ "pytest>=8.3",
116
+ "pytest-asyncio>=0.25",
117
+ ]