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 . Use for 'how much space/free on '.", 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 reports the FS containing (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") }