package scanner import ( "context" "encoding/json" "s1d3sw1ped/robinhood-agentic-mcp/client" "s1d3sw1ped/robinhood-agentic-mcp/internal/wire" ) const ( toolFilterSpecs = "get_scanner_filter_specs" toolDatapoints = "get_scanner_datapoints" toolScans = "get_scans" toolCreate = "create_scan" toolPreview = "preview_scan" toolRun = "run_scan" toolUpdateFilters = "update_scan_filters" toolUpdateConfig = "update_scan_config" ) // Filter is one scanner filter (enum-based or expression-based). type Filter struct { FilterType string Predicate string Values []string Interval string Length int Plot string Expression string DisplayTitle string } // Column is one extra result column on a scan. type Column struct { DisplayName string Expression string Visible *bool Order *int } // FilterSpecsRequest is the argument set for get_scanner_filter_specs (none). type FilterSpecsRequest struct{} // FilterSpec is one filter type from get_scanner_filter_specs. type FilterSpec struct { FilterType string `json:"filter_type"` Name string `json:"name"` } // FilterSpecsResult is the parsed get_scanner_filter_specs payload. type FilterSpecsResult struct { Specs []FilterSpec } // DatapointsRequest is the argument set for get_scanner_datapoints (none). type DatapointsRequest struct{} // Datapoint is one expression token from get_scanner_datapoints. type Datapoint struct { Name string `json:"name"` } // DatapointsResult is the parsed get_scanner_datapoints payload. type DatapointsResult struct { Datapoints []Datapoint } // ScansRequest is the argument set for get_scans (none). type ScansRequest struct{} // Scan is one saved scanner from get_scans. type Scan struct { ID string `json:"id"` Title string `json:"title"` } // ScansResult is the parsed get_scans payload. type ScansResult struct { Scans []Scan `json:"scans"` } // CreateRequest is the argument set for create_scan. type CreateRequest struct { ScanID string Preset string Filters []Filter Columns []Column Title string } // ScanResult is a saved scan plus optional live rows. type ScanResult struct { ID string Title string Rows []ScanRow Total int } // ScanRow is one live scanner match. type ScanRow struct { Symbol string InstrumentID string } // CreateResult is the parsed create_scan payload. type CreateResult struct { ScanResult } // PreviewRequest is the argument set for preview_scan. type PreviewRequest struct { Filters []Filter Columns []Column } // PreviewResult is the parsed preview_scan payload. type PreviewResult struct { ScanResult } // RunRequest is the argument set for run_scan. type RunRequest struct { ScanID string } // RunResult is the parsed run_scan payload. type RunResult struct { ScanResult } // UpdateFiltersRequest is the argument set for update_scan_filters. type UpdateFiltersRequest struct { ScanID string Filters []Filter } // UpdateFiltersResult is the parsed update_scan_filters payload. type UpdateFiltersResult struct { ScanResult } // UpdateConfigRequest is the argument set for update_scan_config. type UpdateConfigRequest struct { ScanID string SortingColumn string SortingDirection string Columns []Column } // UpdateConfigResult is the parsed update_scan_config payload. type UpdateConfigResult struct { ScanResult } // FilterSpecs calls get_scanner_filter_specs. func (c *Client) FilterSpecs(ctx context.Context, req FilterSpecsRequest) (FilterSpecsResult, error) { var out FilterSpecsResult if err := c.parse(ctx, toolFilterSpecs, map[string]any{}, &out); err != nil { return FilterSpecsResult{}, err } return out, nil } // Datapoints calls get_scanner_datapoints. func (c *Client) Datapoints(ctx context.Context, req DatapointsRequest) (DatapointsResult, error) { var out DatapointsResult if err := c.parse(ctx, toolDatapoints, map[string]any{}, &out); err != nil { return DatapointsResult{}, err } return out, nil } // Scans calls get_scans. func (c *Client) Scans(ctx context.Context, req ScansRequest) (ScansResult, error) { var out ScansResult if err := c.parse(ctx, toolScans, map[string]any{}, &out); err != nil { return ScansResult{}, err } return out, nil } // Create calls create_scan. func (c *Client) Create(ctx context.Context, req CreateRequest) (CreateResult, error) { args := map[string]any{} if req.ScanID != "" { args["scan_id"] = req.ScanID } if req.Preset != "" { args["preset"] = req.Preset } if len(req.Filters) > 0 { args["filters"] = encodeFilters(req.Filters) } if len(req.Columns) > 0 { args["columns"] = encodeColumns(req.Columns) } if req.Title != "" { args["title"] = req.Title } var out CreateResult if err := c.parse(ctx, toolCreate, args, &out); err != nil { return CreateResult{}, err } return out, nil } // Preview calls preview_scan. func (c *Client) Preview(ctx context.Context, req PreviewRequest) (PreviewResult, error) { args := map[string]any{} if len(req.Filters) > 0 { args["filters"] = encodeFilters(req.Filters) } if len(req.Columns) > 0 { args["columns"] = encodeColumns(req.Columns) } var out PreviewResult if err := c.parse(ctx, toolPreview, args, &out); err != nil { return PreviewResult{}, err } return out, nil } // Run calls run_scan. scan_id is always sent (required). func (c *Client) Run(ctx context.Context, req RunRequest) (RunResult, error) { var out RunResult if err := c.parse(ctx, toolRun, map[string]any{"scan_id": req.ScanID}, &out); err != nil { return RunResult{}, err } return out, nil } // UpdateFilters calls update_scan_filters. scan_id and filters are always sent (required; empty filters clears). func (c *Client) UpdateFilters(ctx context.Context, req UpdateFiltersRequest) (UpdateFiltersResult, error) { filters := req.Filters if filters == nil { filters = []Filter{} } args := map[string]any{ "scan_id": req.ScanID, "filters": encodeFilters(filters), } var out UpdateFiltersResult if err := c.parse(ctx, toolUpdateFilters, args, &out); err != nil { return UpdateFiltersResult{}, err } return out, nil } // UpdateConfig calls update_scan_config. scan_id is always sent (required). func (c *Client) UpdateConfig(ctx context.Context, req UpdateConfigRequest) (UpdateConfigResult, error) { args := map[string]any{"scan_id": req.ScanID} if req.SortingColumn != "" { args["sorting_column"] = req.SortingColumn } if req.SortingDirection != "" { args["sorting_direction"] = req.SortingDirection } if len(req.Columns) > 0 { args["columns"] = encodeColumns(req.Columns) } var out UpdateConfigResult if err := c.parse(ctx, toolUpdateConfig, args, &out); err != nil { return UpdateConfigResult{}, err } return out, nil } func encodeFilters(filters []Filter) []map[string]any { out := make([]map[string]any, len(filters)) for i, f := range filters { m := map[string]any{} if f.FilterType != "" { m["filter_type"] = f.FilterType } if f.Predicate != "" { m["predicate"] = f.Predicate } if len(f.Values) > 0 { m["values"] = f.Values } if f.Interval != "" { m["interval"] = f.Interval } if f.Length != 0 { m["length"] = f.Length } if f.Plot != "" { m["plot"] = f.Plot } if f.Expression != "" { m["expression"] = f.Expression } if f.DisplayTitle != "" { m["display_title"] = f.DisplayTitle } out[i] = m } return out } func encodeColumns(cols []Column) []map[string]any { out := make([]map[string]any, len(cols)) for i, col := range cols { m := map[string]any{} if col.DisplayName != "" { m["display_name"] = col.DisplayName } if col.Expression != "" { m["expression"] = col.Expression } if col.Visible != nil { m["visible"] = *col.Visible } if col.Order != nil { m["order"] = *col.Order } out[i] = m } return out } 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 }