Add standalone Unraid ARR MCP package
This commit is contained in:
@@ -92,6 +92,11 @@ Details, Prüfschritte und der exakte Sicherungsumfang stehen in
|
|||||||
|
|
||||||
Die MCPHub-Installation, Endpunkte und der schrittweise Rückbau der alten
|
Die MCPHub-Installation, Endpunkte und der schrittweise Rückbau der alten
|
||||||
Athena-MCPs stehen in [`platform/mcphub/README.md`](platform/mcphub/README.md).
|
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
|
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)
|
[`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.
|
ein DockerMan-Template, das unverändert das offizielle Nous-Image verwendet.
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<Container version="2">
|
||||||
|
<Name>ARR-MCP</Name>
|
||||||
|
<Repository>mike-ai/arr-mcp:1.0.0</Repository>
|
||||||
|
<Network>bridge</Network>
|
||||||
|
<Shell>sh</Shell>
|
||||||
|
<Privileged>false</Privileged>
|
||||||
|
<Project>https://git.casaderoll.de/michael/AI-Profile-Router</Project>
|
||||||
|
<ReadMe>https://git.casaderoll.de/michael/AI-Profile-Router/src/branch/main/services/arr-mcp/README.md</ReadMe>
|
||||||
|
<Overview>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.</Overview>
|
||||||
|
<Category>AI:</Category>
|
||||||
|
<WebUI/>
|
||||||
|
<TemplateURL>false</TemplateURL>
|
||||||
|
<Icon>https://raw.githubusercontent.com/Sonarr/Sonarr/develop/Logo/256.png</Icon>
|
||||||
|
<ExtraParams>--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</ExtraParams>
|
||||||
|
<PostArgs/>
|
||||||
|
<CPUset/>
|
||||||
|
<DateInstalled/>
|
||||||
|
<DonateText/>
|
||||||
|
<DonateLink/>
|
||||||
|
<Requires>Das lokale Image mike-ai/arr-mcp:1.0.0 muss vorher mit services/arr-mcp/install-on-unraid.sh gebaut worden sein.</Requires>
|
||||||
|
<Config Name="MCP Port" Target="8000" Default="8207" Mode="tcp" Description="Streamable-HTTP-Endpunkt; MCP-URL ist http://UNRAID-IP:8207/mcp." Type="Port" Display="always" Required="true" Mask="false">8207</Config>
|
||||||
|
</Container>
|
||||||
Regular → Executable
+54
-12
@@ -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:
|
class FakeSonarrClient:
|
||||||
@@ -29,24 +69,26 @@ class FakeSonarrClient:
|
|||||||
{"id": 2, "seasonNumber": 7, "episodeNumber": 2, "hasFile": False},
|
{"id": 2, "seasonNumber": 7, "episodeNumber": 2, "hasFile": False},
|
||||||
]
|
]
|
||||||
|
|
||||||
def post_release(self, data):
|
def post_release(self, data=None, **kwargs):
|
||||||
self.posted.append(data)
|
payload = data if data is not None else kwargs
|
||||||
return data
|
self.posted.append(payload)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase):
|
class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase):
|
||||||
def setUp(self) -> None:
|
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:
|
async def test_exact_release_requires_preview_and_ticket(self) -> None:
|
||||||
client = FakeSonarrClient()
|
client = FakeSonarrClient()
|
||||||
scope = {"series_id": 42, "season_number": 7, "guid": "exact-guid"}
|
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.assertFalse(preview["download_started"])
|
||||||
self.assertEqual(preview["existing_episode_files_in_season"], 1)
|
self.assertEqual(preview["existing_episode_files_in_season"], 1)
|
||||||
self.assertTrue(preview["approval_ticket"])
|
self.assertTrue(preview["approval_ticket"])
|
||||||
|
|
||||||
result = await mcp_sonarr._grab_release(
|
result = await self.module._grab_release(
|
||||||
client,
|
client,
|
||||||
{**scope, "confirm": True, "approval_ticket": preview["approval_ticket"]},
|
{**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:
|
async def test_rejected_release_needs_force_in_preview(self) -> None:
|
||||||
client = FakeSonarrClient(rejected=True)
|
client = FakeSonarrClient(rejected=True)
|
||||||
scope = {"series_id": 42, "season_number": 7, "guid": "exact-guid"}
|
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.assertTrue(blocked["force_required"])
|
||||||
self.assertIsNone(blocked["approval_ticket"])
|
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.assertFalse(approved["force_required"])
|
||||||
self.assertTrue(approved["approval_ticket"])
|
self.assertTrue(approved["approval_ticket"])
|
||||||
|
|
||||||
async def test_guid_must_still_match_current_sonarr_results(self) -> None:
|
async def test_guid_must_still_match_current_sonarr_results(self) -> None:
|
||||||
client = FakeSonarrClient()
|
client = FakeSonarrClient()
|
||||||
with self.assertRaisesRegex(ValueError, "no longer present"):
|
with self.assertRaisesRegex(ValueError, "no longer present"):
|
||||||
await mcp_sonarr._preview_release_grab(
|
await self.module._preview_release_grab(
|
||||||
client,
|
client,
|
||||||
{"series_id": 42, "season_number": 7, "guid": "different-guid"},
|
{"series_id": 42, "season_number": 7, "guid": "different-guid"},
|
||||||
)
|
)
|
||||||
@@ -91,7 +133,7 @@ class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
"releaseGroup": "HQC",
|
"releaseGroup": "HQC",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
result = await mcp_sonarr._search_releases(
|
result = await self.module._search_releases(
|
||||||
client,
|
client,
|
||||||
{
|
{
|
||||||
"series_id": 42,
|
"series_id": 42,
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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://<UNRAID-IP>: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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+47
@@ -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"
|
||||||
Executable
+86
@@ -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"
|
||||||
Executable
+8
@@ -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
|
||||||
Reference in New Issue
Block a user