Author SHA1 Message Date
Hermes 34b6e36787 romarr-mcp: indexer and download-client write tools
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 20s
2026-11-05 23:30:32 +01:00
+106 -23
View File
@@ -226,23 +226,56 @@ def t_manual_scan(a):
return get("/api/v1/manualimport", path=a["path"], platform=a.get("platform"))
def t_import_path(a):
"""Adopt one file or directory with an explicit platform, bypassing the queue.
def t_indexer_schema(_):
return get("/api/v1/indexer/schema")
`POST /api/v1/manualimport` is the action half of Manual Import: the
platform given here wins over the header guess, which is the whole point --
an ISO9660 PS2 disc reports itself as psx, so a guess cannot be trusted.
def _secret_from(body, field, literal, env_name):
"""Set a credential field, optionally resolved from the server's own env.
Passing a key by *reference* means the value never enters the transcript: the
gateway already exposes Prowlarr's and SABnzbd's keys to this process, so the
caller names the variable instead of quoting the secret.
"""
path = (a.get("path") or "").strip()
platform = (a.get("platform") or "").strip()
if not path:
raise ToolError("path is required (a file or directory on ROMarr's host)")
if not platform:
raise ToolError("platform is required (e.g. ps2) -- the header guess is unreliable")
body = {"path": path, "platform": platform}
if a.get("force"):
body["force"] = True
return post("/api/v1/manualimport", body)
if env_name:
value = os.environ.get(env_name, "")
if not value:
raise ToolError(f"{env_name} is not set in this server's environment")
body[field] = value
elif literal:
body[field] = literal
def t_indexer_save(a):
kind = (a.get("type") or "").strip()
if not kind:
raise ToolError("type is required (see indexer_schema), e.g. 'prowlarr'")
body = dict(a.get("extra") or {})
body["type"] = kind
for field in ("name", "url", "enable", "id"):
if a.get(field) not in (None, ""):
body[field] = a[field]
_secret_from(body, "api_key", a.get("api_key"), a.get("api_key_env"))
return post("/api/v1/indexer", body)
def t_client_schema(_):
return get("/api/v1/downloadclient/schema")
def t_client_save(a):
kind = (a.get("type") or "").strip()
if not kind:
raise ToolError("type is required (see download_client_schema), e.g. 'qbittorrent'")
body = dict(a.get("extra") or {})
body["type"] = kind
for field in ("name", "host", "port", "use_ssl", "url_base", "username",
"category", "id"):
if a.get(field) not in (None, ""):
body[field] = a[field]
_secret_from(body, "password", a.get("password"), a.get("password_env"))
_secret_from(body, "api_key", a.get("api_key"), a.get("api_key_env"))
return post("/api/v1/downloadclient", body)
def _obj(props, required=None):
@@ -422,14 +455,6 @@ TOOLS = [
"write": True,
"fn": t_import_finished,
},
{
"name": "import_path",
"description": "Adopt a file or folder into the library with an explicit platform, bypassing queue matching. Use when a finished download never imported.",
"schema": _obj({"path": {"type": "string"}, "platform": P_PLATFORM,
"force": {"type": "boolean"}}, ["path", "platform"]),
"write": True,
"fn": t_import_path,
},
{
"name": "manual_scan",
"description": "Scan a directory on ROMarr's host for files it could adopt, without importing them.",
@@ -437,6 +462,64 @@ TOOLS = [
"write": False,
"fn": t_manual_scan,
},
{
"name": "indexer_schema",
"description": "Every indexer type ROMarr supports, with the fields each one needs — read this before indexer_save.",
"schema": _obj({}),
"write": False,
"fn": t_indexer_schema,
},
{
"name": "indexer_save",
"description": "Create or update an indexer. Prefer api_key_env over api_key: the server resolves the variable from its own environment, so the secret never appears in the call.",
"schema": _obj(
{
"type": {"type": "string", "description": "Indexer type, e.g. 'prowlarr', 'torznab', 'newznab'."},
"name": {"type": "string"},
"url": {"type": "string", "description": "Base URL, e.g. http://127.0.0.1:9696 for Prowlarr."},
"api_key": {"type": "string", "description": "Inline key. Avoid: prefer api_key_env."},
"api_key_env": {"type": "string", "description": "Name of an env var holding the key, e.g. 'PROWLARR_API_KEY'."},
"enable": {"type": "boolean"},
"id": {"type": "string", "description": "Existing indexer id to update."},
"extra": {"type": "object", "description": "Any other fields the type needs, verbatim."},
},
["type"],
),
"write": True,
"fn": t_indexer_save,
},
{
"name": "download_client_schema",
"description": "Every download client type ROMarr supports, with its fields and default port.",
"schema": _obj({}),
"write": False,
"fn": t_client_schema,
},
{
"name": "download_client_save",
"description": "Create or update a torrent/usenet download client. Credentials may be passed by reference (password_env / api_key_env) instead of inline.",
"schema": _obj(
{
"type": {"type": "string", "description": "Client type, e.g. 'qbittorrent', 'sabnzbd', 'transmission'."},
"name": {"type": "string"},
"host": {"type": "string"},
"port": {"type": "integer"},
"use_ssl": {"type": "boolean"},
"url_base": {"type": "string"},
"username": {"type": "string"},
"password": {"type": "string", "description": "Inline. Avoid: prefer password_env."},
"password_env": {"type": "string"},
"api_key": {"type": "string", "description": "Inline. Avoid: prefer api_key_env."},
"api_key_env": {"type": "string", "description": "e.g. 'SABNZBD_API_KEY'."},
"category": {"type": "string"},
"id": {"type": "string", "description": "Existing client id to update."},
"extra": {"type": "object", "description": "Any other fields the type needs, verbatim."},
},
["type"],
),
"write": True,
"fn": t_client_save,
},
]
BY_NAME = {t["name"]: t for t in TOOLS}