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
+95
View File
@@ -0,0 +1,95 @@
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")
}