MCP Server - Kheopsian/Hydranos GitHub Wiki
Applies to Hydranos 4.4.
This page is for you if you want an AI assistant or any other MCP client to look at your library and act on it through the Model Context Protocol. By the end you will know how to connect a client, which tools it gets, which ones can remove data and how to unlock them, and what the server refuses. Terms: Concepts and Glossary.
Every Hydranos instance serves an MCP server on the same listener as the web UI, port 8199, at POST /mcp. There is nothing to install or turn on, and there is no screen for it in the web UI.
-
Transport: stateless "streamable HTTP". Each
POST /mcpcarries one JSON-RPC 2.0 message and gets one JSON answer. There is no session and no SSE stream:GET /mcpandDELETE /mcpanswer405withAllow: POST. -
Protocol versions:
2025-06-18,2025-03-26,2024-11-05. If the client asks for another version, the server answers with2025-06-18. -
Methods:
initialize,ping,tools/list,tools/call. The server offers tools only: no resources, no prompts.serverInfois{"name": "hydranos", "title": "Hydranos", "version": "4.x.y"}(the running version). Theinitializeanswer also carries shortinstructionsthat tell the model to start withoverview, page withfind_torrents, and name torrents by info hash. -
Batches (a JSON array of messages) are refused. A message without an
idis a notification:202with an empty body. -
While Hydranos starts,
/mcpanswers503withRetry-After: 5like every API route, until the catalogue is loaded (REST API, While Hydranos starts). -
Writes use the REST API. Every write tool except
add_torrentsends its request to the real/apiroute inside the process, so it gets the same checks and refusals as the web UI.add_torrentcalls the same add function the add routes use, without going through a route. No write takes a filter: every write names its torrents by info hash.
sequenceDiagram
participant C as MCP client
participant M as Hydranos POST /mcp
participant A as /api routes in-process
C->>M: initialize with Authorization Bearer key
M-->>C: protocolVersion, tools capability, serverInfo hydranos
C->>M: notifications/initialized, no id
M-->>C: 202, no body
C->>M: tools/list
M-->>C: 19 tools, or 21 with allow_destructive
C->>M: tools/call find_torrents
M->>M: build one page from the engine list
M-->>C: result with content text and structuredContent
C->>M: tools/call pause with info_hashes
loop each engine holding some of the hashes
M->>A: POST /api/engines/ENGINE/torrents/bulk
A-->>M: status and answer
end
M-->>C: result, isError false, per-engine ok and status
Note over C,M: isError true when the tool itself fails<br/>(bad arguments, disabled tool, unknown torrent,<br/>refused add, failed read). Per-torrent write<br/>failures stay isError false with ok false
You need two things: the URL http://<host>:8199/mcp and the instance's API key ([daemon] api_key, see Security and Access). The server accepts the key in any of these forms:
| How | Example |
|---|---|
Authorization: Bearer header (what MCP clients send) |
Authorization: Bearer YOUR_API_KEY |
X-Api-Key header |
X-Api-Key: YOUR_API_KEY |
apikey query parameter, for clients that cannot set headers |
http://192.0.2.10:8199/mcp?apikey=YOUR_API_KEY |
The SID cookie set by the qBittorrent shim login (POST /api/v2/auth/login) |
not useful for an MCP client; the web UI itself uses no cookie |
A missing or wrong key gets 401 with WWW-Authenticate: Bearer and {"error": "Invalid or missing API key"}. If api_key is empty, nobody is let in. The query-parameter form puts the key in URLs, which end up in proxy logs: use a header when your client can.
Most MCP clients read a JSON file with an mcpServers object. Add Hydranos as an HTTP server:
{
"mcpServers": {
"hydranos": {
"type": "http",
"url": "http://192.0.2.10:8199/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}Some clients call the type streamable-http or streamableHttp instead of http; Hydranos does not care which, it only sees the POSTs. If your client only supports local (stdio) servers, put a stdio-to-HTTP bridge in between and give it the same URL and header.
With Claude Code, for example, the same thing is one command:
claude mcp add --transport http hydranos http://192.0.2.10:8199/mcp \
--header "Authorization: Bearer YOUR_API_KEY"# Handshake
curl -s http://192.0.2.10:8199/mcp \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
# List the tools
curl -s http://192.0.2.10:8199/mcp \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# Ten torrents of the hoard engine in error with their tracker
curl -s http://192.0.2.10:8199/mcp \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"find_torrents","arguments":{"engine":"hoard","error_class":"dead","limit":10}}}'Because the server is stateless, you can send tools/call without initialize first; the handshake is only there for clients that expect it.
19 tools are listed by default. Each carries MCP annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) so the client can decide when to ask you before calling. engine arguments take an engine id as overview lists it; when omitted, reads use hoard (or the first engine if there is no hoard).
| Tool | Arguments | What it returns |
|---|---|---|
overview |
none | Every engine of this instance: id, role, listening or not, torrent count, live upload/download rate, active peers, counts by state, tracker errors, torrent errors, error classes, uncategorised, and the top 15 categories, trackers and tags. Takes a few seconds on a very large library. |
find_torrents |
engine, search, category, tag, tracker, state, error_class, sort (default added_time), order (asc/desc, default desc), offset, limit (default 25, max 200) |
One page of torrents, filters ANDed. Answer: total, filtered, returned, rows, and next_offset while more pages remain. search matches all words of the name, or an info hash prefix of at least 6 hex characters. Each row is a compact set of fields (name, info hash, state, progress, size, category, tags, tracker host, ratio, upload, rates, peers, seeds, added time, save path, the row label agent, user stop flag, and the tracker error with its class). |
torrent_detail |
info_hash (required) |
The full row of one torrent, the engine that holds it and its live tracker list. |
torrent_files |
info_hash (required), limit (default 100, max 1000) |
File count, total size, and the files with their sizes. |
tracker_errors |
engine, error_class, samples (default 3, max 20) |
Torrents in tracker error grouped by class (dead = unregistered, auth = passkey refused, throttled, unreachable, other), each with its top trackers and sample messages. |
trackers |
problems_only, include_hidden (both default false), limit (default 50, max 500) |
Every tracker host: torrent count, last announce status, last error, passkey and announce settings. Largest first. |
categories |
none | Every category with its save path and mode. |
health |
none | The anomaly report of /api/health/anomalies: torrents whose data is missing, ghost save paths, seeds without their pieces, starved downloads, re-downloads, hashes seeded twice, files another torrent holds with other bytes (from a background pass every 5 minutes, generated_at says when; scan is pending until the first one), plus live counts of failing trackers, late announces, held engines and disks low on space, per engine. Instant. See Monitoring and Logs. |
drain |
history (default 10; at most 50 come back, the route keeps only the 50 latest passes) |
Race drain status per volume and the most recent drain passes. |
jobs |
state (e.g. running, done, failed), limit (default 20; at most 100 come back) |
Background jobs, most recently updated first, with progress. Only the 100 most recently created jobs are looked at, then filtered by state. |
logs |
level (ERROR, WARN, INFO, DEBUG; minimum level, default WARN), contains (case-insensitive, in the message), module (case-insensitive part of the module path, e.g. announce or store), limit (default 50, max 500) |
The newest matching lines of the in-memory log buffer (the last 2000 lines), oldest first, with matched and returned. It covers minutes, not days. |
| Tool | Arguments | What it does | Max hashes |
|---|---|---|---|
pause |
info_hashes |
Stops the torrents (data and row stay). | 1000 |
resume |
info_hashes |
Starts stopped torrents. | 1000 |
reannounce |
info_hashes |
Announces now instead of at the next interval. | 100 |
recheck |
info_hashes |
Hash-checks the data in the background. A stopped torrent stays stopped. Guarded like Recheck in the right-click menu: when another torrent names the same files with other bytes, the torrent is held while those files are checked first, and stays stopped in error if they do not verify (Adding Torrents and Existing Data). | 50 |
set_category |
info_hashes, category ("" removes it) |
Changes the label only. Files stay where they are and the torrent stays in its engine. | 100 |
set_tags |
info_hashes, tags (array; [] clears) |
Replaces the tags with exactly this list. | 100 |
move_to_category |
info_hashes, category, allow_breaking_hardlinks (default false) |
Sets the category and moves the data to its save path, as a job (see jobs); when the category's mode differs, the job is a graduation to the other engine. Refused (409 in the per-torrent result, with its reason) if a file is hardlinked elsewhere and allow_breaking_hardlinks is false, if free space is short, if another torrent reads the same files, or if the destination already holds other files. See Jobs and Moving Data. |
100 |
add_torrent |
category (required), exactly one of torrent_path / torrent_url / magnet, plus save_path, tags (array), paused, engine
|
Adds one torrent. torrent_path is a .torrent file on the Hydranos machine; torrent_url is fetched over http(s) (30 s timeout, 15 MiB max) by the daemon's own way out, as its other requests (VPN and Egress): when the kill switch leaves it no way out, the add fails with the reason. A magnet answers {"info_hash": …, "status": "resolving"} and appears once its metadata arrives. save_path overrides the category's path and must be where the data is or will be. Never uses seed mode: existing data is hash-checked. No subfolder: [daemon] create_torrent_folder is not applied. Placement follows Categories and Routing; the add path's refusals (race disk full, files another torrent holds with path_collision = "refuse", already added) come back as the tool error. |
1 |
Every info_hashes list is checked before anything happens: each entry must be 40 hex characters, duplicates are dropped, and a list over the limit is refused as a whole. Per-torrent tools answer {"ok": n, "failed": n, "results": [...]} with one {info_hash, ok, status, answer} per torrent; pause/resume answer one entry per engine plus a not_found list.
Destructive tools (hidden by default)
| Tool | Arguments | What it does | Max hashes |
|---|---|---|---|
delete_torrents |
info_hashes, delete_files (default false), engine
|
Removes the torrents. Without engine, every copy on this instance goes; with delete_files, their data is deleted from disk too, which cannot be undone. |
100 |
purge_race |
info_hashes |
Removes the copy in the engine named race, with its files (files another copy or torrent still reads are kept); a copy in another engine stays. A hash not in race → status 404 (torrent not in race) in its result. Only the engine named race: for an extra race-role engine use delete_torrents with engine. |
100 |
delete_torrents and purge_race are not listed and not callable unless you enable them:
[mcp]
allow_destructive = trueThe [mcp] section is not in the shipped default.toml, and the Config tab can only change keys that already exist, so add it by hand to the config file (see Configuration: Daemon, Auth and Global Sections) and restart Hydranos (see Operating Hydranos for a clean stop). Once the section exists, it shows up under Other in Advanced settings (default.toml); the switch is read on every request, so a change saved there applies to /mcp at once.
With the switch off, a client that calls one of them anyway (for example from a cached tool list) gets an isError result: `delete_torrents` is disabled on this node; set `allow_destructive = true` under `[mcp]` in the configuration to enable it. Both tools carry destructiveHint: true, so a well-behaved client asks you before each call.
-
Pages, not the library.
find_torrentsreturns at most 200 rows per call; follownext_offset. The other caps are in the tool tables above. -
The category must exist for
add_torrent,set_category(unless empty) andmove_to_category. An unknown name is refused with the list of valid ones. This is stricter than the REST API and the qBittorrent shim: their label routes store any name, and their add routes accept an unknown name as long as a save path is sent (the torrent then lands inrace). Create categories first on the Categories tab. -
Never seed mode, never a subfolder.
add_torrentalways hash-checks data already on disk, for a magnet too, and does not applycreate_subfolder/[daemon] create_torrent_folder. To add over complete data without a check, or with a folder of its own, use the REST API or the shim (see Adding Torrents and Existing Data). -
engineon a magnet is not checked. Withtorrent_pathortorrent_url, an unknownengineis refused with the list of engines; withmagnet, an unknownengineis ignored and the category decides where the torrent goes. - What MCP does not cover in 4.4. There is no tool for RSS feeds and rules, download order (file priorities, sequential), speed or share limits, the queue, the network settings or nodes. Use the web UI or the REST API for those (RSS and Automatic Downloads, Queue, Share and Speed Limits).
-
Reads show the same merged list as the web UI; writes act on this instance only.
overview,find_torrentsandtracker_errorsinclude rows from other local engines with the same role and, if you have declared nodes, their rows too (theagentfield, a legacy name, holds the row labellocal-<engine>or<node>-<engine>).torrent_detailand every write look only at this instance's engines: for a hash held only by a node,torrent_detailanswersno torrent <hash> on this node,recheck,set_category,set_tagsandmove_to_categoryanswerno such torrent on this node(status 404),reannounceanddelete_torrentsreport the route's own 404, andpause/resumelist it innot_found. To act on a node's torrents, point a client at that node's own/mcp. See Nodes and Multiple Machines. -
Copies in several local engines.
reannounce,recheck,set_categoryandmove_to_categoryact on the copy in the first engine that holds it.set_tagschanges the tags of every copy.pause/resumewrite the stopped flag on every copy's row but stop or start only the copy in the first engine; the other copies keep running (or stay stopped) until the next restart, then follow the flag.delete_torrentswithoutenginereaches every copy. -
No
Origincheck. The endpoint relies on the API key alone. Keep port 8199 on your LAN or behind a VPN or reverse proxy, as for the rest of the API.
There are two kinds of failure, and clients show them differently.
Tool errors are normal results flagged isError: true with a text message the model can read and act on, for example unknown category `tv`; this node has: movies, tv-hd, unknown engine `vpn2`; this node runs: race, hoard, info_hashes is empty, or give one of torrent_path, torrent_url or magnet. A successful call returns isError: false with the JSON both as content[0].text and as structuredContent.
Protocol errors are JSON-RPC errors:
| Situation | HTTP status | JSON-RPC code |
|---|---|---|
| Body is not JSON | 400 |
-32700 parse error |
| Batch (JSON array) | 400 |
-32600 batching is not supported |
| Unknown method | 200 |
-32601 method not found |
| Unknown tool name | 200 |
-32602 unknown tool |
Notification (no id) |
202, empty body | none |
GET or DELETE on /mcp
|
405, Allow: POST
|
none |
| Missing or wrong API key | 401, WWW-Authenticate: Bearer
|
none ({"error": "Invalid or missing API key"}) |
- REST API: every route the write tools use, the stubs and the routes removed in 4.4.
- Security and Access: the API key, rotating it, exposing port 8199.
-
Configuration: Daemon, Auth and Global Sections: the
[mcp]key. - Adding Torrents and Existing Data: add paths compared, seed mode.