350 lines
12 KiB
Go
350 lines
12 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
|
messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
|
pmcp "git.k6n.net/mats/platform/mcp"
|
|
)
|
|
|
|
// buildTools returns the cart's MCP tools (transport/dispatch live in platform/mcp).
|
|
func (s *Server) buildTools() []pmcp.Tool {
|
|
return []pmcp.Tool{
|
|
{
|
|
Name: "get_cart",
|
|
Description: "Get the full state of a cart by its base62 cart id: items, totals, vouchers, promotions, user info, currency, language, checkout status, subscription details, and all applied/pending promotions with progress nudges.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
}, []string{"cartId"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
g, err := s.applier.Get(ctx, uint64(id))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get cart: %w", err)
|
|
}
|
|
return g, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "get_cart_items",
|
|
Description: "List all items in a cart with their SKU, name, quantity, unit price, total price, stock, tax rate, markings, custom fields, and optional child/parent relationships.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
}, []string{"cartId"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
g, err := s.applier.Get(ctx, uint64(id))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get cart: %w", err)
|
|
}
|
|
return map[string]any{"items": g.Items, "count": len(g.Items)}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "get_cart_totals",
|
|
Description: "Get price breakdown for a cart: total incVat, total exVat, VAT breakdown by rate, total discount (vouchers + promotions), and per-item totals.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
}, []string{"cartId"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
g, err := s.applier.Get(ctx, uint64(id))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get cart: %w", err)
|
|
}
|
|
return map[string]any{
|
|
"totalPrice": g.TotalPrice,
|
|
"totalDiscount": g.TotalDiscount,
|
|
"currency": g.Currency,
|
|
"language": g.Language,
|
|
"vouchers": g.Vouchers,
|
|
}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "get_cart_promotions",
|
|
Description: "Get all applied and pending promotions on a cart, including discount amounts and progress nudges (e.g. 'spend 1200 SEK more for free shipping').",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
}, []string{"cartId"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
g, err := s.applier.Get(ctx, uint64(id))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get cart: %w", err)
|
|
}
|
|
return map[string]any{
|
|
"appliedPromotions": g.AppliedPromotions,
|
|
"totalDiscount": g.TotalDiscount,
|
|
}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "get_cart_vouchers",
|
|
Description: "List all vouchers applied to a cart with their codes, values, rules, and whether they are currently active.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
}, []string{"cartId"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
g, err := s.applier.Get(ctx, uint64(id))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get cart: %w", err)
|
|
}
|
|
return map[string]any{"vouchers": g.Vouchers, "count": len(g.Vouchers)}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "update_item_quantity",
|
|
Description: "Change the quantity of a line item in a cart. Use quantity=0 to remove the item. Returns the updated cart state.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
"itemId": pmcp.Integer("the line item id (numeric, e.g. 1, 2, 3)"),
|
|
"quantity": pmcp.Integer("the new quantity (0 removes the item)"),
|
|
}, []string{"cartId", "itemId", "quantity"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
ItemID int `json:"itemId"`
|
|
Quantity int32 `json:"quantity"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
res, err := s.applier.Apply(ctx, uint64(id), &messages.ChangeQuantity{
|
|
Id: uint32(a.ItemID),
|
|
Quantity: a.Quantity,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("change quantity: %w", err)
|
|
}
|
|
// Check for per-mutation errors.
|
|
for _, m := range res.Mutations {
|
|
if m.Error != nil {
|
|
return nil, m.Error
|
|
}
|
|
}
|
|
return map[string]any{"cartId": a.CartID, "cart": &res.Result}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "remove_cart_item",
|
|
Description: "Remove a line item (and its children) from a cart by item id. Returns the updated cart state.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
"itemId": pmcp.Integer("the line item id to remove (numeric, e.g. 1, 2, 3)"),
|
|
}, []string{"cartId", "itemId"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
ItemID int `json:"itemId"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
res, err := s.applier.Apply(ctx, uint64(id), &messages.RemoveItem{Id: uint32(a.ItemID)})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("remove item: %w", err)
|
|
}
|
|
for _, m := range res.Mutations {
|
|
if m.Error != nil {
|
|
return nil, m.Error
|
|
}
|
|
}
|
|
return map[string]any{"cartId": a.CartID, "cart": &res.Result}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "clear_cart",
|
|
Description: "Remove all items and vouchers from a cart, resetting it to an empty state. The cart id, user id, and currency are preserved. Returns the updated (empty) cart.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
}, []string{"cartId"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
res, err := s.applier.Apply(ctx, uint64(id), &messages.ClearCartRequest{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("clear cart: %w", err)
|
|
}
|
|
for _, m := range res.Mutations {
|
|
if m.Error != nil {
|
|
return nil, m.Error
|
|
}
|
|
}
|
|
return map[string]any{"cartId": a.CartID, "cart": &res.Result}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "apply_voucher",
|
|
Description: "Apply a voucher code to a cart. The voucher value is in öre (e.g. 10000 = 100 kr). Returns the updated cart with the voucher applied.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
"code": pmcp.String("the voucher code"),
|
|
"value": pmcp.Integer("the voucher value in öre (e.g. 10000 = 100 kr)"),
|
|
"description": pmcp.String("optional description of the voucher"),
|
|
"rules": pmcp.StringArray("optional list of rule expressions for when the voucher applies"),
|
|
}, []string{"cartId", "code", "value"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
Code string `json:"code"`
|
|
Value int64 `json:"value"`
|
|
Description string `json:"description"`
|
|
Rules []string `json:"rules"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
res, err := s.applier.Apply(ctx, uint64(id), &messages.AddVoucher{
|
|
Code: a.Code,
|
|
Value: a.Value,
|
|
Description: a.Description,
|
|
VoucherRules: a.Rules,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("apply voucher: %w", err)
|
|
}
|
|
for _, m := range res.Mutations {
|
|
if m.Error != nil {
|
|
return nil, m.Error
|
|
}
|
|
}
|
|
return map[string]any{"cartId": a.CartID, "cart": &res.Result}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "remove_voucher",
|
|
Description: "Remove a voucher from a cart by its voucher id. Returns the updated cart.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
"voucherId": pmcp.Integer("the voucher id to remove (numeric)"),
|
|
}, []string{"cartId", "voucherId"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
VoucherID int `json:"voucherId"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
res, err := s.applier.Apply(ctx, uint64(id), &messages.RemoveVoucher{Id: uint32(a.VoucherID)})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("remove voucher: %w", err)
|
|
}
|
|
for _, m := range res.Mutations {
|
|
if m.Error != nil {
|
|
return nil, m.Error
|
|
}
|
|
}
|
|
return map[string]any{"cartId": a.CartID, "cart": &res.Result}, nil
|
|
},
|
|
},
|
|
{
|
|
Name: "set_cart_user",
|
|
Description: "Set or update the user ID on a cart, linking it to a customer account. Returns the updated cart.",
|
|
InputSchema: pmcp.Object(pmcp.Props{
|
|
"cartId": pmcp.String("the base62 cart id"),
|
|
"userId": pmcp.String("the user/customer id"),
|
|
}, []string{"cartId", "userId"}),
|
|
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var a struct {
|
|
CartID string `json:"cartId"`
|
|
UserID string `json:"userId"`
|
|
}
|
|
if err := pmcp.Decode(args, &a); err != nil {
|
|
return nil, err
|
|
}
|
|
id, ok := cart.ParseCartId(a.CartID)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cart id %q", a.CartID)
|
|
}
|
|
res, err := s.applier.Apply(ctx, uint64(id), &messages.SetUserId{UserId: a.UserID})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("set user: %w", err)
|
|
}
|
|
for _, m := range res.Mutations {
|
|
if m.Error != nil {
|
|
return nil, m.Error
|
|
}
|
|
}
|
|
return map[string]any{"cartId": a.CartID, "cart": &res.Result}, nil
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ---- result + schema helpers -----------------------------------------------
|