import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { executeCommand } from '../ssh-client.js'; import { SAFE_COMMAND_PREFIXES } from '../constants.js'; export function registerSystemTools(server: McpServer): void { // ── rpi_system_status ──────────────────────────────────────────────── server.registerTool( 'rpi_system_status', { title: 'Raspberry Pi System Status', description: `Get system resource usage on the Raspberry Pi: CPU load, memory, disk, uptime, and temperature. Returns a formatted overview of the system health.`, inputSchema: {}, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async () => { try { const commands = [ { label: 'Hostname', cmd: 'hostname' }, { label: 'Uptime', cmd: 'uptime' }, { label: 'CPU Temperature', cmd: 'cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo "N/A"' }, { label: 'Memory', cmd: 'free -h | head -3' }, { label: 'Disk', cmd: 'df -h / | tail -1' }, { label: 'CPU Load', cmd: 'top -bn1 | head -5' }, ]; const sections: string[] = ['# 🖥️ Raspberry Pi System Status\n']; for (const { label, cmd } of commands) { const result = await executeCommand(cmd); let value = result.stdout || 'N/A'; // Convert CPU temp from millidegrees if (label === 'CPU Temperature' && value !== 'N/A') { const temp = parseInt(value); if (!isNaN(temp)) { value = `${(temp / 1000).toFixed(1)}°C`; } } sections.push(`## ${label}\n\`\`\`\n${value}\n\`\`\`\n`); } return { content: [{ type: 'text', text: sections.join('\n') }] }; } catch (error) { return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); // ── rpi_docker_networks ────────────────────────────────────────────── server.registerTool( 'rpi_docker_networks', { title: 'List Docker Networks', description: `List all Docker networks on the Raspberry Pi with driver and scope info.`, inputSchema: {}, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async () => { try { const result = await executeCommand('docker network ls --format "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.Scope}}"'); if (result.code !== 0) { return { content: [{ type: 'text', text: `Error: ${result.stderr}` }] }; } return { content: [{ type: 'text', text: `## Docker Networks\n\n\`\`\`\n${result.stdout}\n\`\`\`` }] }; } catch (error) { return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); // ── rpi_run_command ────────────────────────────────────────────────── server.registerTool( 'rpi_run_command', { title: 'Run Safe Command on RPi', description: `Execute a read-only command on the Raspberry Pi via SSH. Only whitelisted safe commands are allowed (docker ps, docker logs, df, free, uptime, cat, ls, tail, head, grep, etc.). Args: - command (string): The command to execute (must start with a safe prefix)`, inputSchema: { command: z.string().min(1).max(500).describe('Safe read-only command to execute'), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async ({ command }) => { try { const trimmedCmd = command.trim(); const isSafe = SAFE_COMMAND_PREFIXES.some(prefix => trimmedCmd.startsWith(prefix)); if (!isSafe) { return { content: [{ type: 'text', text: `❌ Command rejected: '${trimmedCmd}' is not in the safe command list.\n\nAllowed prefixes:\n${SAFE_COMMAND_PREFIXES.map(p => ` - ${p}`).join('\n')}` }] }; } const result = await executeCommand(trimmedCmd); const output = result.stdout || result.stderr || '(no output)'; const status = result.code === 0 ? '✅' : `⚠️ (exit code: ${result.code})`; return { content: [{ type: 'text', text: `${status} \`${trimmedCmd}\`\n\n\`\`\`\n${output}\n\`\`\`` }] }; } catch (error) { return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); }