317 lines
9.8 KiB
Go
317 lines
9.8 KiB
Go
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
|
|
}
|