From 38463190eedbef666cb7805a9df273d03568bfa0 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:45:22 +0200 Subject: [PATCH] r006: Add unraid_system_shell tool (direct terminal access) - New helper runShell(): executes /bin/sh -c, returns {exit_code, stdout, stderr} as JSON - New tool unraid_system_shell: direct shell access on Unraid host (root) - params: command (required), timeout_seconds (1-300, default 60) - output size-limited via sanitizeLogOutput - Version bump r005 -> r006 --- package.json | 2 +- src/helpers.ts | 30 +++++++++++++++++++++++++++++- src/tools.ts | 31 ++++++++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d2ccdb2..a55bdf7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mua", - "version": "2026.08.18.r005", + "version": "2026.08.18.r006", "description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid", "type": "module", "main": "src/index.ts", diff --git a/src/helpers.ts b/src/helpers.ts index bf28542..5711687 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -13,7 +13,7 @@ import { createConnection, type Socket } from "net"; // ── Konstanten ────────────────────────────────────────────────────────── export const MUA_SERVER_NAME = "mua"; -export const MUA_VERSION = "2026.08.18.r005"; +export const MUA_VERSION = "2026.08.18.r006"; export const MUA_PROTOCOL_VERSION = "2025-03-26"; export const PHP_HELPER = "/usr/local/bin/unraid-docker-mcp-helper.php"; @@ -53,6 +53,34 @@ export async function runLocal( } } +/** + * Shell-Befehl ausführen und strukturiertes Ergebnis liefern + * (exit_code + stdout + stderr). Für das unraid_system_shell-Tool. + * Liefert JSON, damit der Agent Exit-Code und beides Streams sieht. + */ +export async function runShell(cmd: string, timeoutSec = 60): Promise { + try { + const proc = spawn(["/bin/sh", "-c", cmd], { + stdout: "pipe", + stderr: "pipe", + }); + const timeout = setTimeout(() => proc.kill(), timeoutSec * 1000); + const [stdout, stderr, code] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + clearTimeout(timeout); + return JSON.stringify({ + exit_code: code, + stdout: sanitizeLogOutput(stdout.trim(), 100000), + stderr: sanitizeLogOutput(stderr.trim(), 20000), + }); + } catch (e) { + throw new Error(`run_shell failed: ${String(e)}`); + } +} + /** * Docker-Befehl ausführen (kompakt). */ diff --git a/src/tools.ts b/src/tools.ts index 5d17401..62256dd 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -1,9 +1,9 @@ /** * MUA — Mikes Unraid Agent - * tools.ts — MCP Tool-Definitionen (21 Tools) + * tools.ts — MCP Tool-Definitionen (22 Tools) * * Portiert von mcp/tools.php. Schema: unraid__ - * Kategorien: docker (14), network (6), system (1). + * Kategorien: docker (14), network (6), system (2). */ import { @@ -19,6 +19,7 @@ import { probeDualstack, connectionTest, validateName, + runShell, } from "./helpers"; export interface ToolDef { @@ -294,7 +295,7 @@ export const TOOLS: ToolDef[] = [ }, }, - // ── System (1) ──────────────────────────────────────────────────────── + // ── System (2) ──────────────────────────────────────────────────────── { name: "unraid_system_connection_test", description: @@ -302,6 +303,30 @@ export const TOOLS: ToolDef[] = [ 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 {