MUA v1.0.0 — Mikes Unraid Agent

Natives Unraid-Plugin: 21 Docker-, Netzwerk- und System-Tools
als MCP-Server (Streamable HTTP) auf Port 3002.

- mcp/server.php: MCP-Server (JSON-RPC 2.0, Streamable HTTP)
- mcp/helpers.php: Lokale Tool-Implementierungen (kein SSH)
- mcp/tools.php: 21 Tool-Definitionen
- mcp/selftest.php: Selftest (12 Checks)
- scripts/unraid-docker-mcp-helper.php: Write-Operationen
- scripts/mua.service: systemd-Service
- scripts/install.sh, update.sh, remove.sh: Plugin-Scripts
- MUA.plg: Plugin-Manifest
This commit is contained in:
Mikei386
2026-08-17 22:00:48 +02:00
commit f4e66ea5a1
12 changed files with 1837 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# MUA - Mikes Unraid Agent
# Install-Script
# Wird vom Unraid-Plugin-Installer aufgerufen.
set -e
PLUGIN_DIR="/usr/local/plugins/mua"
SERVICE_FILE="$PLUGIN_DIR/scripts/mua.service"
HELPER_SRC="$PLUGIN_DIR/scripts/unraid-docker-mcp-helper.php"
HELPER_DST="/usr/local/bin/unraid-docker-mcp-helper.php"
echo "=== MUA Install ==="
# 1. Verzeichnisse sicherstellen
mkdir -p "$PLUGIN_DIR/mcp"
mkdir -p "$PLUGIN_DIR/scripts"
# 2. PHP-Helper installieren (Write-Operationen)
if [ -f "$HELPER_SRC" ]; then
cp "$HELPER_SRC" "$HELPER_DST"
chmod 755 "$HELPER_DST"
echo " Helper installiert: $HELPER_DST"
fi
# 3. systemd-Service installieren
if [ -f "$SERVICE_FILE" ]; then
cp "$SERVICE_FILE" /etc/systemd/system/mua.service
chmod 644 /etc/systemd/system/mua.service
systemctl daemon-reload
echo " Service installiert: mua.service"
fi
# 4. Service starten + enable
systemctl enable mua.service
systemctl restart mua.service
# 5. Health-Check
sleep 2
if curl -sf "http://127.0.0.1:3002/health" > /dev/null 2>&1; then
echo " Health-Check: OK"
echo "=== MUA installiert und läuft auf Port 3002 ==="
echo " Endpunkt: http://$(hostname -I | awk '{print $1}'):3002/mcp"
else
echo " WARNUNG: Health-Check fehlgeschlagen"
echo " Service-Status:"
systemctl status mua.service --no-pager || true
exit 1
fi
+20
View File
@@ -0,0 +1,20 @@
[Unit]
Description=MUA - Mikes Unraid Agent (MCP Server)
After=network.target docker.service
Wants=docker.service
[Service]
Type=simple
# PHP Built-in Server auf Port 3002
ExecStart=/usr/bin/php -S 0.0.0.0:3002 /usr/local/plugins/mua/mcp/server.php
Restart=on-failure
RestartSec=5
# Arbeitsverzeichnis
WorkingDirectory=/usr/local/plugins/mua
# Sicherheits-Härtung
NoNewPrivileges=true
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# MUA - Mikes Unraid Agent
# Remove-Script
# Wird vom Unraid-Plugin-Installer bei Deinstallation aufgerufen.
set -e
echo "=== MUA Remove ==="
# 1. Service stoppen + disable
if systemctl list-unit-files | grep -q "^mua.service"; then
systemctl stop mua.service 2>/dev/null || true
systemctl disable mua.service 2>/dev/null || true
rm -f /etc/systemd/system/mua.service
systemctl daemon-reload
echo " Service entfernt"
fi
# 2. PHP-Helper entfernen
if [ -f "/usr/local/bin/unraid-docker-mcp-helper.php" ]; then
rm -f /usr/local/bin/unraid-docker-mcp-helper.php
echo " Helper entfernt"
fi
# 3. Plugin-Verzeichnis entfernen
if [ -d "/usr/local/plugins/mua" ]; then
rm -rf /usr/local/plugins/mua
echo " Plugin-Verzeichnis entfernt"
fi
echo "=== MUA entfernt ==="
+278
View File
@@ -0,0 +1,278 @@
<?php
/**
* Unraid Docker MCP Helper
* Write-Operationen für das Unraid MCP
* Aufruf: php /usr/local/bin/unraid-docker-mcp-helper.php <action> <args...>
*
* Actions:
* create <template_name>
* modify <container> <field> <value>
* update <container>
* rebuild <container>
*
* modify fields:
* port value: newExternalPort (z.B. "8466")
* oder newExternalPort:internalPort (z.B. "8466:8465")
* env value: VAR_NAME=VAR_VALUE (z.B. "TZ=Europe/Berlin")
* volume value: hostPath:containerPath (z.B. "/mnt/user/data:/data")
* network value: bridge|host|none
* privileged value: true|false
*/
error_reporting(E_ALL);
ini_set('display_errors', 1);
// ── Unraid Setup (aus update_container) ──────────────────────────────────
$docroot = '/usr/local/emhttp';
require_once "$docroot/webGui/include/Wrappers.php";
extract(parse_plugin_cfg('dynamix', true));
$_SERVER['REQUEST_URI'] = '';
$login_locale = _var($display, 'locale');
require_once "$docroot/plugins/dynamix.docker.manager/include/DockerClient.php";
$var = parse_ini_file('/var/local/emhttp/var.ini');
$DockerClient = new DockerClient();
$DockerUpdate = new DockerUpdate();
$DockerTemplates = new DockerTemplates();
$custom = DockerUtil::custom();
$subnet = DockerUtil::network($custom);
$cpus = DockerUtil::cpus();
// ── Helper-Funktionen ────────────────────────────────────────────────────
function out(string $msg): void {
echo $msg . "\n";
}
function fail(string $msg): never {
out("ERROR: " . $msg);
exit(1);
}
function docker_exec(string $cmd): string {
$proc = popen("/usr/bin/docker $cmd 2>&1", 'r');
$output = stream_get_contents($proc);
pclose($proc);
return trim($output);
}
function get_template_path(string $name): string {
$dir = '/boot/config/plugins/dockerMan/templates-user';
$file = "$dir/my-$name.xml";
if (!file_exists($file)) {
fail("Template not found: $file");
}
return $file;
}
function rebuild_container(string $name, bool $pull_image = false, bool $force_start = false): void {
global $DockerClient;
$tmpl = get_template_path($name);
$xml = file_get_contents($tmpl);
[$cmd, $Name, $Repository] = xmlToCommand($tmpl);
// Pull image if requested
if ($pull_image) {
out("Pulling image: $Repository");
$pull_out = docker_exec("pull $Repository");
if (strpos($pull_out, 'Error') !== false || strpos($pull_out, 'error') !== false) {
fail("Image pull failed: $pull_out");
}
out("Image pulled: $Repository");
}
// Check if container is running
$oldContainerInfo = $DockerClient->getContainerDetails($Name);
$startContainer = $force_start;
if (!empty($oldContainerInfo) && !empty($oldContainerInfo['State']) && !empty($oldContainerInfo['State']['Running'])) {
$startContainer = true;
out("Stopping container: $Name");
$DockerClient->stopContainer($Name);
}
// Convert create to run if we need to start
if ($startContainer) {
$cmd = str_replace('/docker create ', '/docker run -d ', $cmd);
}
// Remove old container
out("Removing old container: $Name");
$DockerClient->removeContainer($Name);
// Execute the docker command
out("Creating container: $Name");
$proc = popen("$cmd 2>&1", 'r');
$output = stream_get_contents($proc);
$rc = pclose($proc);
if ($rc !== 0) {
fail("Container creation failed (exit $rc): $output");
}
out("Container created: $Name");
// Flush caches
$DockerClient->flushCaches();
out("Done: $Name");
}
/**
* Set the text content of a DOM element.
* This is the actual value Unraid uses (not the Default attribute).
*/
function set_config_value(DOMElement $config, string $value): void {
// Remove existing text children
while ($config->firstChild) {
$config->removeChild($config->firstChild);
}
$config->appendChild(new DOMText($value));
// Also update Default attribute for consistency
$config->setAttribute('Default', $value);
}
function modify_template(string $name, string $field, string $value): void {
$tmpl = get_template_path($name);
$xml = file_get_contents($tmpl);
$dom = new DOMDocument();
$dom->loadXML($xml);
switch ($field) {
case 'port':
// value: newExternalPort or newExternalPort:internalPort
$parts = explode(':', $value, 2);
$newExternal = $parts[0];
$newInternal = $parts[1] ?? null;
$found = false;
foreach ($dom->getElementsByTagName('Config') as $config) {
if (strcasecmp($config->getAttribute('Type'), 'Port') === 0) {
if ($newInternal !== null) {
// Match by internal port (Target attribute)
if ($config->getAttribute('Target') === $newInternal) {
$config->setAttribute('Target', $newInternal);
set_config_value($config, $newExternal);
$found = true;
break;
}
} else {
// No internal port specified — change first Port config
set_config_value($config, $newExternal);
$found = true;
break;
}
}
}
if (!$found) {
fail("Port config not found in template" . ($newInternal !== null ? " (Target=$newInternal)" : ""));
}
break;
case 'env':
// value: VAR_NAME=VAR_VALUE
$eqPos = strpos($value, '=');
if ($eqPos === false) {
fail("Env value must be VAR_NAME=VAR_VALUE, got: $value");
}
$varName = substr($value, 0, $eqPos);
$varValue = substr($value, $eqPos + 1);
$found = false;
foreach ($dom->getElementsByTagName('Config') as $config) {
if (strcasecmp($config->getAttribute('Type'), 'Variable') === 0) {
if ($config->getAttribute('Target') === $varName) {
set_config_value($config, $varValue);
$found = true;
break;
}
}
}
if (!$found) {
fail("Env var $varName not found in template");
}
break;
case 'volume':
// value: hostPath:containerPath
$colonPos = strpos($value, ':');
if ($colonPos === false) {
fail("Volume value must be hostPath:containerPath, got: $value");
}
$hostPath = substr($value, 0, $colonPos);
$containerPath = substr($value, $colonPos + 1);
$found = false;
foreach ($dom->getElementsByTagName('Config') as $config) {
if (strcasecmp($config->getAttribute('Type'), 'Path') === 0) {
if ($config->getAttribute('Target') === $containerPath) {
set_config_value($config, $hostPath);
$found = true;
break;
}
}
}
if (!$found) {
fail("Volume target $containerPath not found in template");
}
break;
case 'network':
$networks = $dom->getElementsByTagName('Network');
if ($networks->length === 0) {
fail("No Network element in template");
}
$networks->item(0)->nodeValue = $value;
break;
case 'privileged':
$privs = $dom->getElementsByTagName('Privileged');
if ($privs->length === 0) {
fail("No Privileged element in template");
}
$privs->item(0)->nodeValue = $value;
break;
default:
fail("Unknown field: $field (use: port|env|volume|network|privileged)");
}
// Write modified template
$dom->save($tmpl);
out("Template modified: $name ($field = $value)");
// Rebuild container (force start so it's running after modify)
rebuild_container($name, false, true);
}
// ── Main ─────────────────────────────────────────────────────────────────
$argv = $_SERVER['argv'];
$action = $argv[1] ?? '';
$arg1 = $argv[2] ?? '';
$arg2 = $argv[3] ?? '';
$arg3 = $argv[4] ?? '';
switch ($action) {
case 'create':
if (!$arg1) fail("Missing template name");
out("Creating container from template: $arg1");
rebuild_container($arg1, true);
break;
case 'modify':
if (!$arg1 || !$arg2 || !$arg3) fail("Usage: modify <container> <field> <value>");
modify_template($arg1, $arg2, $arg3);
break;
case 'update':
if (!$arg1) fail("Missing container name");
out("Updating container: $arg1");
rebuild_container($arg1, true);
break;
case 'rebuild':
if (!$arg1) fail("Missing container name");
out("Rebuilding container: $arg1");
rebuild_container($arg1, false);
break;
default:
fail("Unknown action: $action. Usage: create|modify|update|rebuild");
}
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# MUA - Mikes Unraid Agent
# Update-Script
# Wird vom Unraid-Plugin-Installer bei Updates aufgerufen.
set -e
PLUGIN_DIR="/usr/local/plugins/mua"
SERVICE_FILE="$PLUGIN_DIR/scripts/mua.service"
HELPER_SRC="$PLUGIN_DIR/scripts/unraid-docker-mcp-helper.php"
HELPER_DST="/usr/local/bin/unraid-docker-mcp-helper.php"
echo "=== MUA Update ==="
# 1. PHP-Helper aktualisieren
if [ -f "$HELPER_SRC" ]; then
cp "$HELPER_SRC" "$HELPER_DST"
chmod 755 "$HELPER_DST"
echo " Helper aktualisiert"
fi
# 2. systemd-Service aktualisieren
if [ -f "$SERVICE_FILE" ]; then
cp "$SERVICE_FILE" /etc/systemd/system/mua.service
chmod 644 /etc/systemd/system/mua.service
systemctl daemon-reload
echo " Service aktualisiert"
fi
# 3. Service neu starten
systemctl restart mua.service
# 4. Health-Check
sleep 2
if curl -sf "http://127.0.0.1:3002/health" > /dev/null 2>&1; then
echo " Health-Check: OK"
echo "=== MUA aktualisiert ==="
else
echo " WARNUNG: Health-Check fehlgeschlagen"
systemctl status mua.service --no-pager || true
exit 1
fi