Files

96 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tools
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// registerDiskTools добавляет точные инструменты диска: по конкретному пути
// (безпотерьный ответ "сколько места на X") и по всем точкам монтирования.
// Отличается от ssh__usage, который исторически отдавал только корень /.
func registerDiskTools(s *mcp.Server) {
s.AddTool(&mcp.Tool{
Name: "disk_usage",
Description: "Get free/used space for the filesystem that contains the given path (df equivalent). Resolves the MOUND, not just root: returns the mount point, device, size, used, available and use% for the volume holding <path>. Use for 'how much space/free on <path>'.",
InputSchema: schema(map[string]any{
"path": strProps("Absolute path; reports the filesystem that contains it", true),
"target": strProps("Connection alias (default: primary)", false),
}, []string{"path"}),
}, diskUsageHandler)
s.AddTool(&mcp.Tool{
Name: "disk_usage_all",
Description: "Get free/used space for ALL mounted filesystems (df over every mount point) with a marker on partitions that are >=80% used. Use for an overview of disk pressure.",
InputSchema: schema(map[string]any{
"target": strProps("Connection alias (default: primary)", false),
}, nil),
}, diskUsageAllHandler)
}
func diskUsageHandler(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, err := requireString(args, "path")
if err != nil {
return errorResult(err.Error()), nil
}
target := getString(args, "target", "primary")
// df -hP <path> reports the FS containing <path> (mount point), not just
// root. shellQuote guards the path; if the path doesn't exist yet (e.g., a
// not-yet-mounted volume) we degrade to the full mount list rather than fail.
cmd := diskUsageCmd(path)
output, err := m.Execute(ctx, cmd, target)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(output), nil
}
func diskUsageAllHandler(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")
output, err := m.Execute(ctx, "df -hP 2>/dev/null", target)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(highlightHighUsage(output)), nil
}
// diskUsageCmd собирает команду "место на томе, содержащем path".
func diskUsageCmd(path string) string {
return fmt.Sprintf("df -hP %s 2>/dev/null || df -hP 2>/dev/null", shellQuote(path))
}
// highlightHighUsage помечает строки df с заполненностью >=80% (иначе
// "почти полный" раздел выпадал у ssh__diagnose_system из-за фильтра >90%).
// Парсим в Go, а не awk: тестируемо и не зависит от полей awk.
func highlightHighUsage(dfOut string) string {
if strings.TrimSpace(dfOut) == "" {
return dfOut
}
var b strings.Builder
for _, line := range strings.Split(dfOut, "\n") {
b.WriteString(line)
fields := strings.Fields(line)
// POSIX df -hP: колонка 5 (индекс 4) — "Use%" вроде "84%".
if len(fields) >= 5 {
if pct, err := strconv.Atoi(strings.TrimSuffix(fields[4], "%")); err == nil && pct >= 80 {
b.WriteString(" << USE>=80%")
}
}
b.WriteByte('\n')
}
return strings.TrimRight(b.String(), "\n")
}