/** * MUA — Mikes Unraid Agent * tools.ts — MCP Tool-Definitionen (22 Tools) * * Portiert von mcp/tools.php. Schema: unraid__ * Kategorien: docker (14), network (6), system (2). */ import { containerRuntimeSummary, compactContainerInspect, dockerExec, sanitizeLogOutput, analyzeContainerLogs, runPhpHelper, compactNetworkInventory, hostState, auditAllTcpEndpoints, probeDualstack, connectionTest, validateName, runShell, } from "./helpers"; export interface ToolDef { name: string; description: string; inputSchema: object; handler: (args: Record) => Promise; } const str = (desc: string) => ({ type: "string", description: desc }); const int = (desc: string) => ({ type: "integer", description: desc }); const num = (desc: string) => ({ type: "number", description: desc }); const bool = (desc: string) => ({ type: "boolean", description: desc }); const empty = { type: "object", properties: {}, additionalProperties: false } as const; export const TOOLS: ToolDef[] = [ // ── 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: empty, handler: () => containerRuntimeSummary(), }, { name: "unraid_docker_inspect", description: "Inspect a single Docker container in detail (state, image, ports, env, mounts).", inputSchema: { type: "object", properties: { container: str("Container name or ID") }, required: ["container"], }, handler: (a) => compactContainerInspect(validateName(a["container"], "container")), }, { name: "unraid_docker_logs", description: "Get recent logs from a Docker container (with timestamps).", inputSchema: { type: "object", properties: { container: str("Container name or ID"), tail: int("Number of lines (1-2000, default 200)"), }, required: ["container"], }, handler: async (a) => { const container = validateName(a["container"], "container"); const tail = Number(a["tail"] ?? 200); if (tail < 1 || tail > 2000) throw new Error("tail must be between 1 and 2000"); const raw = await dockerExec(`logs --timestamps --tail ${tail} ${container}`, 60); return sanitizeLogOutput(raw); }, }, { name: "unraid_docker_analyze_logs", description: "Analyze container logs server-side for errors/warnings. Returns pattern counts and sample matches.", inputSchema: { type: "object", properties: { severity: { type: "string", enum: ["error", "warn", "info"], description: "Log severity to scan for", }, container: str("Container name (optional, scans all running containers if omitted)"), since: str("Time filter (default 24h)"), scan_tail: int("Max lines to scan (default 1000)"), max_results: int("Max sample matches (default 50)"), }, required: ["severity"], }, handler: (a) => { const severity = (a["severity"] as string) ?? "error"; const container = a["container"] as string | null | undefined; if (container !== undefined && container !== null && typeof container !== "string") { throw new Error("container must be a string"); } const since = (a["since"] as string) ?? "24h"; const scanTail = Number(a["scan_tail"] ?? 1000); const maxResults = Number(a["max_results"] ?? 50); return analyzeContainerLogs(severity, container ?? null, since, scanTail, maxResults); }, }, { name: "unraid_docker_processes", description: "List processes running inside a Docker container (docker top).", inputSchema: { type: "object", properties: { container: str("Container name or ID") }, required: ["container"], }, handler: (a) => dockerExec( `top ${validateName(a["container"], "container")} -eo pid,ppid,user,stat,lstart,etime,args`, ), }, { name: "unraid_docker_stats", description: "Get live CPU/memory/network/block I/O stats for all running containers.", inputSchema: empty, handler: () => dockerExec(`stats --no-stream --format '{{json .}}'`), }, { name: "unraid_docker_info", description: "Get Docker daemon information (version, storage driver, container counts, etc.).", inputSchema: empty, handler: () => dockerExec(`info --format '{{json .}}'`), }, { name: "unraid_docker_start", description: "Start a Docker container.", inputSchema: { type: "object", properties: { container: str("Container name or ID") }, required: ["container"], }, handler: (a) => dockerExec(`start ${validateName(a["container"], "container")}`), }, { name: "unraid_docker_stop", description: "Stop a Docker container.", inputSchema: { type: "object", properties: { container: str("Container name or ID") }, required: ["container"], }, handler: (a) => dockerExec(`stop ${validateName(a["container"], "container")}`), }, { name: "unraid_docker_restart", description: "Restart a Docker container.", inputSchema: { type: "object", properties: { container: str("Container name or ID") }, required: ["container"], }, handler: (a) => dockerExec(`restart ${validateName(a["container"], "container")}`), }, { name: "unraid_docker_create", description: "Create a Docker container from a template (pulls image, creates container).", inputSchema: { type: "object", properties: { template_name: str('Template name (e.g. "linuxserver/sonarr")'), }, required: ["template_name"], }, handler: (a) => runPhpHelper("create", validateName(a["template_name"], "template_name")), }, { name: "unraid_docker_modify", description: "Modify a container template (port, env, volume, network, privileged) and rebuild.", inputSchema: { type: "object", properties: { container: str("Container/template name"), field: { type: "string", enum: ["port", "env", "volume", "network", "privileged"], }, value: str("New value (format depends on field)"), }, required: ["container", "field", "value"], }, handler: (a) => { const container = validateName(a["container"], "container"); const field = (a["field"] as string) ?? ""; const value = (a["value"] as string) ?? ""; if (!["port", "env", "volume", "network", "privileged"].includes(field)) { throw new Error("field must be one of: port, env, volume, network, privileged"); } return runPhpHelper("modify", container, field, value); }, }, { name: "unraid_docker_update", description: "Update a container (pull latest image, rebuild).", inputSchema: { type: "object", properties: { container: str("Container/template name") }, required: ["container"], }, handler: (a) => runPhpHelper("update", validateName(a["container"], "container")), }, { name: "unraid_docker_rebuild", description: "Rebuild a container from its template (without pulling new image).", inputSchema: { type: "object", properties: { container: str("Container/template name") }, required: ["container"], }, handler: (a) => runPhpHelper("rebuild", validateName(a["container"], "container")), }, // ── Netzwerk (6) ────────────────────────────────────────────────────── { name: "unraid_network_inventory", description: "Compact Docker network inventory (all networks with container counts).", inputSchema: empty, handler: () => compactNetworkInventory(), }, { name: "unraid_network_list", description: "List all Docker networks.", inputSchema: empty, handler: () => dockerExec(`network ls --no-trunc --format '{{json .}}'`), }, { name: "unraid_network_inspect", description: "Inspect a Docker network in detail.", inputSchema: { type: "object", properties: { network: str("Network name or ID") }, required: ["network"], }, handler: (a) => dockerExec(`network inspect -- ${validateName(a["network"], "network")}`), }, { name: "unraid_network_host_state", description: "Get host network state (IPv4/IPv6 addresses, routes, listening sockets).", inputSchema: empty, handler: () => hostState(), }, { name: "unraid_network_audit_tcp", description: "Audit all TCP endpoints: probe IPv4/IPv6 reachability for every published port. Returns classification (dualstack/ipv4-only/ipv6-only/unreachable).", inputSchema: { type: "object", properties: { timeout_seconds: num("Probe timeout (0.2-10, default 2)"), include_all_endpoints: bool("Include all endpoints (default false, only problems)"), }, }, handler: (a) => { const timeout = Number(a["timeout_seconds"] ?? 2); if (timeout < 0.2 || timeout > 10) { throw new Error("timeout_seconds must be between 0.2 and 10"); } const includeAll = Boolean(a["include_all_endpoints"] ?? false); return auditAllTcpEndpoints(timeout, includeAll); }, }, { name: "unraid_network_lan_probe", description: "Probe a specific host:port for IPv4 and IPv6 reachability (dualstack test).", inputSchema: { type: "object", properties: { host: str("Hostname or IP"), port: int("Port (1-65535)"), timeout_seconds: num("Timeout (0.2-10, default 3)"), }, required: ["host", "port"], }, handler: (a) => { const host = validateName(a["host"], "host"); const port = Number(a["port"] ?? 0); const timeout = Number(a["timeout_seconds"] ?? 3); if (port < 1 || port > 65535 || timeout < 0.2 || timeout > 10) { throw new Error("Invalid port or timeout"); } return probeDualstack(host, port, timeout); }, }, // ── System (2) ──────────────────────────────────────────────────────── { name: "unraid_system_connection_test", description: "Test connection to the Unraid host (hostname, kernel, Unraid version).", inputSchema: empty, handler: () => connectionTest(), }, { name: "unraid_system_shell", description: "Execute a shell command on the Unraid host (as root, via /bin/sh -c) and return exit code, stdout, and stderr. Use for direct terminal access: file inspection, system commands, package info, log reading, etc. Commands run with a timeout and output is size-limited.", inputSchema: { type: "object", properties: { command: str( "Shell command to execute on the Unraid host (run via /bin/sh -c)", ), timeout_seconds: int("Timeout in seconds (1-300, default 60)"), }, required: ["command"], }, handler: (a) => { const command = (a["command"] as string) ?? ""; if (command.trim() === "") throw new Error("command is required"); const timeout = Number(a["timeout_seconds"] ?? 60); if (timeout < 1 || timeout > 300) { throw new Error("timeout_seconds must be between 1 and 300"); } return runShell(command, timeout); }, }, ]; export function toolByName(name: string): ToolDef | undefined { return TOOLS.find((t) => t.name === name); }