diff --git a/README.md b/README.md index bb54e2f..62efeb9 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,10 @@ Oder manuell: ```bash # .txz von Gitea laden -curl -O http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.21.r014-x86_64-1.txz +curl -O http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.22.r015-x86_64-1.txz # Installieren -upgradepkg --install-new mua-2026.08.21.r014-x86_64-1.txz +upgradepkg --install-new mua-2026.08.22.r015-x86_64-1.txz ``` ### 2. Service starten @@ -94,7 +94,7 @@ Der Service startet automatisch bei jedem Boot (SysVinit). ```bash curl http://192.168.1.2:3002/health -# → {"status":"ok","version":"2026.08.21.r014","auth":"required"} +# → {"status":"ok","version":"2026.08.22.r015","auth":"required"} ``` --- diff --git a/dist/mua-2026.08.22.r015-x86_64-1.txz b/dist/mua-2026.08.22.r015-x86_64-1.txz new file mode 100644 index 0000000..bdab4c8 Binary files /dev/null and b/dist/mua-2026.08.22.r015-x86_64-1.txz differ diff --git a/mcp/helpers.php b/mcp/helpers.php index fff8b33..646d372 100644 --- a/mcp/helpers.php +++ b/mcp/helpers.php @@ -10,7 +10,7 @@ error_reporting(E_ALL); ini_set('display_errors', 0); // Fehler werden als JSON-RPC-Error zurückgegeben, nicht als HTML -const MUA_VERSION = '2026.08.18.r005'; +const MUA_VERSION = '2026.08.22.r015'; const MUA_SERVER_NAME = 'mua'; /** @@ -235,35 +235,59 @@ function compact_container_inspect(string $raw): string { /** * Container-Runtime-Zusammenfassung (docker ps + docker stats). */ -function container_runtime_summary(): string { +function container_runtime_summary(array $args = []): string { + $state_filter = $args['state'] ?? 'all'; + $include_stats = ($args['include_stats'] ?? false) === true; + $detail = $args['detail'] ?? 'compact'; + $allowed_states = ['all', 'running', 'stopped', 'created', 'exited']; + if (!in_array($state_filter, $allowed_states, true)) { + throw new InvalidArgumentException('state must be one of: ' . implode(', ', $allowed_states)); + } + if (!in_array($detail, ['compact', 'full'], true)) { + throw new InvalidArgumentException('detail must be compact or full'); + } $ps = docker_exec("ps -a --format '{{json .}}'"); $containers = json_lines($ps); // docker stats für laufende Container $running_ids = array_filter(array_map(function ($c) { - return $c['ID'] ?? ''; + return (($c['State'] ?? '') === 'running') ? ($c['ID'] ?? '') : ''; }, $containers)); $stats = []; - if (!empty($running_ids)) { + if ($include_stats && !empty($running_ids)) { $stats_raw = docker_exec("stats --no-stream --format '{{json .}}'"); foreach (json_lines($stats_raw) as $s) { $stats[$s['ID'] ?? ''] = $s; } } + $state_counts = []; + foreach ($containers as $c) { + $state = (string)($c['State'] ?? 'unknown'); + $state_counts[$state] = ($state_counts[$state] ?? 0) + 1; + } + $result = []; foreach ($containers as $c) { + $state = (string)($c['State'] ?? ''); + $matches = $state_filter === 'all' + || ($state_filter === 'stopped' && $state !== 'running') + || $state === $state_filter; + if (!$matches) continue; + $id = $c['ID'] ?? ''; $entry = [ - 'id' => substr($id, 0, 12), 'name' => $c['Names'] ?? '', - 'image' => $c['Image'] ?? '', 'status' => $c['Status'] ?? '', 'state' => $c['State'] ?? '', - 'ports' => $c['Ports'] ?? '', ]; - if (isset($stats[$id])) { + if ($detail === 'full') { + $entry['id'] = substr($id, 0, 12); + $entry['image'] = $c['Image'] ?? ''; + $entry['ports'] = $c['Ports'] ?? ''; + } + if ($include_stats && isset($stats[$id])) { $entry['cpu_percent'] = $stats[$id]['CPUPerc'] ?? ''; $entry['mem_usage'] = $stats[$id]['MemUsage'] ?? ''; $entry['mem_percent'] = $stats[$id]['MemPerc'] ?? ''; @@ -274,8 +298,14 @@ function container_runtime_summary(): string { } return json_encode([ - 'schema_version' => '1.0', + 'schema_version' => '1.1', 'container_count' => count($result), + 'returned_count' => count($result), + 'total_count' => count($containers), + 'running_count' => $state_counts['running'] ?? 0, + 'stopped_count' => count($containers) - ($state_counts['running'] ?? 0), + 'state_counts' => $state_counts, + 'filter' => ['state' => $state_filter, 'include_stats' => $include_stats, 'detail' => $detail], 'containers' => $result, ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } diff --git a/mcp/server.php b/mcp/server.php index 8f395f1..f4a93ac 100644 --- a/mcp/server.php +++ b/mcp/server.php @@ -21,7 +21,7 @@ const MUA_PORT = 3002; const MUA_BIND = '0.0.0.0'; const MUA_SESSION_TTL = 3600; // Sekunden const MUA_SERVER_NAME = 'mua'; -const MUA_VERSION = '2026.08.18.r005'; +const MUA_VERSION = '2026.08.22.r015'; // ── Session-Management (in-memory) ─────────────────────────────────────── $sessions = []; // session_id => ['created' => time, 'initialized' => bool] @@ -91,7 +91,7 @@ function call_tool(string $name, array $args): string { switch ($name) { // ── Docker ────────────────────────────────────────────────────── case 'unraid_docker_list': - return container_runtime_summary(); + return container_runtime_summary($args); case 'unraid_docker_inspect': $container = validate_name($args['container'] ?? null, 'container'); diff --git a/mcp/tools.php b/mcp/tools.php index f3979f2..6314206 100644 --- a/mcp/tools.php +++ b/mcp/tools.php @@ -14,8 +14,16 @@ function mua_tools(): array { // ── Docker (14) ────────────────────────────────────────────────── [ 'name' => 'unraid_docker_list', - 'description' => 'List all Docker containers with runtime stats (CPU, memory, network I/O). Returns a compact JSON summary.', - 'inputSchema' => ['type' => 'object', 'properties' => new stdClass(), 'additionalProperties' => false], + 'description' => "List Docker containers with authoritative server-side counts. For active containers always use state='running'. Stats are optional to keep output compact.", + 'inputSchema' => [ + 'type' => 'object', + 'properties' => [ + 'state' => ['type' => 'string', 'enum' => ['all', 'running', 'stopped', 'created', 'exited'], 'description' => "Use 'running' for active containers. Default: all."], + 'include_stats' => ['type' => 'boolean', 'description' => 'Include live CPU, RAM and I/O statistics. Default: false.'], + 'detail' => ['type' => 'string', 'enum' => ['compact', 'full'], 'description' => 'compact returns name/status/state; full also returns ID/image/ports.'], + ], + 'additionalProperties' => false, + ], ], [ 'name' => 'unraid_docker_inspect', diff --git a/package.json b/package.json index 1e0b930..ecfb57f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mua", - "version": "2026.08.21.r014", + "version": "2026.08.22.r015", "description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid", "type": "module", "main": "src/index.ts", diff --git a/plugin/mua.plg b/plugin/mua.plg index a0f7704..ed0ee1d 100644 --- a/plugin/mua.plg +++ b/plugin/mua.plg @@ -2,13 +2,13 @@ - + - - + + ]> +### 2026.08.22.r015 +- Docker-Liste kann serverseitig nach running/stopped/created/exited filtern und liefert verbindliche Zustandszähler. +- Kompakte Antworten ohne Live-Metriken sind nun Standard; CPU/RAM/I/O und Detailfelder werden nur bei Bedarf erzeugt. +- Die Werkzeugbeschreibung weist kleine Modelle ausdrücklich auf state=running für aktive Container hin. ### 2026.08.21.r014 - CA-Template-Normalisierung: Fehlende Mode-Attribute werden ergänzt, damit Unraids Docker-Manager Community-Apps ohne PHP-Warnungen einliest. ### 2026.08.21.r013 @@ -111,7 +115,7 @@ Das .txz enthält: install/doinst.sh (läuft nach Installation) =========================================== --> - + &txzURL; &txzSHA256; diff --git a/plugin/plugin.json b/plugin/plugin.json index a20cc33..8a462d5 100644 --- a/plugin/plugin.json +++ b/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "mua", "author": "Michael", - "version": "2026.08.21.r014", + "version": "2026.08.22.r015", "minver": "7.0.0", "pluginDirectory": "/usr/local/emhttp/plugins/mua", "configDirectory": "/boot/config/plugins/mua", diff --git a/src/helpers.ts b/src/helpers.ts index abaf14c..1fd13b8 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -14,7 +14,7 @@ import { createHash, randomUUID } from "node:crypto"; // ── Konstanten ────────────────────────────────────────────────────────── export const MUA_SERVER_NAME = "mua"; -export const MUA_VERSION = "2026.08.21.r014"; +export const MUA_VERSION = "2026.08.22.r015"; export const MUA_PROTOCOL_VERSION = "2025-03-26"; export const PHP_HELPER = "/usr/local/bin/unraid-docker-mcp-helper.php"; export const STATUS_HELPER = "/usr/local/bin/unraid-mcp-status-helper.php"; @@ -428,33 +428,67 @@ export async function compactContainerInspect(container: string): Promise { +export interface ContainerListOptions { + state?: "all" | "running" | "stopped" | "created" | "exited"; + includeStats?: boolean; + detail?: "compact" | "full"; +} + +export async function containerRuntimeSummary( + options: ContainerListOptions = {}, +): Promise { + const stateFilter = options.state ?? "all"; + const includeStats = options.includeStats ?? false; + const detail = options.detail ?? "compact"; + const allowedStates = new Set(["all", "running", "stopped", "created", "exited"]); + if (!allowedStates.has(stateFilter)) { + throw new Error("state must be one of: all, running, stopped, created, exited"); + } + if (detail !== "compact" && detail !== "full") { + throw new Error("detail must be compact or full"); + } const ps = await dockerExec(`ps -a --format '{{json .}}'`); const containers = jsonLines(ps); const runningIds = containers + .filter((c) => c["State"] === "running") .map((c) => (c["ID"] as string) ?? "") .filter((id) => id !== ""); const stats: Record> = {}; - if (runningIds.length > 0) { + if (includeStats && runningIds.length > 0) { const statsRaw = await dockerExec(`stats --no-stream --format '{{json .}}'`); for (const s of jsonLines(statsRaw)) { stats[(s["ID"] as string) ?? ""] = s; } } - const result = containers.map((c) => { + const stateCounts: Record = {}; + for (const c of containers) { + const state = String(c["State"] ?? "unknown"); + stateCounts[state] = (stateCounts[state] ?? 0) + 1; + } + + const matchesState = (c: Record) => { + const state = String(c["State"] ?? ""); + if (stateFilter === "all") return true; + if (stateFilter === "stopped") return state !== "running"; + return state === stateFilter; + }; + + const result = containers.filter(matchesState).map((c) => { const id = (c["ID"] as string) ?? ""; const entry: Record = { - id: id.slice(0, 12), name: c["Names"] ?? "", - image: c["Image"] ?? "", status: c["Status"] ?? "", state: c["State"] ?? "", - ports: c["Ports"] ?? "", }; - if (stats[id]) { + if (detail === "full") { + entry.id = id.slice(0, 12); + entry.image = c["Image"] ?? ""; + entry.ports = c["Ports"] ?? ""; + } + if (includeStats && stats[id]) { entry.cpu_percent = stats[id]["CPUPerc"] ?? ""; entry.mem_usage = stats[id]["MemUsage"] ?? ""; entry.mem_percent = stats[id]["MemPerc"] ?? ""; @@ -465,8 +499,14 @@ export async function containerRuntimeSummary(): Promise { }); return JSON.stringify({ - schema_version: "1.0", + schema_version: "1.1", container_count: result.length, + returned_count: result.length, + total_count: containers.length, + running_count: stateCounts["running"] ?? 0, + stopped_count: containers.length - (stateCounts["running"] ?? 0), + state_counts: stateCounts, + filter: { state: stateFilter, include_stats: includeStats, detail }, containers: result, }); } diff --git a/src/tools.ts b/src/tools.ts index d84cb85..fcfe78a 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -74,9 +74,29 @@ export const TOOLS: ToolDef[] = [ { name: "unraid_docker_list", description: - "List all Docker containers with runtime stats (CPU, memory, network I/O). Returns a compact JSON summary.", - inputSchema: empty, - handler: () => containerRuntimeSummary(), + "List Docker containers and return authoritative server-side counts. For questions about active/currently running containers, ALWAYS use state='running'. Defaults to a compact response without performance statistics to avoid large tool output. Use include_stats=true only when CPU, RAM or I/O values are explicitly needed.", + inputSchema: { + type: "object", + properties: { + state: { + type: "string", + enum: ["all", "running", "stopped", "created", "exited"], + description: "Filter by container state. Use 'running' for active/running containers; 'stopped' means every non-running container. Default: all.", + }, + include_stats: bool("Include live CPU, RAM, network and block-I/O statistics. Expensive and verbose; default false."), + detail: { + type: "string", + enum: ["compact", "full"], + description: "compact returns name/status/state; full additionally returns ID, image and ports. Default: compact.", + }, + }, + additionalProperties: false, + }, + handler: (a) => containerRuntimeSummary({ + state: (a["state"] as "all" | "running" | "stopped" | "created" | "exited" | undefined), + includeStats: a["include_stats"] === true, + detail: (a["detail"] as "compact" | "full" | undefined), + }), }, { name: "unraid_docker_inspect",