diff --git a/README.md b/README.md
index e202f5b..eee33ed 100644
--- a/README.md
+++ b/README.md
@@ -92,6 +92,11 @@ Details, Prüfschritte und der exakte Sicherungsumfang stehen in
Die MCPHub-Installation, Endpunkte und der schrittweise Rückbau der alten
Athena-MCPs stehen in [`platform/mcphub/README.md`](platform/mcphub/README.md).
+Der eigenständig betreibbare Sonarr-/Radarr-Container einschließlich
+Debian-Slim-Installer und Unraid-Template liegt unter
+[`services/arr-mcp/`](services/arr-mcp/README.md). Er ist für die schrittweise
+Ablösung des bisherigen MCPHub-Prozesses vorbereitet, wird durch Athenas
+Standardinstallation aber nicht automatisch gestartet.
Für Installation oder Wiederherstellung des Hermes-Gateways auf Unraid liegt unter
[`config/unraid-templates/my-Hermes-Agent-Official.xml`](config/unraid-templates/my-Hermes-Agent-Official.xml)
ein DockerMan-Template, das unverändert das offizielle Nous-Image verwendet.
diff --git a/config/unraid-templates/my-ARR-MCP.xml b/config/unraid-templates/my-ARR-MCP.xml
new file mode 100644
index 0000000..8f57bd1
--- /dev/null
+++ b/config/unraid-templates/my-ARR-MCP.xml
@@ -0,0 +1,23 @@
+
+
+ ARR-MCP
+ mike-ai/arr-mcp:1.0.0
+ bridge
+ sh
+ false
+ https://git.casaderoll.de/michael/AI-Profile-Router
+ https://git.casaderoll.de/michael/AI-Profile-Router/src/branch/main/services/arr-mcp/README.md
+ Eigenständiger Sonarr- und Radarr-MCP für Hermes und andere MCP-Clients. Das lokale Image wird reproduzierbar aus dem privaten Git gebaut. Der Container nutzt kompakte, modellfreundliche Werkzeuge, ein begrenztes Codec-Inventar und ticketgebundene Sonarr-Schreibaktionen. Konfiguration und Schlüssel liegen ausschließlich in Unraid-Appdata.
+ AI:
+
+ false
+ https://raw.githubusercontent.com/Sonarr/Sonarr/develop/Logo/256.png
+ --read-only --cap-drop=ALL --security-opt=no-new-privileges --pids-limit=256 --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m --env-file=/mnt/nvme-storage/appdata/ARR-MCP/arr-mcp.env
+
+
+
+
+
+ Das lokale Image mike-ai/arr-mcp:1.0.0 muss vorher mit services/arr-mcp/install-on-unraid.sh gebaut worden sein.
+ 8207
+
diff --git a/dev/test_sonarr_release_grab.py b/dev/test_sonarr_release_grab.py
old mode 100644
new mode 100755
index aec38f9..448d23c
--- a/dev/test_sonarr_release_grab.py
+++ b/dev/test_sonarr_release_grab.py
@@ -1,6 +1,46 @@
-import unittest
+#!/usr/bin/env python3
+"""Focused offline tests for the model-oriented Sonarr overlay."""
-from arr_mcp.mcp import mcp_sonarr
+from __future__ import annotations
+
+import importlib.util
+import sys
+import types
+import unittest
+from pathlib import Path
+
+
+SOURCE = Path(__file__).parents[1] / "platform/mcp/patches/mcp_sonarr.py"
+
+
+def load_module():
+ async def run_blocking(function, *args, **kwargs):
+ kwargs.pop("service", None)
+ return function(*args, **kwargs)
+
+ def dispatch(client, action, kwargs, **_options):
+ return getattr(client, action)(**kwargs)
+
+ utilities = types.ModuleType("agent_utilities.mcp_utilities")
+ utilities.dispatch = dispatch
+ utilities.run_blocking = run_blocking
+ sys.modules["agent_utilities"] = types.ModuleType("agent_utilities")
+ sys.modules["agent_utilities.mcp_utilities"] = utilities
+ fastmcp = types.ModuleType("fastmcp")
+ fastmcp.FastMCP = object
+ sys.modules["fastmcp"] = fastmcp
+ pydantic = types.ModuleType("pydantic")
+ pydantic.Field = lambda *args, **kwargs: kwargs.get("default")
+ sys.modules["pydantic"] = pydantic
+ auth = types.ModuleType("arr_mcp.auth")
+ auth.get_sonarr_client = lambda: None
+ sys.modules["arr_mcp"] = types.ModuleType("arr_mcp")
+ sys.modules["arr_mcp.auth"] = auth
+ spec = importlib.util.spec_from_file_location("sonarr_patch", SOURCE)
+ module = importlib.util.module_from_spec(spec)
+ assert spec.loader
+ spec.loader.exec_module(module)
+ return module
class FakeSonarrClient:
@@ -29,24 +69,26 @@ class FakeSonarrClient:
{"id": 2, "seasonNumber": 7, "episodeNumber": 2, "hasFile": False},
]
- def post_release(self, data):
- self.posted.append(data)
- return data
+ def post_release(self, data=None, **kwargs):
+ payload = data if data is not None else kwargs
+ self.posted.append(payload)
+ return payload
class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
- mcp_sonarr._APPROVALS.clear()
+ self.module = load_module()
+ self.module._APPROVALS.clear()
async def test_exact_release_requires_preview_and_ticket(self) -> None:
client = FakeSonarrClient()
scope = {"series_id": 42, "season_number": 7, "guid": "exact-guid"}
- preview = await mcp_sonarr._preview_release_grab(client, scope)
+ preview = await self.module._preview_release_grab(client, scope)
self.assertFalse(preview["download_started"])
self.assertEqual(preview["existing_episode_files_in_season"], 1)
self.assertTrue(preview["approval_ticket"])
- result = await mcp_sonarr._grab_release(
+ result = await self.module._grab_release(
client,
{**scope, "confirm": True, "approval_ticket": preview["approval_ticket"]},
)
@@ -58,18 +100,18 @@ class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase):
async def test_rejected_release_needs_force_in_preview(self) -> None:
client = FakeSonarrClient(rejected=True)
scope = {"series_id": 42, "season_number": 7, "guid": "exact-guid"}
- blocked = await mcp_sonarr._preview_release_grab(client, scope)
+ blocked = await self.module._preview_release_grab(client, scope)
self.assertTrue(blocked["force_required"])
self.assertIsNone(blocked["approval_ticket"])
- approved = await mcp_sonarr._preview_release_grab(client, {**scope, "force": True})
+ approved = await self.module._preview_release_grab(client, {**scope, "force": True})
self.assertFalse(approved["force_required"])
self.assertTrue(approved["approval_ticket"])
async def test_guid_must_still_match_current_sonarr_results(self) -> None:
client = FakeSonarrClient()
with self.assertRaisesRegex(ValueError, "no longer present"):
- await mcp_sonarr._preview_release_grab(
+ await self.module._preview_release_grab(
client,
{"series_id": 42, "season_number": 7, "guid": "different-guid"},
)
@@ -91,7 +133,7 @@ class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase):
"releaseGroup": "HQC",
},
]
- result = await mcp_sonarr._search_releases(
+ result = await self.module._search_releases(
client,
{
"series_id": 42,
diff --git a/services/arr-mcp/Dockerfile b/services/arr-mcp/Dockerfile
new file mode 100644
index 0000000..6a0e747
--- /dev/null
+++ b/services/arr-mcp/Dockerfile
@@ -0,0 +1,37 @@
+FROM python:3.13-slim AS builder
+
+ARG ARR_MCP_VERSION=1.0.1
+
+RUN python -m venv /opt/arr-mcp/venv \
+ && /opt/arr-mcp/venv/bin/pip install --no-cache-dir --upgrade pip \
+ && /opt/arr-mcp/venv/bin/pip install --no-cache-dir "arr-mcp[mcp]==${ARR_MCP_VERSION}"
+
+COPY platform/mcp/patches/mcp_sonarr.py /tmp/mcp_sonarr.py
+COPY platform/mcp/patches/mcp_radarr.py /tmp/mcp_radarr.py
+
+RUN site_packages="$(/opt/arr-mcp/venv/bin/python -c 'import site; print(site.getsitepackages()[0])')" \
+ && install -m 0644 /tmp/mcp_sonarr.py "$site_packages/arr_mcp/mcp/mcp_sonarr.py" \
+ && install -m 0644 /tmp/mcp_radarr.py "$site_packages/arr_mcp/mcp/mcp_radarr.py" \
+ && /opt/arr-mcp/venv/bin/python -m compileall -q "$site_packages/arr_mcp/mcp"
+
+FROM python:3.13-slim
+
+RUN groupadd --system --gid 10001 arrmcp \
+ && useradd --system --uid 10001 --gid 10001 --no-create-home arrmcp
+
+COPY --from=builder /opt/arr-mcp /opt/arr-mcp
+COPY services/arr-mcp/run.sh /usr/local/bin/run-arr-mcp
+
+ENV PATH="/opt/arr-mcp/venv/bin:${PATH}" \
+ PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1
+
+RUN chmod 0755 /usr/local/bin/run-arr-mcp
+
+USER 10001:10001
+EXPOSE 8000
+
+HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
+ CMD python -c "import socket; s=socket.create_connection(('127.0.0.1',8000),3); s.close()"
+
+ENTRYPOINT ["/usr/local/bin/run-arr-mcp"]
diff --git a/services/arr-mcp/README.md b/services/arr-mcp/README.md
new file mode 100644
index 0000000..6d9227a
--- /dev/null
+++ b/services/arr-mcp/README.md
@@ -0,0 +1,155 @@
+# ARR-MCP auf Unraid
+
+Dieser Ordner baut **einen eigenständigen Container nur für Sonarr und Radarr**.
+Hermes, Pi Coding und andere MCP-Clients erreichen ihn anschließend über
+`http://:8207/mcp`.
+
+## Warum dieser Fork bleibt
+
+Geprüft am 27. August 2026:
+
+- [`Knuckles-Team/arr-mcp`](https://github.com/Knuckles-Team/arr-mcp) 2.1.0
+ ist der moderne Upstream unseres Pakets, stellt
+ in der kompakten Oberfläche aber weiterhin generische `*_action`-Werkzeuge
+ mit frei wählbaren API-Methoden bereit. Das führte bei lokalen Modellen zu
+ falschen Aktionen, unnötigen Schemaabfragen und großen Antworten.
+- Andere öffentliche ARR-MCPs bieten teilweise mehr Dienste oder eine eigene
+ Weboberfläche, ersetzen aber nicht unser kompaktes Radarr-Codec-Inventar und
+ den an eine Vorschau gebundenen Sonarr-Freigabeablauf.
+
+Wir bleiben deshalb vorläufig bei `arr-mcp` 1.0.1 plus zwei kleinen,
+versionierten Patches. Ein späterer Wechsel ist sinnvoll, sobald ein Upstream
+diese Eigenschaften ohne lokale Anpassungen anbietet.
+
+## Besondere Werkzeuge
+
+- `radarr_movie_codec_inventory`: kompakte, paginierte Liste mit Codec,
+ Auflösung, Sprachen und Dateigröße; keine riesigen Radarr-Rohantworten.
+- `sonarr_action`: begrenzte, kompakte Sonarr-Aktionen wie `find_series`,
+ `get_season_summary` und `search_releases`.
+- Sonarr-Schreibaktionen sind standardmäßig abgeschaltet. Im Schreibmodus
+ benötigen `start_episode_search` und `grab_release` zuerst eine passende
+ Vorschau, danach eine ausdrückliche Freigabe und ein kurzlebiges Ticket.
+
+## Empfohlene Installation auf Unraid
+
+Im vollständigen Checkout des Repositories als root:
+
+```bash
+./services/arr-mcp/install-on-unraid.sh
+```
+
+Beim ersten Aufruf entsteht:
+
+```text
+/mnt/nvme-storage/appdata/ARR-MCP/arr-mcp.env
+```
+
+Dort Sonarr- und Radarr-Schlüssel eintragen. Danach denselben Installationsbefehl
+erneut ausführen. Das Skript:
+
+1. baut das lokale Image `mike-ai/arr-mcp:1.0.0` aus `python:3.13-slim`,
+2. startet ausschließlich den Container `ARR-MCP` im normalen Bridge-Netz,
+3. wartet auf den Healthcheck und
+4. installiert das DockerMan-Template für spätere Bedienung in der Unraid-GUI.
+
+Es liest oder druckt keine Schlüssel. Eine vorhandene Konfiguration wird nie
+überschrieben.
+
+### Kurzer Auftrag für Hermes/Qwen
+
+```text
+Klone auf Unraid das Repository
+ssh://git@192.168.1.2:33/michael/AI-Profile-Router.git, lies vollständig
+services/arr-mcp/README.md und installiere ausschließlich nach dem dort
+dokumentierten Weg. Verwende services/arr-mcp/install-on-unraid.sh. Zeige keine
+API-Schlüssel an und überschreibe keine vorhandene arr-mcp.env. Stoppe, falls
+Werte fehlen, und sage mir nur, welche Variablennamen ich eintragen muss.
+Entferne den bisherigen ARR-Endpunkt erst nach einem erfolgreichen Healthcheck
+und MCP-Handshake des neuen Containers.
+```
+
+## Installation in einem vorhandenen Debian-Slim-Container
+
+Nur verwenden, wenn bereits bewusst ein nackter Debian-Slim-Container mit dem
+kompletten Git-Checkout läuft:
+
+```bash
+./services/arr-mcp/install-in-debian-slim.sh
+```
+
+Danach startet `/usr/local/bin/run-arr-mcp` den Server. Dieser Weg funktioniert,
+ist aber weniger reproduzierbar als der Dockerfile-Build: Eine Neuerstellung des
+nackten Containers entfernt die Installation. Für den Produktivbetrieb deshalb
+den ersten Weg verwenden.
+
+## Hermes oder anderer MCP-Client
+
+Direkte Registrierung:
+
+```yaml
+mcp_servers:
+ arr:
+ url: http://192.168.1.2:8207/mcp
+ timeout: 600
+```
+
+Nach dem Eintragen die MCP-Liste des Clients neu laden beziehungsweise einen
+neuen Chat öffnen. Sonarr und Radarr erscheinen als Werkzeuge desselben
+Fach-MCPs; sie sind nicht Bestandteil des Hermes-Containers.
+
+## Schreiben aktivieren
+
+In `arr-mcp.env`:
+
+```text
+ARR_MCP_WRITE=1
+```
+
+Danach nur diesen Container neu starten. Selbst dann erlaubt der Sonarr-Patch
+nur die dokumentierten, ticketgebundenen Aktionen. Radarr besitzt derzeit noch
+keinen gleichwertigen Freigabeablauf; Änderungen dort nur auf einen eindeutigen
+Benutzerauftrag ausführen.
+
+## Update und Test
+
+Nach einem Git-Update denselben Befehl erneut ausführen:
+
+```bash
+./services/arr-mcp/install-on-unraid.sh
+```
+
+Status und Logs:
+
+```bash
+docker inspect --format '{{.State.Health.Status}}' ARR-MCP
+docker logs --tail 100 ARR-MCP
+```
+
+Die Offline-Tests des Forks:
+
+```bash
+python3 dev/test_radarr_patch.py
+python3 -m unittest dev/test_sonarr_release_grab.py
+```
+
+## Deinstallation
+
+```bash
+./services/arr-mcp/install-on-unraid.sh uninstall
+```
+
+Der Container verschwindet, während die Appdata-Konfiguration als Rückfall
+erhalten bleibt. Erst wenn sie wirklich nicht mehr benötigt wird, kann
+`/mnt/nvme-storage/appdata/ARR-MCP` separat gelöscht werden.
+
+## Regeln für Hermes/Qwen
+
+1. Diese README vollständig lesen.
+2. Keine eigene ARR-Implementierung und keinen zweiten Sonarr-/Radarr-Dienst
+ erstellen.
+3. Ausschließlich `install-on-unraid.sh` verwenden; keine Befehle improvisieren.
+4. Vorhandene `arr-mcp.env` weder anzeigen noch überschreiben.
+5. Nach Installation Healthcheck und MCP-Handshake prüfen.
+6. Den alten MCPHub-/Athena-Endpunkt erst entfernen, wenn der neue Endpunkt
+ nachweislich funktioniert.
diff --git a/services/arr-mcp/arr-mcp.env.example b/services/arr-mcp/arr-mcp.env.example
new file mode 100644
index 0000000..c259df3
--- /dev/null
+++ b/services/arr-mcp/arr-mcp.env.example
@@ -0,0 +1,28 @@
+# Nur diese Datei kopieren und die beiden Schlüssel eintragen. Keine echten
+# Zugangsdaten in Git committen.
+
+SONARRTOOL=True
+SONARR_BASE_URL=http://192.168.1.2:8989
+SONARR_TOKEN=CHANGE_ME
+
+RADARRTOOL=True
+RADARR_BASE_URL=http://192.168.1.2:7878
+RADARR_TOKEN=CHANGE_ME
+
+# Andere *arr-Dienste sind in diesem Container absichtlich deaktiviert.
+LIDARRTOOL=False
+PROWLARRTOOL=False
+BAZARRTOOL=False
+SEERRTOOL=False
+CHAPTARRTOOL=False
+
+# Zwei kompakte Werkzeuge statt hunderter Einzeltools.
+MCP_TOOL_MODE=condensed
+
+# Standardmäßig rein lesend. Erst nach bewusster Entscheidung auf 1 setzen.
+# Auch im Schreibmodus bleiben nur die eng begrenzten, ticketgebundenen
+# Sonarr-Aktionen unseres Patches erlaubt.
+ARR_MCP_WRITE=0
+
+ARR_MCP_HOST=0.0.0.0
+ARR_MCP_PORT=8000
diff --git a/services/arr-mcp/compose.yaml b/services/arr-mcp/compose.yaml
new file mode 100644
index 0000000..b8fc558
--- /dev/null
+++ b/services/arr-mcp/compose.yaml
@@ -0,0 +1,29 @@
+services:
+ arr-mcp:
+ build:
+ context: ../..
+ dockerfile: services/arr-mcp/Dockerfile
+ args:
+ ARR_MCP_VERSION: "1.0.1"
+ image: mike-ai/arr-mcp:1.0.0
+ container_name: ARR-MCP
+ restart: unless-stopped
+ network_mode: bridge
+ env_file:
+ - ${ARR_MCP_ENV_FILE:-/mnt/nvme-storage/appdata/ARR-MCP/arr-mcp.env}
+ ports:
+ - "${ARR_MCP_PUBLIC_PORT:-8207}:8000"
+ read_only: true
+ tmpfs:
+ - /tmp:rw,noexec,nosuid,nodev,size=64m
+ cap_drop:
+ - ALL
+ security_opt:
+ - no-new-privileges:true
+ pids_limit: 256
+ healthcheck:
+ test: ["CMD", "python", "-c", "import socket; s=socket.create_connection(('127.0.0.1',8000),3); s.close()"]
+ interval: 30s
+ timeout: 5s
+ start_period: 20s
+ retries: 3
diff --git a/services/arr-mcp/install-in-debian-slim.sh b/services/arr-mcp/install-in-debian-slim.sh
new file mode 100755
index 0000000..fe6c2e3
--- /dev/null
+++ b/services/arr-mcp/install-in-debian-slim.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+# Installation innerhalb eines bereits laufenden Debian-Slim-Containers.
+# Der empfohlene, reproduzierbare Weg bleibt der Dockerfile-Build.
+set -Eeuo pipefail
+
+[[ $EUID -eq 0 ]] || { echo "Bitte im Container als root ausführen." >&2; exit 1; }
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+SONARR_PATCH="$REPO_ROOT/platform/mcp/patches/mcp_sonarr.py"
+RADARR_PATCH="$REPO_ROOT/platform/mcp/patches/mcp_radarr.py"
+
+[[ -f $SONARR_PATCH && -f $RADARR_PATCH ]] || {
+ echo "Die Installation muss aus einem vollständigen Checkout dieses Repositories laufen." >&2
+ exit 2
+}
+
+need_python_packages=0
+venv_check_dir="$(mktemp -d)"
+if ! command -v python3 >/dev/null 2>&1 \
+ || ! python3 -m venv "$venv_check_dir/check" >/dev/null 2>&1; then
+ need_python_packages=1
+fi
+rm -rf "$venv_check_dir"
+
+if (( need_python_packages )); then
+ command -v apt-get >/dev/null 2>&1 || {
+ echo "Dieser Installer erwartet Debian/Ubuntu mit apt-get." >&2
+ exit 3
+ }
+ apt-get update
+ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+ ca-certificates python3 python3-pip python3-venv
+ rm -rf /var/lib/apt/lists/*
+fi
+
+python3 -m venv /opt/arr-mcp/venv
+/opt/arr-mcp/venv/bin/pip install --no-cache-dir --upgrade pip
+/opt/arr-mcp/venv/bin/pip install --no-cache-dir "arr-mcp[mcp]==1.0.1"
+
+site_packages="$(/opt/arr-mcp/venv/bin/python -c 'import site; print(site.getsitepackages()[0])')"
+install -m 0644 "$SONARR_PATCH" "$site_packages/arr_mcp/mcp/mcp_sonarr.py"
+install -m 0644 "$RADARR_PATCH" "$site_packages/arr_mcp/mcp/mcp_radarr.py"
+install -m 0755 "$SCRIPT_DIR/run.sh" /usr/local/bin/run-arr-mcp
+
+/opt/arr-mcp/venv/bin/python -m compileall -q "$site_packages/arr_mcp/mcp"
+echo "ARR-MCP ist installiert. Start: /usr/local/bin/run-arr-mcp"
diff --git a/services/arr-mcp/install-on-unraid.sh b/services/arr-mcp/install-on-unraid.sh
new file mode 100755
index 0000000..245c885
--- /dev/null
+++ b/services/arr-mcp/install-on-unraid.sh
@@ -0,0 +1,86 @@
+#!/usr/bin/env bash
+# Baut und verwaltet ausschließlich den eigenständigen ARR-MCP-Container.
+set -Eeuo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+APPDATA_DIR=${ARR_MCP_APPDATA_DIR:-/mnt/nvme-storage/appdata/ARR-MCP}
+ENV_FILE=${ARR_MCP_ENV_FILE:-$APPDATA_DIR/arr-mcp.env}
+PUBLIC_PORT=${ARR_MCP_PUBLIC_PORT:-8207}
+TEMPLATE_SOURCE="$REPO_ROOT/config/unraid-templates/my-ARR-MCP.xml"
+TEMPLATE_DEST=${ARR_MCP_TEMPLATE_DEST:-/boot/config/plugins/dockerMan/templates-user/my-ARR-MCP.xml}
+
+usage() {
+ cat <<'EOF'
+Verwendung:
+ ./services/arr-mcp/install-on-unraid.sh
+ ./services/arr-mcp/install-on-unraid.sh uninstall
+
+Der erste Lauf legt nur dann eine Beispielkonfiguration an, wenn noch keine
+existiert. Echte Schlüssel werden niemals überschrieben oder ausgegeben.
+EOF
+}
+
+command=${1:-install}
+case "$command" in
+ -h|--help) usage; exit 0 ;;
+ install|uninstall) ;;
+ *) usage >&2; exit 2 ;;
+esac
+
+command -v docker >/dev/null 2>&1 || { echo "Docker wurde nicht gefunden." >&2; exit 1; }
+docker compose version >/dev/null 2>&1 || { echo "Docker Compose v2 wurde nicht gefunden." >&2; exit 1; }
+
+compose=(docker compose -f "$SCRIPT_DIR/compose.yaml")
+
+if [[ $command == uninstall ]]; then
+ ARR_MCP_ENV_FILE="$ENV_FILE" ARR_MCP_PUBLIC_PORT="$PUBLIC_PORT" \
+ "${compose[@]}" down --remove-orphans
+ echo "ARR-MCP wurde entfernt. Appdata und Konfiguration bleiben erhalten: $APPDATA_DIR"
+ exit 0
+fi
+
+install -d -m 0700 "$APPDATA_DIR"
+if [[ ! -e $ENV_FILE ]]; then
+ install -m 0600 "$SCRIPT_DIR/arr-mcp.env.example" "$ENV_FILE"
+ echo "Beispielkonfiguration angelegt: $ENV_FILE"
+ echo "Bitte SONARR_TOKEN und RADARR_TOKEN eintragen und denselben Befehl erneut starten."
+ exit 3
+fi
+
+# Nur Vollständigkeit prüfen; Werte niemals ausgeben.
+for key in SONARR_TOKEN RADARR_TOKEN; do
+ if ! grep -Eq "^${key}=[^[:space:]]+" "$ENV_FILE" \
+ || grep -Eq "^${key}=CHANGE_ME$" "$ENV_FILE"; then
+ echo "In $ENV_FILE fehlt noch $key." >&2
+ exit 3
+ fi
+done
+
+docker build \
+ --build-arg ARR_MCP_VERSION=1.0.1 \
+ -f "$SCRIPT_DIR/Dockerfile" \
+ -t mike-ai/arr-mcp:1.0.0 \
+ "$REPO_ROOT"
+
+ARR_MCP_ENV_FILE="$ENV_FILE" ARR_MCP_PUBLIC_PORT="$PUBLIC_PORT" \
+ "${compose[@]}" up -d --no-build
+
+if [[ -d $(dirname "$TEMPLATE_DEST") ]]; then
+ install -m 0644 "$TEMPLATE_SOURCE" "$TEMPLATE_DEST"
+fi
+
+for _ in {1..30}; do
+ health=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' ARR-MCP 2>/dev/null || true)
+ [[ $health == healthy ]] && break
+ sleep 1
+done
+
+health=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' ARR-MCP 2>/dev/null || true)
+[[ $health == healthy ]] || {
+ echo "ARR-MCP wurde gestartet, ist aber noch nicht healthy (Status: ${health:-unbekannt})." >&2
+ echo "Prüfen mit: docker logs --tail 100 ARR-MCP" >&2
+ exit 1
+}
+
+echo "ARR-MCP läuft: http://$(hostname -I | awk '{print $1}'):$PUBLIC_PORT/mcp"
diff --git a/services/arr-mcp/run.sh b/services/arr-mcp/run.sh
new file mode 100755
index 0000000..79b6eaf
--- /dev/null
+++ b/services/arr-mcp/run.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env sh
+set -eu
+
+exec /opt/arr-mcp/venv/bin/arr-mcp \
+ --transport streamable-http \
+ --host "${ARR_MCP_HOST:-0.0.0.0}" \
+ --port "${ARR_MCP_PORT:-8000}" \
+ --auth-type none