package tools import ( "context" "omnichannel-configserver-mcp/internal/configserver" "github.com/modelcontextprotocol/go-sdk/mcp" ) // operate.go — жизненный цикл сервиса (deploy/restart/down/migrate) и загрузка // фронта. Мутации; обычно require_approval в agent.yaml. func registerOperateTools(s *mcp.Server) { addPatternTool(s, &mcp.Tool{ Name: "deploy", Description: "Поднять сервис (docker-compose up -d). Создаёт задачу агенту; wait=true — дождаться результата.", InputSchema: lifecycleSchema(), }, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return runLifecycle(ctx, req, "deploy", false) }) addPatternTool(s, &mcp.Tool{ Name: "restart", Description: "Перезапустить сервис (down + up -d). Создаёт задачу агенту; wait=true — дождаться результата.", InputSchema: lifecycleSchema(), }, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return runLifecycle(ctx, req, "restart", false) }) addPatternTool(s, &mcp.Tool{ Name: "migrate", Description: "Запустить миграции сервиса (docker-compose run migration). Создаёт задачу агенту.", InputSchema: lifecycleSchema(), }, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return runLifecycle(ctx, req, "migrate", false) }) addPatternTool(s, &mcp.Tool{ Name: "down", Description: "Остановить сервис (docker-compose down). Разрушительно: требует confirm=\"true\".", InputSchema: schema(map[string]any{ "server": serverProp(), "app_id": intProps("Идентификатор сервиса", true), "wait": boolProps("Дождаться завершения (по умолчанию false)", false), "confirm": boolProps("Подтверждение разрушительной операции (обязательно true)", true), }, []string{"app_id", "confirm"}), }, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return runLifecycle(ctx, req, "down", true) }) addPatternTool(s, &mcp.Tool{ Name: "update_front", Description: "Загрузить локальную сборку фронта на хост сервиса (полная замена build_folder). Разрушительно: требует confirm=\"true\". Каталог — внутри front_roots.", InputSchema: schema(map[string]any{ "server": serverProp(), "app_id": intProps("Идентификатор фронт-сервиса", true), "build_dir": strProps("Локальный каталог сборки (абсолютный путь внутри front_roots)", true), "confirm": boolProps("Подтверждение разрушительной операции (обязательно true)", true), }, []string{"app_id", "build_dir", "confirm"}), }, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := requestArgs(req) if res := confirmGate(args, "update_front"); res != nil { return res, nil } id, res := appIDArg(args) if res != nil { return res, nil } buildDir, err := requireString(args, "build_dir") if err != nil { return errorResult(err.Error()), nil } api, res := openAPI(ctx, args, true) if res != nil { return res, nil } defer api.Close() return result(api.UpdateFront(ctx, id, buildDir)) }) } // lifecycleSchema — общая схема deploy/restart/migrate/down. func lifecycleSchema() map[string]any { return schema(map[string]any{ "server": serverProp(), "app_id": intProps("Идентификатор сервиса", true), "wait": boolProps("Дождаться завершения задачи (по умолчанию false)", false), }, []string{"app_id"}) } // runLifecycle — общий путь операций жизненного цикла: вызвать действие, при // wait=true ограниченно дождаться задачи агента. func runLifecycle(ctx context.Context, req *mcp.CallToolRequest, action string, needConfirm bool) (*mcp.CallToolResult, error) { args := requestArgs(req) if needConfirm { if res := confirmGate(args, action); res != nil { return res, nil } } id, res := appIDArg(args) if res != nil { return res, nil } wait := getBool(args, "wait", false) api, res := openAPI(ctx, args, true) if res != nil { return res, nil } defer api.Close() var ( ack *configserver.TaskAck err error ) switch action { case "deploy": ack, err = api.Deploy(ctx, id) case "restart": ack, err = api.Restart(ctx, id) case "down": ack, err = api.Down(ctx, id) case "migrate": ack, err = api.Migrate(ctx, id) } if err != nil { return result(nil, err) } // Без wait отдаём task_id, чтобы не блокировать последовательный loop агента. if !wait { return result(ack, nil) } task, err := api.WaitTask(ctx, ack.TaskID) if err != nil { return result(nil, err) } return result(map[string]any{"ack": ack, "task": task}, nil) }