Files
robinhood-agentic-mcp/scanner/scanner.go
T

294 lines
7.4 KiB
Go

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{}
// FilterSpecsResult is the parsed get_scanner_filter_specs payload.
type FilterSpecsResult struct{}
// DatapointsRequest is the argument set for get_scanner_datapoints (none).
type DatapointsRequest struct{}
// DatapointsResult is the parsed get_scanner_datapoints payload.
type DatapointsResult struct{}
// 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
}
// CreateResult is the parsed create_scan payload.
type CreateResult struct{}
// 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{}
// RunRequest is the argument set for run_scan.
type RunRequest struct {
ScanID string
}
// RunResult is the parsed run_scan payload.
type RunResult struct{}
// 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{}
// 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{}
// 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
}