feat: add scanner MCP methods
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user