generated from fahricansecer/boilerplate-fe
v1
This commit is contained in:
134
mcp-servers/rpi-deploy-mcp-server/src/tools/system.ts
Normal file
134
mcp-servers/rpi-deploy-mcp-server/src/tools/system.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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)}` }] };
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user