Initial commit: forge-tools-ssh — MCP-сервер для администрирования по SSH

This commit is contained in:
Maksim Totmin
2026-10-01 10:13:48 +07:00
commit 1821fe7968
30 changed files with 5342 additions and 0 deletions
+316
View File
@@ -0,0 +1,316 @@
package tools
import (
"context"
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func registerMonitoringTools(s *mcp.Server) {
// usage
s.AddTool(&mcp.Tool{
Name: "usage",
Description: "Get CPU/RAM/Disk usage summary",
InputSchema: schema(map[string]any{
"target": strProps("Connection alias (default: primary)", false),
}, nil),
}, usageHandler)
// ps
s.AddTool(&mcp.Tool{
Name: "ps",
Description: "List top processes sorted by CPU or memory",
InputSchema: schema(map[string]any{
"sort_by": strProps("Sort field (default: cpu)", false, "cpu", "mem"),
"limit": intProps("Number of processes to show (default: 10)", false),
"target": strProps("Connection alias (default: primary)", false),
}, nil),
}, psHandler)
// logs
s.AddTool(&mcp.Tool{
Name: "logs",
Description: "Read the tail of a log file",
InputSchema: schema(map[string]any{
"path": strProps("Path to log file", true),
"lines": intProps("Number of lines to read (default: 50, max: 500)", false),
"grep": strProps("Optional filter pattern", false),
"target": strProps("Connection alias (default: primary)", false),
}, []string{"path"}),
}, logsHandler)
// journal_read
s.AddTool(&mcp.Tool{
Name: "journal_read",
Description: "Read system logs (journalctl/syslog)",
InputSchema: schema(map[string]any{
"service": strProps("Service name to filter (e.g., nginx, sshd)", false),
"since": strProps("Time filter (e.g., '1 hour ago')", false),
"lines": intProps("Number of lines (default: 100, max: 500)", false),
"priority": strProps("Minimum log priority level", false, "emerg", "alert", "crit", "err", "warning", "notice", "info", "debug"),
"target": strProps("Connection alias (default: primary)", false),
}, nil),
}, journalReadHandler)
// dmesg_read
s.AddTool(&mcp.Tool{
Name: "dmesg_read",
Description: "Read kernel ring buffer (dmesg)",
InputSchema: schema(map[string]any{
"grep": strProps("Optional pattern to filter messages", false),
"lines": intProps("Number of lines (default: 100)", false),
"target": strProps("Connection alias (default: primary)", false),
}, nil),
}, dmesgReadHandler)
// diagnose_system
s.AddTool(&mcp.Tool{
Name: "diagnose_system",
Description: "One-click SRE health check: load, OOM, disk, failed services",
InputSchema: schema(map[string]any{
"target": strProps("Connection alias (default: primary)", false),
}, nil),
}, diagnoseHandler)
// list_services
s.AddTool(&mcp.Tool{
Name: "list_services",
Description: "List system services (systemd/OpenRC)",
InputSchema: schema(map[string]any{
"failed_only": boolProps("Show only failed services", false),
"target": strProps("Connection alias (default: primary)", false),
}, nil),
}, listServicesHandler)
}
func usageHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
m := manager()
if m == nil {
return errorResult("no SSH manager initialized"), nil
}
target := getString(requestArgs(req), "target", "primary")
cmd := `
echo "=== LOAD AVERAGE ==="
uptime 2>/dev/null
echo ""
echo "=== MEMORY ==="
free -h 2>/dev/null || top -l 1 -s 0 2>/dev/null | grep -i phys || cat /proc/meminfo 2>/dev/null | head -5
echo ""
echo "=== DISK (all mounts) ==="
df -hP 2>/dev/null
`
output, err := m.Execute(ctx, cmd, target)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(output), nil
}
func psHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
m := manager()
if m == nil {
return errorResult("no SSH manager initialized"), nil
}
args := requestArgs(req)
sortBy := getString(args, "sort_by", "cpu")
limit := getInt(args, "limit", 10)
target := getString(args, "target", "primary")
if limit > 50 {
limit = 50
}
sortCol := "3"
if sortBy == "mem" {
sortCol = "4"
}
cmd := fmt.Sprintf("ps -eo pid,user,%%cpu,%%mem,comm | awk 'NR==1{print} NR>1{print | \"sort -k%s -rn\"}' | head -n %d", sortCol, limit+1)
output, err := m.Execute(ctx, cmd, target)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(output), nil
}
func logsHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
m := manager()
if m == nil {
return errorResult("no SSH manager initialized"), nil
}
args := requestArgs(req)
path, _ := requireString(args, "path")
lines := getInt(args, "lines", 50)
grep := getString(args, "grep", "")
target := getString(args, "target", "primary")
if lines > 500 {
lines = 500
}
cmd := fmt.Sprintf("tail -n %d %s", lines, shellQuote(path))
if grep != "" {
cmd += fmt.Sprintf(" | grep %s", shellQuote(grep))
}
output, err := m.Execute(ctx, cmd, target)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(output), nil
}
func journalReadHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
m := manager()
if m == nil {
return errorResult("no SSH manager initialized"), nil
}
args := requestArgs(req)
service := getString(args, "service", "")
since := getString(args, "since", "")
lines := getInt(args, "lines", 100)
priority := getString(args, "priority", "")
target := getString(args, "target", "primary")
if lines > 500 {
lines = 500
}
checkCmd := "command -v journalctl >/dev/null 2>&1 && echo 'systemd' || echo 'syslog'"
checkOutput, err := m.Execute(ctx, checkCmd, target)
if err != nil {
return errorResult(err.Error()), nil
}
var cmd string
if containsString(checkOutput, "systemd") {
cmd = "journalctl --no-pager"
if service != "" {
cmd += fmt.Sprintf(" -u %s", shellQuote(service))
}
if since != "" {
cmd += fmt.Sprintf(" --since %s", shellQuote(since))
}
if priority != "" {
cmd += fmt.Sprintf(" -p %s", shellQuote(priority))
}
cmd += fmt.Sprintf(" -n %d 2>/dev/null", lines)
} else {
cmd = fmt.Sprintf("cat /var/log/syslog /var/log/messages /var/log/system.log 2>/dev/null | tail -n %d", lines)
if service != "" {
cmd += fmt.Sprintf(" | grep -i %s", shellQuote(service))
}
}
output, err := m.Execute(ctx, cmd, target)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(output), nil
}
func dmesgReadHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
m := manager()
if m == nil {
return errorResult("no SSH manager initialized"), nil
}
args := requestArgs(req)
grep := getString(args, "grep", "")
lines := getInt(args, "lines", 100)
target := getString(args, "target", "primary")
if lines > 500 {
lines = 500
}
cmd := "dmesg --time-format iso 2>/dev/null || dmesg 2>/dev/null"
if grep != "" {
cmd += fmt.Sprintf(" | grep -i %s", shellQuote(grep))
}
cmd += fmt.Sprintf(" | tail -n %d", lines)
output, err := m.Execute(ctx, cmd, target)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(output), nil
}
func diagnoseHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
m := manager()
if m == nil {
return errorResult("no SSH manager initialized"), nil
}
target := getString(requestArgs(req), "target", "primary")
cmd := `
echo "=== SYSTEM HEALTH DIAGNOSTIC ==="
echo ""
echo "--- LOAD AVERAGE ---"
LOAD=$(uptime 2>/dev/null | awk -F'load average[s]?: ' '{print $2}' | awk -F'[, ]' '{print $1}')
CPUS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)
echo "Load (1min): $LOAD (CPUs: $CPUS)"
if echo "$LOAD $CPUS" | awk '{if ($1 > $2 * 2) exit 0; else exit 1}'; then
echo "WARNING: High load detected!"
fi
echo ""
echo "--- TOP CPU CONSUMERS ---"
ps -eo pid,user,%cpu,%mem,comm | awk 'NR==1{print} NR>1{print | "sort -k3 -rn"}' | head -n 6
echo ""
echo "--- OOM EVENTS ---"
OOM=$(dmesg 2>/dev/null | grep -i 'out of memory' | tail -n 3)
if [ -n "$OOM" ]; then
echo "$OOM"
echo "WARNING: OOM events found!"
else
echo "No OOM events in dmesg"
fi
echo ""
echo "--- DISK PRESSURE (>=80%) ---"
df -hP 2>/dev/null | awk 'NR==1{print; next} {c=$5; sub(/%/,"",c); m=(c+0>=80 ? " <<ALERT" : ""); print $0 m}'
echo ""
echo "--- FAILED SERVICES ---"
if command -v systemctl >/dev/null 2>&1; then
FAILED=$(systemctl --failed --no-legend --no-pager 2>/dev/null | head -n 5)
if [ -n "$FAILED" ]; then
echo "$FAILED"
else
echo "No failed services"
fi
elif command -v rc-status >/dev/null 2>&1; then
rc-status --crashed 2>/dev/null | head -n 5 || echo "No crashed services"
else
echo "Init system not detected"
fi
echo ""
echo "=== END DIAGNOSTIC ==="
`
output, err := m.Execute(ctx, cmd, target)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(output), nil
}
func listServicesHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
m := manager()
if m == nil {
return errorResult("no SSH manager initialized"), nil
}
args := requestArgs(req)
failedOnly := getBool(args, "failed_only", false)
target := getString(args, "target", "primary")
var cmd string
if failedOnly {
cmd = `if command -v systemctl >/dev/null 2>&1; then systemctl --failed --no-pager 2>/dev/null; elif command -v rc-status >/dev/null 2>&1; then rc-status --crashed 2>/dev/null; elif command -v launchctl >/dev/null 2>&1; then launchctl list 2>/dev/null | head -50; else echo "No supported init system detected"; fi`
} else {
cmd = `if command -v systemctl >/dev/null 2>&1; then systemctl list-units --type=service --no-pager 2>/dev/null | head -50; elif command -v rc-status >/dev/null 2>&1; then rc-status 2>/dev/null; elif command -v launchctl >/dev/null 2>&1; then launchctl list 2>/dev/null | head -50; elif [ -x /usr/sbin/service ]; then service -e 2>/dev/null | head -50; else echo "No supported init system detected"; fi`
}
output, err := m.Execute(ctx, cmd, target)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(output), nil
}