Initial commit: forge-tools-proxmox — MCP-сервер для Proxmox VE

This commit is contained in:
Maksim Totmin
2026-10-01 10:41:43 +07:00
commit a7addaead9
30 changed files with 4226 additions and 0 deletions
+354
View File
@@ -0,0 +1,354 @@
package tools
import (
"context"
"fmt"
"net/url"
"strconv"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// lxc.go — LXC-контейнеры: чтение (list/describe/config) + lifecycle
// (start/stop/reboot/shutdown), clone, delete, config_update, resize.
// Всё по аналогии с QEMU, но через /lxc. Мутации = gate + confirm + probe.
const vmTypeLXC = "lxc"
func registerLXCTools(s *mcp.Server) {
// --- read ---
s.AddTool(&mcp.Tool{
Name: "containers_list",
Description: "List LXC containers across the cluster. Read-only.",
InputSchema: schema(map[string]any{
"host": strProps("Cluster alias (default: primary)", false),
}, nil),
}, containersListHandler)
s.AddTool(&mcp.Tool{
Name: "container_describe",
Description: "Describe one LXC container: status + config. Read-only.",
InputSchema: schema(map[string]any{
"node": strProps("Node name", true),
"vmid": strProps("CT ID", true),
"host": strProps("Cluster alias (default: primary)", false),
}, []string{"node", "vmid"}),
}, containerDescribeHandler)
s.AddTool(&mcp.Tool{
Name: "container_config",
Description: "Get the raw LXC config of a container. Read-only.",
InputSchema: schema(map[string]any{
"node": strProps("Node name", true),
"vmid": strProps("CT ID", true),
"host": strProps("Cluster alias (default: primary)", false),
}, []string{"node", "vmid"}),
}, containerConfigHandler)
// --- lifecycle ---
registerPatternTool(s, &mcp.Tool{
Name: "container_start",
Description: "Start an LXC container. Requires write permission.",
InputSchema: containerActionSchema(false),
}, vmPatterns, containerStartHandler)
registerPatternTool(s, &mcp.Tool{
Name: "container_stop",
Description: "Stop an LXC container. Requires confirm + write permission.",
InputSchema: containerActionSchema(true),
}, vmPatterns, containerStopHandler)
registerPatternTool(s, &mcp.Tool{
Name: "container_reboot",
Description: "Reboot an LXC container. Requires confirm + write permission.",
InputSchema: containerActionSchema(true),
}, vmPatterns, containerRebootHandler)
registerPatternTool(s, &mcp.Tool{
Name: "container_shutdown",
Description: "Gracefully shut down an LXC container. Requires confirm + write permission.",
InputSchema: containerActionSchema(true),
}, vmPatterns, containerShutdownHandler)
// --- clone / delete / config / resize ---
registerPatternTool(s, &mcp.Tool{
Name: "container_clone",
Description: "Clone an LXC container/template into a new CT ID. Requires confirm + write permission.",
InputSchema: schema(map[string]any{
"node": strProps("Node name", true),
"vmid": strProps("Source CT ID", true),
"newid": intProps("New CT ID (0 = next free)", false),
"name": strProps("Name for the clone", false),
"confirm": strProps("Set to \"true\" to confirm", true, "true"),
}, []string{"node", "vmid", "confirm"}),
}, vmPatterns, containerCloneHandler)
registerPatternTool(s, &mcp.Tool{
Name: "container_delete",
Description: "Permanently delete an LXC container. Requires confirm + write permission.",
InputSchema: schema(map[string]any{
"node": strProps("Node name", true),
"vmid": strProps("CT ID", true),
"confirm": strProps("Set to \"true\" to confirm permanent deletion", true, "true"),
}, []string{"node", "vmid", "confirm"}),
}, vmPatterns, containerDeleteHandler)
registerPatternTool(s, &mcp.Tool{
Name: "container_config_update",
Description: "Update safe LXC config fields (hostname, memory, swap, cores, unprivileged). Denied keys rejected. Requires confirm + write permission.",
InputSchema: schema(map[string]any{
"node": strProps("Node name", true),
"vmid": strProps("CT ID", true),
"updates": objectProps("Object of key->value config fields", true),
"confirm": strProps("Set to \"true\" to confirm", true, "true"),
}, []string{"node", "vmid", "updates", "confirm"}),
}, vmPatterns, containerConfigUpdateHandler)
registerPatternTool(s, &mcp.Tool{
Name: "container_resize",
Description: "Resize a rootfs/mountpoint of an LXC container. Requires confirm + write permission.",
InputSchema: schema(map[string]any{
"node": strProps("Node name", true),
"vmid": strProps("CT ID", true),
"disk": strProps("Disk to resize (e.g. rootfs)", true),
"size": strProps("Size change, e.g. +5G", true),
"confirm": strProps("Set to \"true\" to confirm", true, "true"),
}, []string{"node", "vmid", "disk", "size", "confirm"}),
}, vmPatterns, containerResizeHandler)
}
// containerActionSchema — схема для lifecycle LXC (с/без confirm).
func containerActionSchema(withConfirm bool) map[string]any {
props := map[string]any{
"node": strProps("Node name", true),
"vmid": strProps("CT ID", true),
}
req := []string{"node", "vmid"}
if withConfirm {
props["confirm"] = strProps("Set to \"true\" to confirm", true, "true")
req = append(req, "confirm")
}
return schema(props, req)
}
// --- read ---
func containersListHandler(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.GuestResources(cctx, host, vmTypeLXC)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(pretty(data)), nil
}
func containerDescribeHandler(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, vmid, host, r := resolveVMArgs(t, args)
if r != nil {
return r, nil
}
cctx, cancel := timeout(ctx, t)
defer cancel()
status, err := t.ContainerStatus(cctx, host, node, vmid)
if err != nil {
return errorResult(err.Error()), nil
}
cfg, err := t.ContainerConfig(cctx, host, node, vmid)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult("=== Status ===\n" + pretty(status) + "\n\n=== Config ===\n" + pretty(cfg)), nil
}
func containerConfigHandler(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, vmid, host, r := resolveVMArgs(t, args)
if r != nil {
return r, nil
}
cctx, cancel := timeout(ctx, t)
defer cancel()
data, err := t.ContainerConfig(cctx, host, node, vmid)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(pretty(data)), nil
}
// --- lifecycle ---
func containerStartHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return containerActionHandler(ctx, req, "start", false)
}
func containerStopHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return containerActionHandler(ctx, req, "stop", true)
}
func containerRebootHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return containerActionHandler(ctx, req, "reboot", true)
}
func containerShutdownHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return containerActionHandler(ctx, req, "shutdown", true)
}
func containerActionHandler(ctx context.Context, req *mcp.CallToolRequest, action string, needConfirm bool) (*mcp.CallToolResult, error) {
t, err := tenantFor(ctx, req)
if err != nil {
return errorResult(err.Error()), nil
}
args := requestArgs(req)
node, vmid, host, r := resolveVMArgs(t, args)
if r != nil {
return r, nil
}
if needConfirm {
if err := confirm(args, "container_"+action); err != nil {
return errorResult(err.Error()), nil
}
}
if err := gateVMWrite(t, host, vmid); err != nil {
return errorResult(err.Error()), nil
}
upid, err := t.ContainerAction(ctx, host, node, vmid, action)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(upidMsg("container_"+action, fmt.Sprintf("%s/%d", node, vmid), upid)), nil
}
func containerCloneHandler(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, src, host, r := resolveVMArgs(t, args)
if r != nil {
return r, nil
}
if err := confirm(args, "container_clone"); err != nil {
return errorResult(err.Error()), nil
}
if err := gateVMWrite(t, host, src); err != nil {
return errorResult(err.Error()), nil
}
newID := getInt(args, "newid", 0)
if newID == 0 {
data, err := t.NextID(ctx, host)
if err != nil {
return errorResult(err.Error()), nil
}
newID = intFromData(data)
}
form := url.Values{"newid": {strconv.Itoa(newID)}}
if name := getString(args, "name", ""); name != "" {
form.Set("name", name)
}
upid, err := t.ContainerClone(ctx, host, node, src, newID, form)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(upidMsg("container_clone", fmt.Sprintf("%d -> %d", src, newID), upid)), nil
}
func containerDeleteHandler(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, vmid, host, r := resolveVMArgs(t, args)
if r != nil {
return r, nil
}
if err := confirm(args, "container_delete"); err != nil {
return errorResult(err.Error()), nil
}
if err := gateVMWrite(t, host, vmid); err != nil {
return errorResult(err.Error()), nil
}
upid, err := t.ContainerDelete(ctx, host, node, vmid)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(upidMsg("container_delete", fmt.Sprintf("%s/%d", node, vmid), upid)), nil
}
func containerConfigUpdateHandler(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, vmid, host, r := resolveVMArgs(t, args)
if r != nil {
return r, nil
}
if err := confirm(args, "container_config_update"); err != nil {
return errorResult(err.Error()), nil
}
if err := gateVMWrite(t, host, vmid); err != nil {
return errorResult(err.Error()), nil
}
updates := getObject(args, "updates")
if len(updates) == 0 {
return errorResult("'updates' must be a non-empty object"), nil
}
form := make(url.Values, len(updates))
for k, v := range updates {
if t.Config().DenyConfigKey(k) {
return errorResult(fmt.Sprintf("field %q is denied by policy", k)), nil
}
form.Set(k, fmt.Sprint(v))
}
upid, err := t.ConfigPost(ctx, host, node, vmTypeLXC, vmid, form)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(upidMsg("container_config_update", fmt.Sprintf("%s/%d", node, vmid), upid)), nil
}
func containerResizeHandler(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, vmid, host, r := resolveVMArgs(t, args)
if r != nil {
return r, nil
}
if err := confirm(args, "container_resize"); err != nil {
return errorResult(err.Error()), nil
}
if err := gateVMWrite(t, host, vmid); err != nil {
return errorResult(err.Error()), nil
}
disk, err := requireName(args, "disk")
if err != nil {
return errorResult(err.Error()), nil
}
size := getString(args, "size", "")
if size == "" {
return errorResult("'size' is required (e.g. +5G)"), nil
}
upid, err := t.ResizeDisk(ctx, host, node, vmTypeLXC, vmid, url.Values{"disk": {disk}, "size": {size}})
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(upidMsg("container_resize", fmt.Sprintf("%s/%d %s", node, vmid, disk), upid)), nil
}