package watchlists import ( "context" "encoding/json" "s1d3sw1ped/robinhood-agentic-mcp/client" "s1d3sw1ped/robinhood-agentic-mcp/internal/wire" ) const ( toolLists = "get_watchlists" toolItems = "get_watchlist_items" toolOptionList = "get_option_watchlist" toolPopular = "get_popular_watchlists" toolCreate = "create_watchlist" toolUpdate = "update_watchlist" toolFollow = "follow_watchlist" toolUnfollow = "unfollow_watchlist" toolAdd = "add_to_watchlist" toolRemove = "remove_from_watchlist" toolAddOption = "add_option_to_watchlist" toolRemoveOption = "remove_option_from_watchlist" ) // ListsRequest is the argument set for get_watchlists (none). type ListsRequest struct{} // Watchlist is one list from get_watchlists. Title falls back to name. type Watchlist struct { ID string Title string } // ListsResult is the parsed get_watchlists payload. type ListsResult struct { Watchlists []Watchlist } // ItemsRequest is the argument set for get_watchlist_items. type ItemsRequest struct { ListID string } // Instrument is the nested instrument object on a watchlist item. type Instrument struct { Symbol string Type string } // Item is one row from get_watchlist_items. Object types are not filtered. type Item struct { Symbol string ObjectType string Instrument *Instrument } // ItemsResult is the parsed get_watchlist_items payload. type ItemsResult struct { Items []Item } // OptionListRequest is the argument set for get_option_watchlist (none). type OptionListRequest struct{} // OptionWatchItem is one contract on the options watchlist. type OptionWatchItem struct { OptionID string Symbol string Title string PositionType string } // OptionListResult is the parsed get_option_watchlist payload. type OptionListResult struct { Items []OptionWatchItem } // PopularRequest is the argument set for get_popular_watchlists (none). type PopularRequest struct{} // PopularResult is the parsed get_popular_watchlists payload. type PopularResult struct { Watchlists []Watchlist } // CreateRequest is the argument set for create_watchlist. type CreateRequest struct { DisplayName string IconEmoji string DisplayDescription string } // UpdateRequest is the argument set for update_watchlist. type UpdateRequest struct { ListID string DisplayName string IconEmoji string DisplayDescription string } // ListIDRequest is a single list_id argument. type ListIDRequest struct { ListID string } // AddRequest is the argument set for add_to_watchlist. type AddRequest struct { ListID string Symbols []string CurrencyPairIDs []string IndexIDs []string } // RemoveRequest is the argument set for remove_from_watchlist. type RemoveRequest struct { ListID string Symbols []string CurrencyPairIDs []string IndexIDs []string } // OptionMutateRequest is the argument set for add/remove option watchlist tools. type OptionMutateRequest struct { OptionIDs []string PositionType string } // Lists calls get_watchlists. func (c *Client) Lists(ctx context.Context, req ListsRequest) (ListsResult, error) { raw, err := c.c.Call(ctx, toolLists, map[string]any{}) if err != nil { return ListsResult{}, err } data := wire.Unwrap(raw) var wrap struct { Watchlists []watchlistJSON `json:"watchlists"` } if err := json.Unmarshal(data, &wrap); err != nil { var list []watchlistJSON if err := json.Unmarshal(data, &list); err != nil { return ListsResult{}, client.ToolErrorf(toolLists, "parse: %w", err) } wrap.Watchlists = list } out := make([]Watchlist, 0, len(wrap.Watchlists)) for _, w := range wrap.Watchlists { title := w.Title if title == "" { title = w.Name } out = append(out, Watchlist{ID: w.ID, Title: title}) } return ListsResult{Watchlists: out}, nil } // Items calls get_watchlist_items. Every object_type is kept. func (c *Client) Items(ctx context.Context, req ItemsRequest) (ItemsResult, error) { raw, err := c.c.Call(ctx, toolItems, listIDArgs(req.ListID)) if err != nil { return ItemsResult{}, err } data := wire.Unwrap(raw) var wrap struct { Items []itemJSON `json:"items"` } if err := json.Unmarshal(data, &wrap); err != nil { var list []itemJSON if err := json.Unmarshal(data, &list); err != nil { return ItemsResult{}, client.ToolErrorf(toolItems, "parse: %w", err) } wrap.Items = list } out := make([]Item, 0, len(wrap.Items)) for _, it := range wrap.Items { sym := it.Symbol typ := it.ObjectType var inst *Instrument if it.Instrument != nil { inst = &Instrument{Symbol: it.Instrument.Symbol, Type: it.Instrument.Type} if typ == "" { typ = it.Instrument.Type } if sym == "" { sym = it.Instrument.Symbol } } out = append(out, Item{Symbol: sym, ObjectType: typ, Instrument: inst}) } return ItemsResult{Items: out}, nil } // OptionList calls get_option_watchlist. func (c *Client) OptionList(ctx context.Context, req OptionListRequest) (OptionListResult, error) { var out OptionListResult if err := c.parse(ctx, toolOptionList, map[string]any{}, &out); err != nil { return OptionListResult{}, err } return out, nil } // Popular calls get_popular_watchlists. func (c *Client) Popular(ctx context.Context, req PopularRequest) (PopularResult, error) { var out PopularResult if err := c.parse(ctx, toolPopular, map[string]any{}, &out); err != nil { return PopularResult{}, err } return out, nil } // Create calls create_watchlist. func (c *Client) Create(ctx context.Context, req CreateRequest) error { args := map[string]any{} if req.DisplayName != "" { args["display_name"] = req.DisplayName } if req.IconEmoji != "" { args["icon_emoji"] = req.IconEmoji } if req.DisplayDescription != "" { args["display_description"] = req.DisplayDescription } _, err := c.c.Call(ctx, toolCreate, args) return err } // Update calls update_watchlist. func (c *Client) Update(ctx context.Context, req UpdateRequest) error { args := listIDArgs(req.ListID) if req.DisplayName != "" { args["display_name"] = req.DisplayName } if req.IconEmoji != "" { args["icon_emoji"] = req.IconEmoji } if req.DisplayDescription != "" { args["display_description"] = req.DisplayDescription } _, err := c.c.Call(ctx, toolUpdate, args) return err } // Follow calls follow_watchlist. func (c *Client) Follow(ctx context.Context, req ListIDRequest) error { _, err := c.c.Call(ctx, toolFollow, listIDArgs(req.ListID)) return err } // Unfollow calls unfollow_watchlist. func (c *Client) Unfollow(ctx context.Context, req ListIDRequest) error { _, err := c.c.Call(ctx, toolUnfollow, listIDArgs(req.ListID)) return err } // Add calls add_to_watchlist. func (c *Client) Add(ctx context.Context, req AddRequest) error { _, err := c.c.Call(ctx, toolAdd, mutateArgs(req.ListID, req.Symbols, req.CurrencyPairIDs, req.IndexIDs)) return err } // Remove calls remove_from_watchlist. func (c *Client) Remove(ctx context.Context, req RemoveRequest) error { _, err := c.c.Call(ctx, toolRemove, mutateArgs(req.ListID, req.Symbols, req.CurrencyPairIDs, req.IndexIDs)) return err } // AddOption calls add_option_to_watchlist. func (c *Client) AddOption(ctx context.Context, req OptionMutateRequest) error { _, err := c.c.Call(ctx, toolAddOption, optionArgs(req)) return err } // RemoveOption calls remove_option_from_watchlist. func (c *Client) RemoveOption(ctx context.Context, req OptionMutateRequest) error { _, err := c.c.Call(ctx, toolRemoveOption, optionArgs(req)) return err } type watchlistJSON struct { ID string `json:"id"` Title string `json:"title"` Name string `json:"name"` } type itemJSON struct { Symbol string `json:"symbol"` ObjectType string `json:"object_type"` Instrument *struct { Symbol string `json:"symbol"` Type string `json:"type"` } `json:"instrument"` } func listIDArgs(id string) map[string]any { args := map[string]any{} if id != "" { args["list_id"] = id } return args } func mutateArgs(listID string, symbols, pairIDs, indexIDs []string) map[string]any { args := listIDArgs(listID) if len(symbols) > 0 { args["symbols"] = symbols } if len(pairIDs) > 0 { args["currency_pair_ids"] = pairIDs } if len(indexIDs) > 0 { args["index_ids"] = indexIDs } return args } func optionArgs(req OptionMutateRequest) map[string]any { args := map[string]any{} if len(req.OptionIDs) > 0 { args["option_ids"] = req.OptionIDs } if req.PositionType != "" { args["position_type"] = req.PositionType } return args } func (c *Client) parse(ctx context.Context, tool string, args map[string]any, dest any) error { raw, err := c.c.Call(ctx, tool, args) if err != nil { return err } if err := json.Unmarshal(wire.Unwrap(raw), dest); err != nil { return client.ToolErrorf(tool, "parse: %w", err) } return nil }