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" ) // tool is one MCP tool: a name, a description, a JSON-Schema for its arguments, // and the handler that runs it. type tool struct { Name string `json:"name"` Description string `json:"description"` InputSchema json.RawMessage `json:"inputSchema"` invoke func(args json.RawMessage) (any, error) `json:"-"` } func (s *Server) callTool(req *rpcRequest) *rpcResponse { var p struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } if err := json.Unmarshal(req.Params, &p); err != nil { return errorResponse(req.ID, codeInvalidParams, "invalid params") } var t *tool for i := range s.tools { if s.tools[i].Name == p.Name { t = &s.tools[i] break } } if t == nil { return errorResponse(req.ID, codeInvalidParams, "unknown tool: "+p.Name) } out, err := t.invoke(p.Arguments) if err != nil { return result(req.ID, toolError(err)) } return result(req.ID, toolText(out)) } func (s *Server) buildTools() []tool { return []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: object(props{ "cartId": str("the base62 cart id"), }, []string{"cartId"}), invoke: func(args json.RawMessage) (any, error) { var a struct { CartID string `json:"cartId"` } if err := 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(context.Background(), 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: object(props{ "cartId": str("the base62 cart id"), }, []string{"cartId"}), invoke: func(args json.RawMessage) (any, error) { var a struct { CartID string `json:"cartId"` } if err := 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(context.Background(), 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: object(props{ "cartId": str("the base62 cart id"), }, []string{"cartId"}), invoke: func(args json.RawMessage) (any, error) { var a struct { CartID string `json:"cartId"` } if err := 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(context.Background(), 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: object(props{ "cartId": str("the base62 cart id"), }, []string{"cartId"}), invoke: func(args json.RawMessage) (any, error) { var a struct { CartID string `json:"cartId"` } if err := 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(context.Background(), 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: object(props{ "cartId": str("the base62 cart id"), }, []string{"cartId"}), invoke: func(args json.RawMessage) (any, error) { var a struct { CartID string `json:"cartId"` } if err := 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(context.Background(), 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: object(props{ "cartId": str("the base62 cart id"), "itemId": integer("the line item id (numeric, e.g. 1, 2, 3)"), "quantity": integer("the new quantity (0 removes the item)"), }, []string{"cartId", "itemId", "quantity"}), invoke: func(args json.RawMessage) (any, error) { var a struct { CartID string `json:"cartId"` ItemID int `json:"itemId"` Quantity int32 `json:"quantity"` } if err := 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(context.Background(), 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: object(props{ "cartId": str("the base62 cart id"), "itemId": integer("the line item id to remove (numeric, e.g. 1, 2, 3)"), }, []string{"cartId", "itemId"}), invoke: func(args json.RawMessage) (any, error) { var a struct { CartID string `json:"cartId"` ItemID int `json:"itemId"` } if err := 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(context.Background(), 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: object(props{ "cartId": str("the base62 cart id"), }, []string{"cartId"}), invoke: func(args json.RawMessage) (any, error) { var a struct { CartID string `json:"cartId"` } if err := 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(context.Background(), 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: object(props{ "cartId": str("the base62 cart id"), "code": str("the voucher code"), "value": integer("the voucher value in öre (e.g. 10000 = 100 kr)"), "description": str("optional description of the voucher"), "rules": stringArray("optional list of rule expressions for when the voucher applies"), }, []string{"cartId", "code", "value"}), invoke: func(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 := 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(context.Background(), 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: object(props{ "cartId": str("the base62 cart id"), "voucherId": integer("the voucher id to remove (numeric)"), }, []string{"cartId", "voucherId"}), invoke: func(args json.RawMessage) (any, error) { var a struct { CartID string `json:"cartId"` VoucherID int `json:"voucherId"` } if err := 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(context.Background(), 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: object(props{ "cartId": str("the base62 cart id"), "userId": str("the user/customer id"), }, []string{"cartId", "userId"}), invoke: func(args json.RawMessage) (any, error) { var a struct { CartID string `json:"cartId"` UserID string `json:"userId"` } if err := 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(context.Background(), 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 ----------------------------------------------- func toolText(v any) map[string]any { b, err := json.Marshal(v) if err != nil { return toolError(err) } return map[string]any{ "content": []map[string]any{{"type": "text", "text": string(b)}}, } } func toolError(err error) map[string]any { return map[string]any{ "isError": true, "content": []map[string]any{{"type": "text", "text": err.Error()}}, } } // decode unmarshals tool arguments, tolerating empty/absent arguments. func decode(args json.RawMessage, v any) error { if len(args) == 0 || string(args) == "null" { return nil } if err := json.Unmarshal(args, v); err != nil { return fmt.Errorf("invalid arguments: %w", err) } return nil } type props map[string]json.RawMessage func object(p props, required []string) json.RawMessage { m := map[string]any{ "type": "object", "properties": p, } if len(required) > 0 { m["required"] = required } b, _ := json.Marshal(m) return b } func str(desc string) json.RawMessage { return scalar("string", desc) } func integer(d string) json.RawMessage { return scalar("integer", d) } func obj(desc string) json.RawMessage { return scalar("object", desc) } func scalar(typ, desc string) json.RawMessage { b, _ := json.Marshal(map[string]any{"type": typ, "description": desc}) return b } func stringArray(desc string) json.RawMessage { b, _ := json.Marshal(map[string]any{ "type": "array", "description": desc, "items": map[string]string{"type": "string"}, }) return b }