7 Commits
Author SHA1 Message Date
luis 2393f594b3 Merge pull request 'Add romarr-mcp: curated MCP server for ROMarr (21 tools)' (#21) from feat/romarr-mcp into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 13s
Reviewed-on: #21
2026-11-04 22:30:01 +01:00
Hermes c57cf38abe Add romarr-mcp: curated MCP server for ROMarr
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 19s
2026-11-04 22:24:56 +01:00
hermes-agent 78b0a621fd Merge pull request 'romm-mcp: restore library scans via POST /api/tasks/scan' (#20) from fix/romm-scan-endpoint into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 13s
2026-11-02 13:56:16 +01:00
hermes-agent 106bf1014e romm-mcp: restore library scans via POST /api/tasks/scan
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 25s
romm_system action=scan threw client-side:

  Scan endpoint removed in RomM 5.0. Use the RomM web UI to trigger library scans.

That note is stale. The comment above the stub says RomM 5.0 removed POST
/api/tasks/run -- true -- but 5.4 serves POST /api/tasks/scan with a ScanPayload
(type, platforms, platform_fs_slugs, roms_ids, apis). Verified against the running
instance: /api/tasks/scan is in the 5.4.1 OpenAPI (226 paths), and a call with a
bad key answers 401 from that exact endpoint rather than 404.

So the stub was blocking a working endpoint, and scans still needed a human in the
UI. Patch restores runScan() defaulting to type=quick, which indexes new and
changed files without re-hashing the whole library (a full scan hashes 229 GB).

The patch targets dist/client.js: the npm tarball ships only dist/ plus a .d.ts, so
there is no .ts to patch without dragging in typescript. Keep it small and delete
it when upstream restores runScan().
2026-11-02 13:50:58 +01:00
hermes-agent ecf37568a3 Merge pull request 'Add romm-mcp package' (#19) from add-romm-mcp into main
NixOS Configuration CI / Nix Flake Check (push) Successful in 13s
2026-11-02 12:59:32 +01:00
hermes-agent b55c7af097 romm-mcp: pin the real source and npmDeps hashes
NixOS Configuration CI / Nix Flake Check (pull_request) Successful in 25s
Both come from the build, and both were non-obvious:

- fetchzip hashes the *unpacked* tree, not the .tgz bytes, so a hash computed
  from the published tarball does not match; the derivation reports the NAR hash
  of the extracted contents instead.
- npmDepsHash only exists once npm has materialised the dependency tree from the
  committed lockfile.

Verified with nix build .#romm-mcp: the derivation builds and ./result/bin/romm-mcp
starts and prints 'RomM MCP server running on stdio'.
2026-11-02 10:13:46 +01:00
hermes-agent e9dc6f91b7 Add romm-mcp package
NixOS Configuration CI / Nix Flake Check (pull_request) Failing after 22s
RomM MCP server: browse, search and administrate a self-hosted ROM library,
including triggering library scans (POST /api/tasks/scan), which RomM 5.x only
exposes through its API.

Upstream ships no package-lock.json, so the lock committed here is generated from
the published 1.0.5 tarball (npm install --package-lock-only --ignore-scripts),
pinning exactly the tree that shipped with that version. dist/ is prebuilt in the
tarball, hence dontNpmBuild.

npmDepsHash is a placeholder: it can only be derived from the lockfile by a build,
so CI (nix flake check, which builds every package) reports the real value.
2026-11-02 10:09:42 +01:00
6 changed files with 1929 additions and 0 deletions

No files matched your search

+2
View File
@@ -27,11 +27,13 @@
mcp-email-server = pkgs.callPackage ./mcp-email-server/package.nix { };
namecheap-mcp = pkgs.callPackage ./namecheap-mcp/package.nix { };
navidrome-mcp = pkgs.callPackage ./navidrome-mcp/package.nix { };
romarr-mcp = pkgs.callPackage ./romarr-mcp/package.nix { };
ovhcloud-mcp = pkgs.callPackage ./ovhcloud-mcp/package.nix { };
slskd-mcp = pkgs.callPackage ./slskd-mcp/package.nix { };
yarr = pkgs.callPackage ./yarr/package.nix { };
pdf-mcp = pkgs.callPackage ./pdf-mcp/package.nix { };
mcp-searxng = pkgs.callPackage ./mcp-searxng/package.nix { };
python-ssh-mcp = pkgs.callPackage ./python-ssh-mcp/package.nix { };
romm-mcp = pkgs.callPackage ./romm-mcp/package.nix { };
# example-mcp-server = pkgs.callPackage ./example-mcp-server/package.nix { };
}
+37
View File
@@ -0,0 +1,37 @@
{
lib,
stdenv,
python3,
makeWrapper,
}:
stdenv.mkDerivation {
pname = "romarr-mcp";
version = "1.0.0";
src = ./.;
nativeBuildInputs = [ makeWrapper ];
dontBuild = true;
dontConfigure = true;
# Single stdlib-only file: no wheels to vendor, no interpreter to pin beyond
# the python3 in nixpkgs. ROMarr's own API is reached over plain HTTP(S).
installPhase = ''
runHook preInstall
mkdir -p $out/lib/romarr-mcp $out/bin
install -m644 romarr_mcp.py $out/lib/romarr-mcp/romarr_mcp.py
makeWrapper ${python3}/bin/python3 $out/bin/romarr-mcp \
--add-flags "$out/lib/romarr-mcp/romarr_mcp.py"
runHook postInstall
'';
meta = with lib; {
description = "MCP server for ROMarr: library, wanted, queue, indexer search, DAT audit";
homepage = "https://github.com/BlizzHacker/romarr";
license = licenses.mit;
mainProgram = "romarr-mcp";
platforms = platforms.all;
};
}
+560
View File
@@ -0,0 +1,560 @@
#!/usr/bin/env python3
"""romarr-mcp — an MCP server for ROMarr, the *arr for games.
Talks to ROMarr's REST API (documented at /api/v1/openapi.json). ROMarr
generates its spec from the routes it actually serves, but it carries no
OpenAPI tags, so a generic spec-to-MCP bridge exposes all 133 operations with
names like ``postApiV1AuditQuarantineMove``. This server names the twenty that
are actually useful and gives them arguments a model can reason about.
Standard library only: no httpx, no mcp SDK. That keeps the Nix package a
single file with no vendored wheels, and the protocol surface this server needs
(initialize, tools/list, tools/call, ping) is small.
Environment:
ROMARR_URL base URL, default http://127.0.0.1:6868
ROMARR_API_KEY required; Settings -> General, or ROMARR_API_KEY upstream
ROMARR_READ_ONLY when "1"/"true", refuses every write tool
ROMARR_TIMEOUT seconds, default 60
ROMARR_VERIFY_TLS set "0" to skip certificate verification
"""
from __future__ import annotations
import json
import os
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
SERVER_NAME = "romarr"
SERVER_VERSION = "1.0.0"
PROTOCOL_VERSION = "2024-11-05"
BASE_URL = os.environ.get("ROMARR_URL", "http://127.0.0.1:6868").rstrip("/")
API_KEY = os.environ.get("ROMARR_API_KEY", "").strip()
READ_ONLY = os.environ.get("ROMARR_READ_ONLY", "").strip().lower() in ("1", "true", "yes", "on")
TIMEOUT = float(os.environ.get("ROMARR_TIMEOUT", "60") or 60)
VERIFY_TLS = os.environ.get("ROMARR_VERIFY_TLS", "1").strip().lower() not in ("0", "false", "no", "off")
class ToolError(Exception):
"""A tool failed in a way the caller should see as an error, not a crash."""
# --------------------------------------------------------------------------- http
def _ssl_context():
if VERIFY_TLS:
return None
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
def call_api(method, path, params=None, body=None):
"""Call ROMarr and return the decoded JSON body."""
if not API_KEY:
raise ToolError(
"ROMARR_API_KEY is not set. Find the key under ROMarr -> Settings -> General "
"(or GET /api/v1/system/apikey with an existing credential)."
)
url = BASE_URL + path
clean = {k: v for k, v in (params or {}).items() if v is not None and v != ""}
if clean:
url += "?" + urllib.parse.urlencode(clean, doseq=True)
data = None
headers = {"X-Api-Key": API_KEY, "Accept": "application/json"}
if body is not None:
data = json.dumps(body).encode()
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=TIMEOUT, context=_ssl_context()) as resp:
raw = resp.read().decode("utf-8", "replace")
status = resp.status
except urllib.error.HTTPError as exc:
raw = exc.read().decode("utf-8", "replace")
status = exc.code
detail = raw
try:
parsed = json.loads(raw)
detail = parsed.get("error") or parsed.get("detail") or raw
except Exception:
pass
raise ToolError(f"ROMarr {method} {path} -> HTTP {status}: {str(detail)[:400]}")
except urllib.error.URLError as exc:
raise ToolError(f"cannot reach ROMarr at {BASE_URL}: {exc.reason}")
if not raw.strip():
return {"status": status, "ok": True}
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"status": status, "body": raw[:4000]}
def get(endpoint, **params):
return call_api("GET", endpoint, params=params)
def post(endpoint, body=None):
if READ_ONLY:
raise ToolError("ROMARR_READ_ONLY is set; this server only reads right now.")
return call_api("POST", endpoint, body=body if body is not None else {})
# -------------------------------------------------------------------------- tools
def t_system_status(_):
return get("/api/v1/system/status")
def t_system_counts(_):
return get("/api/v1/system/counts")
def t_tasks(a):
return get("/api/v1/system/tasks", limit=a.get("limit"), platform=a.get("platform"))
def t_library(a):
return get(
"/api/v1/game",
platform=a.get("platform"),
shelf=a.get("shelf"),
limit=a.get("limit", 50),
offset=a.get("offset"),
decade=a.get("decade"),
genre=a.get("genre"),
region=a.get("region"),
origin=a.get("origin"),
)
def t_wanted(a):
return get("/api/v1/wanted/missing", platform=a.get("platform"), limit=a.get("limit"), view=a.get("view"))
def t_queue(_):
return get("/api/v1/queue")
def t_history(a):
return get("/api/v1/history", limit=a.get("limit"), platform=a.get("platform"))
def t_search_releases(a):
if not a.get("game"):
raise ToolError("game is required")
return get("/api/v1/release", game=a["game"], platform=a.get("platform"))
def t_platforms(_):
return get("/api/platforms")
def t_audit(a):
return get("/api/v1/audit", platform=a.get("platform"), status=a.get("status"), game=a.get("game"))
def t_duplicates(_):
return get("/api/v1/audit/quarantine")
def t_indexers(_):
return get("/api/v1/indexer")
def t_download_clients(_):
return get("/api/v1/downloadclient")
def t_request_game(a):
game, platform = (a.get("game") or "").strip(), (a.get("platform") or "").strip()
if not game or not platform:
raise ToolError("both game and platform are required")
return post("/api/request", {"game": game, "platform": platform})
def t_grab_release(a):
release_id = (a.get("id") or "").strip()
if not release_id:
raise ToolError("id is required (from search_releases)")
return post("/api/v1/release/grab", {"id": release_id})
def t_queue_action(a):
action = (a.get("action") or "").strip()
if action not in ("retry", "remove"):
raise ToolError("action must be 'retry' or 'remove'")
if a.get("index") is None:
raise ToolError("index is required (row number from queue)")
return post("/api/v1/queue/action", {"index": int(a["index"]), "action": action})
def t_audit_run(a):
return post("/api/v1/audit", {"platform": a.get("platform") or ""})
def t_quarantine(a):
if a.get("confirm") is not True:
raise ToolError("pass confirm=true; this moves files into ROMarr's quarantine")
files = a.get("files") or []
if not files:
raise ToolError("files is required (the paths from the duplicates list)")
return post("/api/v1/audit/quarantine/move", {"platform": a.get("platform") or "", "files": files})
def t_rename_preview(a):
return post(
"/api/v1/audit/renames/preview",
{"platform": a.get("platform") or "", "scope": a.get("scope") or "", "offset": int(a.get("offset") or 0)},
)
def t_import_finished(_):
return post("/api/import", {})
def t_manual_scan(a):
if not a.get("path"):
raise ToolError("path is required (a directory on ROMarr's host)")
return get("/api/v1/manualimport", path=a["path"], platform=a.get("platform"))
def _obj(props, required=None):
return {
"type": "object",
"properties": props,
"required": required or [],
"additionalProperties": False,
}
P_PLATFORM = {"type": "string", "description": "Platform folder slug, e.g. 'psx', 'snes', 'gbc'."}
P_LIMIT = {"type": "integer", "description": "Maximum rows to return.", "minimum": 1, "maximum": 500}
TOOLS = [
{
"name": "system_status",
"description": "Health of every ROMarr dependency (library server, indexers, download clients) plus play-route counts.",
"schema": _obj({}),
"write": False,
"fn": t_system_status,
},
{
"name": "system_counts",
"description": "Library and queue sizes: how many games, how many on disk, how many catalogued elsewhere.",
"schema": _obj({}),
"write": False,
"fn": t_system_counts,
},
{
"name": "tasks",
"description": "The scheduled jobs with interval, last run and last result — use it to see whether a scan or audit is in flight.",
"schema": _obj({"limit": P_LIMIT, "platform": P_PLATFORM}),
"write": False,
"fn": t_tasks,
},
{
"name": "library",
"description": "The games ROMarr holds, filterable by platform, shelf, decade, genre, region. Paged by limit/offset.",
"schema": _obj(
{
"platform": P_PLATFORM,
"shelf": {"type": "string", "description": "Shelf filter such as 'top-rated' or 'recent'."},
"limit": P_LIMIT,
"offset": {"type": "integer", "minimum": 0},
"decade": {"type": "string", "description": "e.g. '1990'."},
"genre": {"type": "string"},
"region": {"type": "string", "description": "e.g. 'USA', 'Europe', 'Japan'."},
"origin": {"type": "string", "description": "Where the entry came from: on-disk, elsewhere, or wanted."},
}
),
"write": False,
"fn": t_library,
},
{
"name": "wanted_missing",
"description": "Games that are wanted but not yet found — the acquisition backlog.",
"schema": _obj({"platform": P_PLATFORM, "limit": P_LIMIT, "view": {"type": "string"}}),
"write": False,
"fn": t_wanted,
},
{
"name": "queue",
"description": "Active downloads with their state, progress and error text.",
"schema": _obj({}),
"write": False,
"fn": t_queue,
},
{
"name": "history",
"description": "What ROMarr has done: grabs, imports, failures, with timestamps.",
"schema": _obj({"limit": P_LIMIT, "platform": P_PLATFORM}),
"write": False,
"fn": t_history,
},
{
"name": "search_releases",
"description": "Interactive search across every configured indexer for one game+platform; returns scored candidates with a rejection reason per candidate and the id to pass to grab_release.",
"schema": _obj({"game": {"type": "string"}, "platform": P_PLATFORM}, ["game"]),
"write": False,
"fn": t_search_releases,
},
{
"name": "platforms",
"description": "Every platform ROMarr knows, with its media, file extensions, size ceiling and how many games it holds.",
"schema": _obj({}),
"write": False,
"fn": t_platforms,
},
{
"name": "audit",
"description": "DAT verification state of the library: verified, bad dump, unknown, per platform. Read-only view of the last audit.",
"schema": _obj({"platform": P_PLATFORM, "status": {"type": "string"}, "game": {"type": "string"}}),
"write": False,
"fn": t_audit,
},
{
"name": "duplicates",
"description": "Preview exact byte-identical duplicates and list the reversible quarantine manifests already recorded.",
"schema": _obj({}),
"write": False,
"fn": t_duplicates,
},
{
"name": "indexers",
"description": "Configured indexers with their keys masked, plus whether each is enabled.",
"schema": _obj({}),
"write": False,
"fn": t_indexers,
},
{
"name": "download_clients",
"description": "Configured download clients (torrent and usenet), keys masked.",
"schema": _obj({}),
"write": False,
"fn": t_download_clients,
},
{
"name": "request_game",
"description": "Request a game: ROMarr searches the indexers, scores the releases, grabs the best and imports it. This starts a download.",
"schema": _obj({"game": {"type": "string"}, "platform": P_PLATFORM}, ["game", "platform"]),
"write": True,
"fn": t_request_game,
},
{
"name": "grab_release",
"description": "Grab one specific candidate returned by search_releases, by its id.",
"schema": _obj({"id": {"type": "string", "description": "Release id from search_releases."}}, ["id"]),
"write": True,
"fn": t_grab_release,
},
{
"name": "queue_action",
"description": "Retry or remove one queue row by its index.",
"schema": _obj(
{
"index": {"type": "integer", "minimum": 0, "description": "Row index from queue."},
"action": {"type": "string", "enum": ["retry", "remove"]},
},
["index", "action"],
),
"write": True,
"fn": t_queue_action,
},
{
"name": "audit_run",
"description": "Re-verify one platform (or all) against the loaded No-Intro/Redump DATs. Long-running.",
"schema": _obj({"platform": P_PLATFORM}),
"write": True,
"fn": t_audit_run,
},
{
"name": "quarantine_duplicates",
"description": "Move selected byte-identical duplicates into ROMarr's quarantine. Reversible via the manifest, but it moves files: requires confirm=true.",
"schema": _obj(
{
"files": {"type": "array", "items": {"type": "string"}, "description": "File paths from duplicates."},
"platform": P_PLATFORM,
"confirm": {"type": "boolean", "description": "Must be true; acknowledgement that files are moved."},
},
["files", "confirm"],
),
"write": True,
"fn": t_quarantine,
},
{
"name": "rename_preview",
"description": "Build a read-only, DAT-verified rename preview (e.g. scope='missing-dat-names'). Nothing is renamed.",
"schema": _obj({"platform": P_PLATFORM, "scope": {"type": "string"}, "offset": {"type": "integer", "minimum": 0}}),
"write": True,
"fn": t_rename_preview,
},
{
"name": "import_finished",
"description": "Run an import pass over finished downloads, so completed items land in the library.",
"schema": _obj({}),
"write": True,
"fn": t_import_finished,
},
{
"name": "manual_scan",
"description": "Scan a directory on ROMarr's host for files it could adopt, without importing them.",
"schema": _obj({"path": {"type": "string"}, "platform": P_PLATFORM}, ["path"]),
"write": False,
"fn": t_manual_scan,
},
]
BY_NAME = {t["name"]: t for t in TOOLS}
def tool_descriptors():
out = []
for t in TOOLS:
d = {"name": t["name"], "description": t["description"], "inputSchema": t["schema"]}
d["annotations"] = {
"title": t["name"].replace("_", " ").title(),
"readOnlyHint": not t["write"],
"destructiveHint": t["name"] in ("quarantine_duplicates", "queue_action"),
"openWorldHint": True,
}
out.append(d)
return out
# ----------------------------------------------------------------------- protocol
def handle(req):
method = req.get("method")
rid = req.get("id")
params = req.get("params") or {}
if method == "initialize":
return {
"protocolVersion": params.get("protocolVersion") or PROTOCOL_VERSION,
"capabilities": {"tools": {"listChanged": False}},
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
}
if method == "ping":
return {}
if method == "tools/list":
return {"tools": tool_descriptors()}
if method == "tools/call":
name = params.get("name")
tool = BY_NAME.get(name)
if tool is None:
raise ToolError(f"unknown tool: {name}")
if tool["write"] and READ_ONLY:
raise ToolError("ROMARR_READ_ONLY is set; this server only reads right now.")
result = tool["fn"](params.get("arguments") or {})
return {
"content": [{"type": "text", "text": json.dumps(result, indent=2, ensure_ascii=False)[:100000]}],
"structuredContent": result if isinstance(result, dict) else {"result": result},
"isError": False,
}
if method in ("notifications/initialized", "notifications/cancelled"):
return None
if method == "resources/list":
return {"resources": []}
if method == "prompts/list":
return {"prompts": []}
raise _UnknownMethod(method)
class _UnknownMethod(Exception):
def __init__(self, method):
self.method = method
def _read_message(stream):
"""Newline-delimited JSON, tolerating the LSP-style Content-Length framing."""
while True:
line = stream.readline()
if not line:
return None
line = line.strip()
if not line:
continue
if line.lower().startswith("content-length:"):
try:
length = int(line.split(":", 1)[1].strip())
except ValueError:
continue
while True: # skip remaining headers up to the blank line
hdr = stream.readline()
if not hdr or not hdr.strip():
break
payload = stream.read(length)
if not payload:
return None
try:
return json.loads(payload)
except json.JSONDecodeError:
continue
try:
return json.loads(line)
except json.JSONDecodeError:
continue
def _write_message(obj):
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
sys.stdout.flush()
def main():
if not API_KEY:
sys.stderr.write("romarr-mcp: ROMARR_API_KEY is not set; every tool will fail.\n")
sys.stderr.write(f"romarr-mcp {SERVER_VERSION}: stdio, target {BASE_URL}, {len(TOOLS)} tools\n")
sys.stderr.flush()
while True:
msg = _read_message(sys.stdin)
if msg is None:
return 0
if not isinstance(msg, dict) or "method" not in msg:
continue
is_notification = "id" not in msg
try:
result = handle(msg)
except _UnknownMethod as exc:
if not is_notification:
_write_message({"jsonrpc": "2.0", "id": msg.get("id"), "error": {"code": -32601, "message": f"method not found: {exc.method}"}})
continue
except ToolError as exc:
if not is_notification:
_write_message(
{
"jsonrpc": "2.0",
"id": msg.get("id"),
"result": {"content": [{"type": "text", "text": f"error: {exc}"}], "isError": True},
}
)
continue
except Exception as exc: # noqa: BLE001 - never kill the server on one bad call
if not is_notification:
_write_message(
{
"jsonrpc": "2.0",
"id": msg.get("id"),
"result": {"content": [{"type": "text", "text": f"error: {type(exc).__name__}: {exc}"}], "isError": True},
}
)
continue
if is_notification or result is None:
continue
_write_message({"jsonrpc": "2.0", "id": msg.get("id"), "result": result})
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
sys.exit(0)
+1267
View File
File diff suppressed because it is too large. Load diff
+44
View File
@@ -0,0 +1,44 @@
{
lib,
buildNpmPackage,
fetchzip,
}:
buildNpmPackage rec {
pname = "romm-mcp";
version = "1.0.5";
src = fetchzip {
url = "https://registry.npmjs.org/${pname}/-/${pname}-${version}.tgz";
hash = "sha256-SOYzG0CtNSvIrxOYJq3UDXRlXylqbDjo2f5qfpbiYmI=";
};
# Upstream publishes no package-lock.json (only package.json), and
# buildNpmPackage requires one to compute npmDepsHash. This lock is generated
# from the published tarball itself -- `npm install --package-lock-only
# --ignore-scripts` inside the extracted 1.0.5 package -- so it locks exactly
# the dependency tree that shipped with this version.
npmDepsHash = "sha256-oSlECdIf2ZP8ZZhOUChkcJAmUTj6TFTdeTJLY5RwLGo=";
postPatch = ''
cp ${./package-lock.json} package-lock.json
# romm_system action=scan was stubbed out client-side ("Scan endpoint removed in
# RomM 5.0"), but RomM 5.4 restored POST /api/tasks/scan, so the stub only blocks a
# working endpoint. Upstream publishes no sources in the npm tarball, so this patches
# the compiled dist/ directly; drop it once upstream restores runScan().
patch -p1 < ${./romm-mcp-scan.patch}
'';
# dist/*.js (tsc output) ships inside the npm tarball, so there is nothing to
# compile here -- same arrangement as navidrome-mcp and n8n-mcp.
dontNpmBuild = true;
meta = with lib; {
description = "MCP server for RomM: browse, search and administrate a self-hosted ROM library";
homepage = "https://github.com/rodrigosiviero/romm-mcp";
license = licenses.mit;
mainProgram = pname;
platforms = platforms.all;
};
}
+19
View File
@@ -0,0 +1,19 @@
--- a/dist/client.js
+++ b/dist/client.js
@@ -244,8 +244,14 @@
// NOTE: RomM 5.0 removed the POST /api/tasks/run endpoint.
// Scans are now managed through RQ workers and require background job management.
// Use the RomM web UI for scans and metadata updates.
- async runScan() {
- throw new Error("Scan endpoint removed in RomM 5.0. Use the RomM web UI to trigger library scans.");
+ async runScan(scanType = "quick") {
+ // RomM 5.4 restored library scans behind POST /api/tasks/scan (ScanPayload:
+ // type, platforms, platform_fs_slugs, roms_ids, apis). The stub above predates
+ // that. "quick" indexes new/changed files; "full" re-hashes the whole library.
+ return this.request("/api/tasks/scan", {
+ method: "POST",
+ body: JSON.stringify({ type: scanType }),
+ });
}
async runMetadataScan() {
throw new Error("Metadata scan endpoint removed in RomM 5.0. Use the RomM web UI to trigger metadata updates.");