diff --git a/packages/romarr-mcp/romarr_mcp.py b/packages/romarr-mcp/romarr_mcp.py index 15d4da6..823a7f3 100644 --- a/packages/romarr-mcp/romarr_mcp.py +++ b/packages/romarr-mcp/romarr_mcp.py @@ -226,6 +226,58 @@ def t_manual_scan(a): return get("/api/v1/manualimport", path=a["path"], platform=a.get("platform")) +def t_indexer_schema(_): + return get("/api/v1/indexer/schema") + + +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. + """ + 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): return { "type": "object", @@ -410,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}