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:
+59
-10
@@ -25,8 +25,17 @@ type PairsRequest struct {
|
||||
Limit int
|
||||
}
|
||||
|
||||
// CurrencyPair is one row from get_currency_pairs.
|
||||
type CurrencyPair struct {
|
||||
ID string
|
||||
Symbol string
|
||||
}
|
||||
|
||||
// PairsResult is the parsed get_currency_pairs payload.
|
||||
type PairsResult struct{}
|
||||
type PairsResult struct {
|
||||
Pairs []CurrencyPair
|
||||
NextCursor string
|
||||
}
|
||||
|
||||
// QuotesRequest is the argument set for get_crypto_quotes.
|
||||
type QuotesRequest struct {
|
||||
@@ -35,8 +44,19 @@ type QuotesRequest struct {
|
||||
RHSAccountNumber string
|
||||
}
|
||||
|
||||
// CryptoQuote is one pair quote from get_crypto_quotes.
|
||||
type CryptoQuote struct {
|
||||
Symbol string
|
||||
Bid decimal.Decimal
|
||||
Ask decimal.Decimal
|
||||
Mark decimal.Decimal
|
||||
PrevClose decimal.Decimal
|
||||
}
|
||||
|
||||
// QuotesResult is the parsed get_crypto_quotes payload.
|
||||
type QuotesResult struct{}
|
||||
type QuotesResult struct {
|
||||
Quotes []CryptoQuote
|
||||
}
|
||||
|
||||
// PositionsRequest is the argument set for get_crypto_positions.
|
||||
type PositionsRequest struct {
|
||||
@@ -44,8 +64,18 @@ type PositionsRequest struct {
|
||||
Cursor string
|
||||
}
|
||||
|
||||
// CryptoPosition is one holding from get_crypto_positions.
|
||||
type CryptoPosition struct {
|
||||
Symbol string
|
||||
Quantity decimal.Decimal
|
||||
CostBasis decimal.Decimal
|
||||
}
|
||||
|
||||
// PositionsResult is the parsed get_crypto_positions payload.
|
||||
type PositionsResult struct{}
|
||||
type PositionsResult struct {
|
||||
Positions []CryptoPosition
|
||||
NextCursor string
|
||||
}
|
||||
|
||||
// OrdersRequest is the argument set for get_crypto_orders.
|
||||
type OrdersRequest struct {
|
||||
@@ -61,7 +91,10 @@ type OrdersRequest struct {
|
||||
}
|
||||
|
||||
// OrdersResult is the parsed get_crypto_orders payload.
|
||||
type OrdersResult struct{}
|
||||
type OrdersResult struct {
|
||||
Orders []Order
|
||||
NextCursor string
|
||||
}
|
||||
|
||||
// PlaceOrderRequest is the argument set for preview_crypto_order and place_crypto_order.
|
||||
type PlaceOrderRequest struct {
|
||||
@@ -78,11 +111,20 @@ type PlaceOrderRequest struct {
|
||||
}
|
||||
|
||||
// PreviewResult is the pre-trade check from preview_crypto_order.
|
||||
type PreviewResult struct{}
|
||||
type PreviewResult struct {
|
||||
Errors []string `json:"errors"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Quantity decimal.Decimal `json:"-"`
|
||||
Price decimal.Decimal `json:"-"`
|
||||
}
|
||||
|
||||
// Order is a placed crypto order.
|
||||
// Order is a placed or listed crypto order.
|
||||
type Order struct {
|
||||
ID string `json:"id"`
|
||||
ID string `json:"id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"`
|
||||
State string `json:"state"`
|
||||
Qty decimal.Decimal `json:"-"`
|
||||
}
|
||||
|
||||
// CancelOrderRequest is the argument set for cancel_crypto_order.
|
||||
@@ -190,11 +232,18 @@ func (c *Client) PreviewOrder(ctx context.Context, req PlaceOrderRequest) (Previ
|
||||
|
||||
// PlaceOrder calls place_crypto_order. RefID is sent as ref_id.
|
||||
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) > 0 {
|
||||
return ords[0], nil
|
||||
}
|
||||
return Order{}, nil
|
||||
}
|
||||
|
||||
// CancelOrder calls cancel_crypto_order.
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
decimal "github.com/alpacahq/alpacadecimal"
|
||||
"s1d3sw1ped/robinhood-agentic-mcp/internal/wire"
|
||||
)
|
||||
|
||||
func (o *Order) UnmarshalJSON(b []byte) error {
|
||||
var row struct {
|
||||
ID string `json:"id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Quantity any `json:"quantity"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &row); err != nil {
|
||||
return err
|
||||
}
|
||||
o.ID, o.Symbol, o.Side = row.ID, row.Symbol, row.Side
|
||||
o.State = row.State
|
||||
if o.State == "" {
|
||||
o.State = row.Status
|
||||
}
|
||||
var err error
|
||||
o.Qty, err = firstDec(row.Quantity)
|
||||
return err
|
||||
}
|
||||
|
||||
func parseOrders(raw json.RawMessage) ([]Order, error) {
|
||||
rows, _, err := wire.UnmarshalRows[Order](raw, "results", "orders")
|
||||
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
|
||||
}
|
||||
|
||||
func (r *PairsResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
ID string `json:"id"`
|
||||
Symbol string `json:"symbol"`
|
||||
}
|
||||
rows, next, err := wire.UnmarshalRows[row](b, "results", "pairs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.NextCursor = next
|
||||
r.Pairs = make([]CurrencyPair, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
r.Pairs = append(r.Pairs, CurrencyPair{ID: row.ID, Symbol: row.Symbol})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *QuotesResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Bid any `json:"bid"`
|
||||
Ask any `json:"ask"`
|
||||
Mark any `json:"mark"`
|
||||
PrevClose any `json:"open_price"`
|
||||
Close any `json:"previous_close"`
|
||||
}
|
||||
rows, _, err := wire.UnmarshalRows[row](b, "results", "quotes")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Quotes = make([]CryptoQuote, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
bid, err := firstDec(row.Bid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ask, err := firstDec(row.Ask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mark, err := firstDec(row.Mark)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prev, err := firstDec(row.PrevClose, row.Close)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Quotes = append(r.Quotes, CryptoQuote{Symbol: row.Symbol, Bid: bid, Ask: ask, Mark: mark, PrevClose: prev})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PositionsResult) UnmarshalJSON(b []byte) error {
|
||||
type row struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Asset string `json:"asset"`
|
||||
Quantity any `json:"quantity"`
|
||||
CostBasis any `json:"cost_basis"`
|
||||
}
|
||||
rows, next, err := wire.UnmarshalRows[row](b, "results", "positions")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.NextCursor = next
|
||||
r.Positions = make([]CryptoPosition, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
sym := row.Symbol
|
||||
if sym == "" {
|
||||
sym = row.Asset
|
||||
}
|
||||
qty, err := firstDec(row.Quantity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cost, err := firstDec(row.CostBasis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Positions = append(r.Positions, CryptoPosition{Symbol: sym, Quantity: qty, CostBasis: cost})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *OrdersResult) UnmarshalJSON(b []byte) error {
|
||||
rows, next, err := wire.UnmarshalRows[Order](b, "results", "orders")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Orders = rows
|
||||
r.NextCursor = next
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PreviewResult) UnmarshalJSON(b []byte) error {
|
||||
var wrap struct {
|
||||
Errors []string `json:"errors"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Quantity any `json:"quantity"`
|
||||
Price any `json:"price"`
|
||||
}
|
||||
if err := json.Unmarshal(wire.Unwrap(b), &wrap); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Errors, r.Warnings = wrap.Errors, wrap.Warnings
|
||||
var err error
|
||||
if r.Quantity, err = firstDec(wrap.Quantity); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Price, err = firstDec(wrap.Price)
|
||||
return err
|
||||
}
|
||||
|
||||
func firstDec(vs ...any) (decimal.Decimal, error) {
|
||||
for _, v := range vs {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if s, ok := v.(string); ok && s == "" {
|
||||
continue
|
||||
}
|
||||
return wire.Dec(v)
|
||||
}
|
||||
return decimal.Zero, nil
|
||||
}
|
||||
Reference in New Issue
Block a user