MCP Server - Kheopsian/Hydranos GitHub Wiki

🇬🇧 English · 🇫🇷 Français

MCP Server (AI clients)

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.

What it is

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 /mcp carries one JSON-RPC 2.0 message and gets one JSON answer. There is no session and no SSE stream: GET /mcp and DELETE /mcp answer 405 with Allow: POST.
  • Protocol versions: 2025-06-18, 2025-03-26, 2024-11-05. If the client asks for another version, the server answers with 2025-06-18.
  • Methods: initialize, ping, tools/list, tools/call. The server offers tools only: no resources, no prompts. serverInfo is {"name": "hydranos", "title": "Hydranos", "version": "4.x.y"} (the running version). The initialize answer also carries short instructions that tell the model to start with overview, page with find_torrents, and name torrents by info hash.
  • Batches (a JSON array of messages) are refused. A message without an id is a notification: 202 with an empty body.
  • While Hydranos starts, /mcp answers 503 with Retry-After: 5 like every API route, until the catalogue is loaded (REST API, While Hydranos starts).
  • Writes use the REST API. Every write tool except add_torrent sends its request to the real /api route inside the process, so it gets the same checks and refusals as the web UI. add_torrent calls 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
Loading

Connecting a client

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.

Generic client configuration (HTTP)

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"

Test it with curl

# 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.

Tools

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).

Read tools

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.

Write tools (reversible, on by default)

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

Destructive tools

delete_torrents and purge_race are not listed and not callable unless you enable them:

[mcp]
allow_destructive = true

The [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.

Limits

  • Pages, not the library. find_torrents returns at most 200 rows per call; follow next_offset. The other caps are in the tool tables above.
  • The category must exist for add_torrent, set_category (unless empty) and move_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 in race). Create categories first on the Categories tab.
  • Never seed mode, never a subfolder. add_torrent always hash-checks data already on disk, for a magnet too, and does not apply create_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).
  • engine on a magnet is not checked. With torrent_path or torrent_url, an unknown engine is refused with the list of engines; with magnet, an unknown engine is 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_torrents and tracker_errors include rows from other local engines with the same role and, if you have declared nodes, their rows too (the agent field, a legacy name, holds the row label local-<engine> or <node>-<engine>). torrent_detail and every write look only at this instance's engines: for a hash held only by a node, torrent_detail answers no torrent <hash> on this node, recheck, set_category, set_tags and move_to_category answer no such torrent on this node (status 404), reannounce and delete_torrents report the route's own 404, and pause/resume list it in not_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_category and move_to_category act on the copy in the first engine that holds it. set_tags changes the tags of every copy. pause/resume write 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_torrents without engine reaches every copy.
  • No Origin check. 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.

Errors

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"})

See also

⚠️ **GitHub.com Fallback** ⚠️