fix: parse MCP result payloads and drop place idempotency_key

Typed result structs replace empty envelopes. Equity place sends
ref_id only so live additionalProperties:false schemas accept the call.
This commit is contained in:
2026-09-01 14:18:39 -05:00
parent f52a19181d
commit a6bf8632ce
22 changed files with 2225 additions and 74 deletions
+81 -7
View File
@@ -2,6 +2,7 @@ package equity
import (
"context"
"encoding/json"
decimal "github.com/alpacahq/alpacadecimal"
"s1d3sw1ped/robinhood-agentic-mcp/client"
@@ -42,9 +43,73 @@ type ReviewResult struct {
Warnings []string `json:"warnings"`
}
// Order is a placed equity order.
// Order is a placed or listed equity order.
type Order struct {
ID string `json:"id"`
ID string `json:"id"`
IdempotencyKey string `json:"idempotency_key"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Qty decimal.Decimal `json:"-"`
FilledQty decimal.Decimal `json:"-"`
Price decimal.Decimal `json:"-"`
State string `json:"state"`
}
func (o *Order) UnmarshalJSON(b []byte) error {
var row struct {
ID string `json:"id"`
IdempotencyKey string `json:"idempotency_key"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Quantity any `json:"quantity"`
Qty any `json:"qty"`
FilledQty any `json:"filled_quantity"`
CumulativeQty any `json:"cumulative_quantity"`
Price any `json:"price"`
AveragePrice any `json:"average_price"`
State string `json:"state"`
Status string `json:"status"`
}
if err := json.Unmarshal(b, &row); err != nil {
return err
}
o.ID = row.ID
o.IdempotencyKey = row.IdempotencyKey
o.Symbol = row.Symbol
o.Side = row.Side
o.State = row.State
if o.State == "" {
o.State = row.Status
}
var err error
if o.Qty, err = firstDec(row.Quantity, row.Qty); err != nil {
return err
}
if o.FilledQty, err = firstDec(row.FilledQty, row.CumulativeQty); err != nil {
return err
}
if o.Price, err = firstDec(row.Price, row.AveragePrice); err != nil {
return err
}
return nil
}
func parseOrders(raw json.RawMessage) ([]Order, error) {
rows, _, err := wire.UnmarshalRows[Order](raw, "orders", "results")
if err != nil {
return nil, err
}
if len(rows) > 0 {
return rows, nil
}
var one Order
if err := json.Unmarshal(wire.Unwrap(raw), &one); err != nil {
return nil, err
}
if one.ID != "" {
return []Order{one}, nil
}
return nil, nil
}
// CancelOrderRequest is the argument set for cancel_equity_order.
@@ -62,13 +127,23 @@ func (c *Client) ReviewOrder(ctx context.Context, req PlaceOrderRequest) (Review
return out, nil
}
// PlaceOrder calls place_equity_order. RefID is sent as both ref_id and idempotency_key.
// PlaceOrder calls place_equity_order. RefID is sent as ref_id only.
func (c *Client) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) {
var out Order
if err := c.parse(ctx, toolPlace, placeArgs(req, true), &out); err != nil {
raw, err := c.c.Call(ctx, toolPlace, placeArgs(req, true))
if err != nil {
return Order{}, err
}
return out, nil
ords, err := parseOrders(raw)
if err != nil {
return Order{}, client.ToolErrorf(toolPlace, "parse: %w", err)
}
if len(ords) == 1 {
return ords[0], nil
}
if len(ords) > 1 {
return ords[0], nil
}
return Order{}, nil
}
// CancelOrder calls cancel_equity_order.
@@ -128,7 +203,6 @@ func placeArgs(req PlaceOrderRequest, withRef bool) map[string]any {
}
if withRef && req.RefID != "" {
args["ref_id"] = req.RefID
args["idempotency_key"] = req.RefID
}
return args
}