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
This commit is contained in:
Mikei386
2026-08-18 20:45:22 +02:00
parent c29fc00886
commit 38463190ee
3 changed files with 58 additions and 5 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mua", "name": "mua",
"version": "2026.08.18.r005", "version": "2026.08.18.r006",
"description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid", "description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+29 -1
View File
@@ -13,7 +13,7 @@ import { createConnection, type Socket } from "net";
// ── Konstanten ────────────────────────────────────────────────────────── // ── Konstanten ──────────────────────────────────────────────────────────
export const MUA_SERVER_NAME = "mua"; 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 MUA_PROTOCOL_VERSION = "2025-03-26";
export const PHP_HELPER = "/usr/local/bin/unraid-docker-mcp-helper.php"; 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<string> {
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). * Docker-Befehl ausführen (kompakt).
*/ */
+28 -3
View File
@@ -1,9 +1,9 @@
/** /**
* MUA — Mikes Unraid Agent * 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_<kategorie>_<aktion> * Portiert von mcp/tools.php. Schema: unraid_<kategorie>_<aktion>
* Kategorien: docker (14), network (6), system (1). * Kategorien: docker (14), network (6), system (2).
*/ */
import { import {
@@ -19,6 +19,7 @@ import {
probeDualstack, probeDualstack,
connectionTest, connectionTest,
validateName, validateName,
runShell,
} from "./helpers"; } from "./helpers";
export interface ToolDef { export interface ToolDef {
@@ -294,7 +295,7 @@ export const TOOLS: ToolDef[] = [
}, },
}, },
// ── System (1) ──────────────────────────────────────────────────────── // ── System (2) ────────────────────────────────────────────────────────
{ {
name: "unraid_system_connection_test", name: "unraid_system_connection_test",
description: description:
@@ -302,6 +303,30 @@ export const TOOLS: ToolDef[] = [
inputSchema: empty, inputSchema: empty,
handler: () => connectionTest(), 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 { export function toolByName(name: string): ToolDef | undefined {