package mcp import ( "encoding/json" "fmt" "git.k6n.net/mats/go-cart-actor/pkg/promotions" pmcp "git.k6n.net/mats/platform/mcp" ) // AdminTool is one admin promotion tool definition — name, description, input // schema, and the invoke function. It mirrors cmsmount.MCPTool but lives in // this package so the promotions module doesn't import the CMS module. type AdminTool struct { Name string Description string InputSchema json.RawMessage Invoke func(args json.RawMessage) (any, error) } // AdminTools returns the admin-only promotion MCP tool definitions (upsert, // set_status, delete). Pass these to cmsmount.AddMCPTools to merge them into // the CMS MCP behind full auth. func AdminTools(store *promotions.Store, eval *promotions.PromotionService) []AdminTool { if eval == nil { eval = promotions.NewPromotionService(nil) } return []AdminTool{ { Name: "upsert_promotion", Description: "Create a promotion or replace the existing one with the same id. " + "`promotion` is a full promotion-rule object (id, name, status, priority, conditions, actions). " + "For a tiered volume discount (Volymrabatt) use a cart_total condition plus a tiered_discount action " + "whose config.tiers each have minTotal/maxTotal (öre incl. VAT) and percent.", InputSchema: pmcp.Object(pmcp.Props{ "promotion": pmcp.ObjectValue("the full promotion rule object"), }, []string{"promotion"}), Invoke: func(args json.RawMessage) (any, error) { var a struct { Promotion json.RawMessage `json:"promotion"` } if err := pmcp.Decode(args, &a); err != nil { return nil, err } var rule promotions.PromotionRule if err := json.Unmarshal(a.Promotion, &rule); err != nil { return nil, fmt.Errorf("invalid promotion: %w", err) } if rule.ID == "" { return nil, fmt.Errorf("promotion.id is required") } if rule.Status == "" { rule.Status = promotions.StatusActive } replaced, err := store.Upsert(rule) if err != nil { return nil, err } return map[string]any{"id": rule.ID, "replaced": replaced}, nil }, }, { Name: "set_promotion_status", Description: "Set a promotion's status (active|inactive|scheduled|expired) to toggle it on or off.", InputSchema: pmcp.Object(pmcp.Props{ "id": pmcp.String("the promotion id"), "status": pmcp.String("new status: active|inactive|scheduled|expired"), }, []string{"id", "status"}), Invoke: func(args json.RawMessage) (any, error) { var a struct { ID string `json:"id"` Status string `json:"status"` } if err := pmcp.Decode(args, &a); err != nil { return nil, err } if !validStatus(a.Status) { return nil, fmt.Errorf("invalid status %q", a.Status) } found, err := store.SetStatus(a.ID, promotions.PromotionStatus(a.Status)) if err != nil { return nil, err } if !found { return nil, fmt.Errorf("promotion %q not found", a.ID) } return map[string]any{"id": a.ID, "status": a.Status}, nil }, }, { Name: "delete_promotion", Description: "Delete a promotion rule by id.", InputSchema: pmcp.Object(pmcp.Props{"id": pmcp.String("the promotion id")}, []string{"id"}), Invoke: func(args json.RawMessage) (any, error) { var a struct { ID string `json:"id"` } if err := pmcp.Decode(args, &a); err != nil { return nil, err } found, err := store.Delete(a.ID) if err != nil { return nil, err } if !found { return nil, fmt.Errorf("promotion %q not found", a.ID) } return map[string]any{"id": a.ID, "deleted": true}, nil }, }, } }