Refactor: PHP-Built-in-Server durch TypeScript-Binary (Bun Runtime) ersetzt
- src/*.ts: Port von mcp/*.php (helpers, tools, server) - Bun.serve() statt php -S — production-grade HTTP, POST funktioniert - dist/mua-1.0.0-x86_64-1.txz: Slackware-Paket (Binary + rc.mua + mua.page + PHP-Helper) - plugin/mua.plg: .txz-Installation via upgradepkg - scripts/rc.mua: Binary statt php -S - scripts/mua.page: Binary-Info statt PHP-Version - scripts/package.sh: .txz-Builder Fix: MCP-POST-Endpunkt lieferte leeren Body (php -S Limitation). Bun.serve() löst das Problem.
This commit is contained in:
+309
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* MUA — Mikes Unraid Agent
|
||||
* tools.ts — MCP Tool-Definitionen (21 Tools)
|
||||
*
|
||||
* Portiert von mcp/tools.php. Schema: unraid_<kategorie>_<aktion>
|
||||
* Kategorien: docker (14), network (6), system (1).
|
||||
*/
|
||||
|
||||
import {
|
||||
containerRuntimeSummary,
|
||||
compactContainerInspect,
|
||||
dockerExec,
|
||||
sanitizeLogOutput,
|
||||
analyzeContainerLogs,
|
||||
runPhpHelper,
|
||||
compactNetworkInventory,
|
||||
hostState,
|
||||
auditAllTcpEndpoints,
|
||||
probeDualstack,
|
||||
connectionTest,
|
||||
validateName,
|
||||
} from "./helpers";
|
||||
|
||||
export interface ToolDef {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: object;
|
||||
handler: (args: Record<string, unknown>) => Promise<string>;
|
||||
}
|
||||
|
||||
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 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 (1) ────────────────────────────────────────────────────────
|
||||
{
|
||||
name: "unraid_system_connection_test",
|
||||
description:
|
||||
"Test connection to the Unraid host (hostname, kernel, Unraid version).",
|
||||
inputSchema: empty,
|
||||
handler: () => connectionTest(),
|
||||
},
|
||||
];
|
||||
|
||||
export function toolByName(name: string): ToolDef | undefined {
|
||||
return TOOLS.find((t) => t.name === name);
|
||||
}
|
||||
Reference in New Issue
Block a user