package tools import ( "context" "fmt" "strings" "github.com/modelcontextprotocol/go-sdk/mcp" ) // cluster.go — read-only инструменты кластера/нод (наблюдение, без мутаций). // Здесь НЕТ node_reboot/shutdown: в однонодовом homelab рестарт хоста // обрушил бы всё окружение (blast-radius при нулевой ценности). func registerClusterTools(s *mcp.Server) { // clusters_list — перечень настроенных кластеров (операторских) + версия. s.AddTool(&mcp.Tool{ Name: "clusters_list", Description: "List configured Proxmox clusters (host aliases) and their PVE version. Read-only.", InputSchema: schema(nil, nil), }, clustersListHandler) // cluster_status — состояние кластера (quorum, узлы, версии). s.AddTool(&mcp.Tool{ Name: "cluster_status", Description: "Get cluster health: quorum, node list, versions. Read-only.", InputSchema: schema(map[string]any{ "host": strProps("Cluster alias (default: primary)", false), }, nil), }, clusterStatusHandler) // nodes_list — список нод кластера. s.AddTool(&mcp.Tool{ Name: "nodes_list", Description: "List all nodes in the cluster with status and resource usage. Read-only.", InputSchema: schema(map[string]any{ "host": strProps("Cluster alias (default: primary)", false), }, nil), }, nodesListHandler) // node_status — детальное состояние ноды. s.AddTool(&mcp.Tool{ Name: "node_status", Description: "Get detailed status (CPU/memory/disk/uptime) of one node. Read-only.", InputSchema: schema(map[string]any{ "node": strProps("Node name", true), "host": strProps("Cluster alias (default: primary)", false), }, []string{"node"}), }, nodeStatusHandler) // node_network — сетевые интерфейсы ноды (мосты/eth). s.AddTool(&mcp.Tool{ Name: "node_network", Description: "List network interfaces and bridges of a node. Read-only.", InputSchema: schema(map[string]any{ "node": strProps("Node name", true), "host": strProps("Cluster alias (default: primary)", false), }, []string{"node"}), }, nodeNetworkHandler) } func clustersListHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { t, err := tenantFor(ctx, req) if err != nil { return errorResult(err.Error()), nil } var aliases []string for _, h := range t.Config().Hosts { aliases = append(aliases, h.Alias) } cctx, cancel := timeout(ctx, t) defer cancel() ver, err := t.Version(cctx, "") if err != nil { return textResult(fmt.Sprintf("Clusters configured: %s\nPVE version: unavailable (%s)", strings.Join(aliases, ", "), err)), nil } return textResult(fmt.Sprintf("Clusters configured: %s\nPVE version: %s", strings.Join(aliases, ", "), pretty(ver))), nil } func clusterStatusHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { t, err := tenantFor(ctx, req) if err != nil { return errorResult(err.Error()), nil } host, err := resolveHost(t, requestArgs(req)) if err != nil { return errorResult(err.Error()), nil } cctx, cancel := timeout(ctx, t) defer cancel() data, err := t.ClusterStatus(cctx, host) if err != nil { return errorResult(err.Error()), nil } return textResult(pretty(data)), nil } func nodesListHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { t, err := tenantFor(ctx, req) if err != nil { return errorResult(err.Error()), nil } host, err := resolveHost(t, requestArgs(req)) if err != nil { return errorResult(err.Error()), nil } cctx, cancel := timeout(ctx, t) defer cancel() data, err := t.Nodes(cctx, host) if err != nil { return errorResult(err.Error()), nil } return textResult(pretty(data)), nil } func nodeStatusHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { t, err := tenantFor(ctx, req) if err != nil { return errorResult(err.Error()), nil } args := requestArgs(req) node, err := requireNode(args) if err != nil { return errorResult(err.Error()), nil } host, err := resolveHost(t, args) if err != nil { return errorResult(err.Error()), nil } cctx, cancel := timeout(ctx, t) defer cancel() data, err := t.NodeStatus(cctx, host, node) if err != nil { return errorResult(err.Error()), nil } return textResult(pretty(data)), nil } func nodeNetworkHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { t, err := tenantFor(ctx, req) if err != nil { return errorResult(err.Error()), nil } args := requestArgs(req) node, err := requireNode(args) if err != nil { return errorResult(err.Error()), nil } host, err := resolveHost(t, args) if err != nil { return errorResult(err.Error()), nil } cctx, cancel := timeout(ctx, t) defer cancel() data, err := t.NodeNetwork(cctx, host, node) if err != nil { return errorResult(err.Error()), nil } return textResult(pretty(data)), nil }