all the refactor
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// JSON-RPC 2.0 envelope types (the wire format MCP rides on). Notifications are
|
||||
// requests with no id and must not receive a response.
|
||||
|
||||
type rpcRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
func (r *rpcRequest) isNotification() bool { return len(r.ID) == 0 }
|
||||
|
||||
type rpcResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id,omitempty"`
|
||||
Result any `json:"result,omitempty"`
|
||||
Error *rpcError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type rpcError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
codeParseError = -32700
|
||||
codeInvalidRequest = -32600
|
||||
codeMethodNotFound = -32601
|
||||
codeInvalidParams = -32602
|
||||
codeInternalError = -32603
|
||||
)
|
||||
|
||||
func result(id json.RawMessage, v any) *rpcResponse {
|
||||
return &rpcResponse{JSONRPC: "2.0", ID: id, Result: v}
|
||||
}
|
||||
|
||||
func errorResponse(id json.RawMessage, code int, msg string) *rpcResponse {
|
||||
return &rpcResponse{JSONRPC: "2.0", ID: id, Error: &rpcError{Code: code, Message: msg}}
|
||||
}
|
||||
+11
-121
@@ -3,27 +3,25 @@
|
||||
// (list items, change quantities, remove items, clear carts, apply vouchers,
|
||||
// manage users, etc.).
|
||||
//
|
||||
// Transport is streamable HTTP (JSON-RPC 2.0 over POST). It is mounted by
|
||||
// cmd/cart under /mcp on the same HTTP server as the cart API. Tools map
|
||||
// 1:1 to cart grain read/apply operations; no restart is needed because every
|
||||
// tool call goes through the shared grain pool.
|
||||
// Transport (JSON-RPC 2.0 over streamable HTTP), dispatch, and the schema/result
|
||||
// helpers live in platform/mcp; this package only defines the cart-specific
|
||||
// tools and wires them to the grain pool. Mounted by cmd/cart under /mcp.
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
pmcp "git.k6n.net/mats/platform/mcp"
|
||||
"net/http"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
serverName = "cart"
|
||||
serverVersion = "0.1.0"
|
||||
protocolVersion = "2025-06-18"
|
||||
serverName = "cart"
|
||||
serverVersion = "0.1.0"
|
||||
)
|
||||
|
||||
// Applier is the minimal grain-pool interface the cart MCP needs: read a
|
||||
@@ -37,123 +35,15 @@ type Applier interface {
|
||||
// Server is the MCP edge over the cart grain pool.
|
||||
type Server struct {
|
||||
applier Applier
|
||||
tools []tool
|
||||
mcp *pmcp.Server
|
||||
}
|
||||
|
||||
// New builds an MCP server exposing the cart grain as tools.
|
||||
func New(applier Applier) *Server {
|
||||
s := &Server{applier: applier}
|
||||
s.tools = s.buildTools()
|
||||
s.mcp = pmcp.NewServer(pmcp.Info{Name: serverName, Version: serverVersion}, s.buildTools()...)
|
||||
return s
|
||||
}
|
||||
|
||||
// Handler returns the streamable-HTTP handler to mount (e.g. at /mcp).
|
||||
func (s *Server) Handler() http.Handler {
|
||||
return http.HandlerFunc(s.serveHTTP)
|
||||
}
|
||||
|
||||
func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", "POST")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
|
||||
if err != nil {
|
||||
writeRPC(w, errorResponse(nil, codeParseError, "read error"))
|
||||
return
|
||||
}
|
||||
if isBatch(body) {
|
||||
var reqs []rpcRequest
|
||||
if err := json.Unmarshal(body, &reqs); err != nil {
|
||||
writeRPC(w, errorResponse(nil, codeParseError, "invalid JSON"))
|
||||
return
|
||||
}
|
||||
var out []*rpcResponse
|
||||
for i := range reqs {
|
||||
if resp := s.dispatch(&reqs[i]); resp != nil {
|
||||
out = append(out, resp)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
writeRPC(w, out)
|
||||
return
|
||||
}
|
||||
var req rpcRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
writeRPC(w, errorResponse(nil, codeParseError, "invalid JSON"))
|
||||
return
|
||||
}
|
||||
resp := s.dispatch(&req)
|
||||
if resp == nil {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
writeRPC(w, resp)
|
||||
}
|
||||
|
||||
func (s *Server) dispatch(req *rpcRequest) *rpcResponse {
|
||||
if req.JSONRPC != "2.0" {
|
||||
if req.isNotification() {
|
||||
return nil
|
||||
}
|
||||
return errorResponse(req.ID, codeInvalidRequest, "jsonrpc must be \"2.0\"")
|
||||
}
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
return result(req.ID, s.initialize(req.Params))
|
||||
case "ping":
|
||||
return result(req.ID, struct{}{})
|
||||
case "tools/list":
|
||||
return result(req.ID, map[string]any{"tools": s.tools})
|
||||
case "tools/call":
|
||||
return s.callTool(req)
|
||||
case "notifications/initialized", "notifications/cancelled":
|
||||
return nil
|
||||
default:
|
||||
if req.isNotification() {
|
||||
return nil
|
||||
}
|
||||
return errorResponse(req.ID, codeMethodNotFound, "unknown method: "+req.Method)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) initialize(params json.RawMessage) map[string]any {
|
||||
pv := protocolVersion
|
||||
if len(params) > 0 {
|
||||
var p struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
}
|
||||
if json.Unmarshal(params, &p) == nil && p.ProtocolVersion != "" {
|
||||
pv = p.ProtocolVersion
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"protocolVersion": pv,
|
||||
"capabilities": map[string]any{"tools": map[string]any{}},
|
||||
"serverInfo": map[string]any{"name": serverName, "version": serverVersion},
|
||||
}
|
||||
}
|
||||
|
||||
func isBatch(body []byte) bool {
|
||||
for _, b := range body {
|
||||
switch b {
|
||||
case ' ', '\t', '\r', '\n':
|
||||
continue
|
||||
case '[':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func writeRPC(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
func (s *Server) Handler() http.Handler { return s.mcp.Handler() }
|
||||
|
||||
+68
-200
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user