all the refactor
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s

This commit is contained in:
2026-06-28 17:51:52 +02:00
parent 7db0d236c7
commit aa8b2bdedc
84 changed files with 4328 additions and 2344 deletions
+68 -200
View File
@@ -4,107 +4,33 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"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"
)
// 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")
}
// Extract UCP meta from arguments (UCP-Agent profile advertisement).
// Per the UCP spec, meta is a sibling key inside arguments:
// "arguments": { "meta": { "ucp-agent": { "profile": "..." } }, ... }
args := extractUCPAgentMeta(&p.Arguments)
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(args)
if err != nil {
return result(req.ID, toolError(err))
}
return result(req.ID, toolText(out))
}
// extractUCPAgentMeta checks if the JSON arguments contain a "meta" key with
// a nested "ucp-agent.profile" field. If found, it logs the profile (for
// observability) and strips the "meta" key before returning the cleaned
// arguments to the tool handler, so the meta object does not leak into the
// tool's argument namespace.
func extractUCPAgentMeta(arguments *json.RawMessage) json.RawMessage {
if arguments == nil || len(*arguments) == 0 {
return *arguments
}
var obj map[string]json.RawMessage
if err := json.Unmarshal(*arguments, &obj); err != nil {
return *arguments
}
metaRaw, hasMeta := obj["meta"]
if !hasMeta {
return *arguments
}
// Try to extract the profile for observability.
var meta struct {
UCPAgent *struct {
Profile string `json:"profile"`
} `json:"ucp-agent"`
}
if err := json.Unmarshal(metaRaw, &meta); err == nil && meta.UCPAgent != nil && meta.UCPAgent.Profile != "" {
log.Printf("ucp-agent: profile=%s", meta.UCPAgent.Profile)
}
// Strip meta from arguments before passing to the tool handler.
delete(obj, "meta")
cleaned, err := json.Marshal(obj)
if err != nil {
return *arguments
}
return cleaned
}
func (s *Server) buildTools() []tool {
return []tool{
// 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: object(props{
"cartId": str("the base62 cart id"),
InputSchema: pmcp.Object(pmcp.Props{
"cartId": pmcp.String("the base62 cart id"),
}, []string{"cartId"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
CartID string `json:"cartId"`
}
if err := decode(args, &a); err != nil {
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(context.Background(), uint64(id))
g, err := s.applier.Get(ctx, uint64(id))
if err != nil {
return nil, fmt.Errorf("get cart: %w", err)
}
@@ -114,21 +40,21 @@ func (s *Server) buildTools() []tool {
{
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"),
InputSchema: pmcp.Object(pmcp.Props{
"cartId": pmcp.String("the base62 cart id"),
}, []string{"cartId"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
CartID string `json:"cartId"`
}
if err := decode(args, &a); err != nil {
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(context.Background(), uint64(id))
g, err := s.applier.Get(ctx, uint64(id))
if err != nil {
return nil, fmt.Errorf("get cart: %w", err)
}
@@ -138,21 +64,21 @@ func (s *Server) buildTools() []tool {
{
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"),
InputSchema: pmcp.Object(pmcp.Props{
"cartId": pmcp.String("the base62 cart id"),
}, []string{"cartId"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
CartID string `json:"cartId"`
}
if err := decode(args, &a); err != nil {
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(context.Background(), uint64(id))
g, err := s.applier.Get(ctx, uint64(id))
if err != nil {
return nil, fmt.Errorf("get cart: %w", err)
}
@@ -168,21 +94,21 @@ func (s *Server) buildTools() []tool {
{
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"),
InputSchema: pmcp.Object(pmcp.Props{
"cartId": pmcp.String("the base62 cart id"),
}, []string{"cartId"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
CartID string `json:"cartId"`
}
if err := decode(args, &a); err != nil {
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(context.Background(), uint64(id))
g, err := s.applier.Get(ctx, uint64(id))
if err != nil {
return nil, fmt.Errorf("get cart: %w", err)
}
@@ -195,21 +121,21 @@ func (s *Server) buildTools() []tool {
{
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"),
InputSchema: pmcp.Object(pmcp.Props{
"cartId": pmcp.String("the base62 cart id"),
}, []string{"cartId"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
CartID string `json:"cartId"`
}
if err := decode(args, &a); err != nil {
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(context.Background(), uint64(id))
g, err := s.applier.Get(ctx, uint64(id))
if err != nil {
return nil, fmt.Errorf("get cart: %w", err)
}
@@ -219,25 +145,25 @@ func (s *Server) buildTools() []tool {
{
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)"),
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(args json.RawMessage) (any, error) {
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 := decode(args, &a); err != nil {
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(context.Background(), uint64(id), &messages.ChangeQuantity{
res, err := s.applier.Apply(ctx, uint64(id), &messages.ChangeQuantity{
Id: uint32(a.ItemID),
Quantity: a.Quantity,
})
@@ -256,23 +182,23 @@ func (s *Server) buildTools() []tool {
{
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)"),
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(args json.RawMessage) (any, error) {
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
CartID string `json:"cartId"`
ItemID int `json:"itemId"`
}
if err := decode(args, &a); err != nil {
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(context.Background(), uint64(id), &messages.RemoveItem{Id: uint32(a.ItemID)})
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)
}
@@ -287,21 +213,21 @@ func (s *Server) buildTools() []tool {
{
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"),
InputSchema: pmcp.Object(pmcp.Props{
"cartId": pmcp.String("the base62 cart id"),
}, []string{"cartId"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
CartID string `json:"cartId"`
}
if err := decode(args, &a); err != nil {
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(context.Background(), uint64(id), &messages.ClearCartRequest{})
res, err := s.applier.Apply(ctx, uint64(id), &messages.ClearCartRequest{})
if err != nil {
return nil, fmt.Errorf("clear cart: %w", err)
}
@@ -316,14 +242,14 @@ func (s *Server) buildTools() []tool {
{
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"),
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(args json.RawMessage) (any, error) {
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
CartID string `json:"cartId"`
Code string `json:"code"`
@@ -331,14 +257,14 @@ func (s *Server) buildTools() []tool {
Description string `json:"description"`
Rules []string `json:"rules"`
}
if err := decode(args, &a); err != nil {
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(context.Background(), uint64(id), &messages.AddVoucher{
res, err := s.applier.Apply(ctx, uint64(id), &messages.AddVoucher{
Code: a.Code,
Value: a.Value,
Description: a.Description,
@@ -358,23 +284,23 @@ func (s *Server) buildTools() []tool {
{
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)"),
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(args json.RawMessage) (any, error) {
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
CartID string `json:"cartId"`
VoucherID int `json:"voucherId"`
}
if err := decode(args, &a); err != nil {
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(context.Background(), uint64(id), &messages.RemoveVoucher{Id: uint32(a.VoucherID)})
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)
}
@@ -389,23 +315,23 @@ func (s *Server) buildTools() []tool {
{
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"),
InputSchema: pmcp.Object(pmcp.Props{
"cartId": pmcp.String("the base62 cart id"),
"userId": pmcp.String("the user/customer id"),
}, []string{"cartId", "userId"}),
invoke: func(args json.RawMessage) (any, error) {
Invoke: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
CartID string `json:"cartId"`
UserID string `json:"userId"`
}
if err := decode(args, &a); err != nil {
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(context.Background(), uint64(id), &messages.SetUserId{UserId: a.UserID})
res, err := s.applier.Apply(ctx, uint64(id), &messages.SetUserId{UserId: a.UserID})
if err != nil {
return nil, fmt.Errorf("set user: %w", err)
}
@@ -421,61 +347,3 @@ func (s *Server) buildTools() []tool {
}
// ---- 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
}