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
+172
View File
@@ -0,0 +1,172 @@
package tools
import (
"context"
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// task.go — задачи Proxmox (UPID): list (read), status (read, с опц. wait),
// log (read), stop (confirm + write). Мутации возвращают UPID, поэтому
// task_status(wait=true) — ключ к подтверждению async-операций, при этом
// poll строго ограничен timeout (bounded §9.6).
func registerTaskTools(s *mcp.Server) {
s.AddTool(&mcp.Tool{
Name: "tasks_list",
Description: "List recent cluster/node tasks. Read-only.",
InputSchema: schema(map[string]any{
"node": strProps("Node name (empty = whole cluster)", false),
"limit": intProps("Number of tasks (default: 20)", false),
"host": strProps("Cluster alias (default: primary)", false),
}, nil),
}, tasksListHandler)
s.AddTool(&mcp.Tool{
Name: "task_status",
Description: "Get status of a task by UPID. Set wait=true to poll until it finishes (bounded by task_poll_max_sec). Read-only.",
InputSchema: schema(map[string]any{
"node": strProps("Node name", true),
"upid": strProps("Task UPID", true),
"wait": boolProps("Wait for completion (default: false)", false),
"host": strProps("Cluster alias (default: primary)", false),
}, []string{"node", "upid"}),
}, taskStatusHandler)
s.AddTool(&mcp.Tool{
Name: "task_log",
Description: "Get the log of a task by UPID. Read-only.",
InputSchema: schema(map[string]any{
"node": strProps("Node name", true),
"upid": strProps("Task UPID", true),
"limit": intProps("Number of lines (default: 50)", false),
"host": strProps("Cluster alias (default: primary)", false),
}, []string{"node", "upid"}),
}, taskLogHandler)
registerPatternTool(s, &mcp.Tool{
Name: "task_stop",
Description: "Stop/cancel a running task by UPID. Requires confirm + write permission.",
InputSchema: schema(map[string]any{
"node": strProps("Node name", true),
"upid": strProps("Task UPID", true),
"confirm": strProps("Set to \"true\" to confirm", true, "true"),
"host": strProps("Cluster alias (default: primary)", false),
}, []string{"node", "upid", "confirm"}),
}, nodePatterns, taskStopHandler)
}
func tasksListHandler(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)
host, err := resolveHost(t, args)
if err != nil {
return errorResult(err.Error()), nil
}
limit := getInt(args, "limit", 20)
cctx, cancel := timeout(ctx, t)
defer cancel()
data, err := t.TasksList(cctx, host, getString(args, "node", ""), limit)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(pretty(data)), nil
}
func taskStatusHandler(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
}
upid, err := requireUPID(args)
if err != nil {
return errorResult(err.Error()), nil
}
host, err := resolveHost(t, args)
if err != nil {
return errorResult(err.Error()), nil
}
if getBool(args, "wait", false) {
out, err := t.TaskWait(ctx, host, node, upid)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(out), nil
}
cctx, cancel := timeout(ctx, t)
defer cancel()
data, err := t.TaskStatus(cctx, host, node, upid)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(pretty(data)), nil
}
func taskLogHandler(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
}
upid, err := requireUPID(args)
if err != nil {
return errorResult(err.Error()), nil
}
host, err := resolveHost(t, args)
if err != nil {
return errorResult(err.Error()), nil
}
limit := getInt(args, "limit", 50)
cctx, cancel := timeout(ctx, t)
defer cancel()
data, err := t.TaskLog(cctx, host, node, upid, limit)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(pretty(data)), nil
}
func taskStopHandler(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
}
upid, err := requireUPID(args)
if err != nil {
return errorResult(err.Error()), nil
}
host, err := resolveHost(t, args)
if err != nil {
return errorResult(err.Error()), nil
}
if err := confirm(args, "task_stop"); err != nil {
return errorResult(err.Error()), nil
}
// task_stop — отмена выполняющейся задачи (не ресурс кластера).
if err := gateNodeWrite(t, host, node); err != nil {
return errorResult(err.Error()), nil
}
upidMsg2, err := t.TaskStop(ctx, host, node, upid)
if err != nil {
return errorResult(err.Error()), nil
}
return textResult(upidMsg("task_stop", fmt.Sprintf("%s %s", node, upid), upidMsg2)), nil
}