diff --git a/client/caller.go b/client/caller.go new file mode 100644 index 0000000..b910bf1 --- /dev/null +++ b/client/caller.go @@ -0,0 +1,18 @@ +package client + +import ( + "context" + "encoding/json" +) + +// Caller invokes an MCP tool by name and returns its JSON result. +type Caller interface { + Call(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) +} + +// Func adapts a function to Caller so tests can inject a stub. +type Func func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) + +func (f Func) Call(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) { + return f(ctx, name, args) +} diff --git a/client/enums.go b/client/enums.go new file mode 100644 index 0000000..932f968 --- /dev/null +++ b/client/enums.go @@ -0,0 +1,41 @@ +package client + +// Side is an order side. Values are Robinhood wire strings. +type Side string + +const ( + Buy Side = "buy" + Sell Side = "sell" +) + +// OrderType is an order type. Values are Robinhood wire strings. +type OrderType string + +const ( + Market OrderType = "market" + Limit OrderType = "limit" + Stop OrderType = "stop_market" // equity + options; crypto uses StopLoss + StopLimit OrderType = "stop_limit" + StopLoss OrderType = "stop_loss" // crypto only +) + +// TimeInForce is an order duration. Values are Robinhood wire strings. +type TimeInForce string + +const ( + GFD TimeInForce = "gfd" + GTC TimeInForce = "gtc" + GFW TimeInForce = "gfw" // crypto + GFM TimeInForce = "gfm" // crypto +) + +// MarketHours is a trading-session window. Values are Robinhood wire strings. +type MarketHours string + +const ( + RegularHours MarketHours = "regular_hours" + ExtendedHours MarketHours = "extended_hours" + AllDayHours MarketHours = "all_day_hours" + RegularCurbHours MarketHours = "regular_curb_hours" + RegularCurbOvernightHours MarketHours = "regular_curb_overnight_hours" +) diff --git a/client/enums_test.go b/client/enums_test.go new file mode 100644 index 0000000..95db7be --- /dev/null +++ b/client/enums_test.go @@ -0,0 +1,17 @@ +package client_test + +import ( + "testing" + + "s1d3sw1ped/robinhood-agentic-mcp/client" +) + +func TestEnums_wireValues(t *testing.T) { + t.Parallel() + if client.Buy != "buy" || client.Stop != "stop_market" || client.StopLoss != "stop_loss" { + t.Fatal("side/type") + } + if client.GFD != "gfd" || client.RegularHours != "regular_hours" { + t.Fatal("tif/hours") + } +} diff --git a/client/error.go b/client/error.go new file mode 100644 index 0000000..a6d59a5 --- /dev/null +++ b/client/error.go @@ -0,0 +1,28 @@ +package client + +import "fmt" + +// ToolError is a failed MCP tool call or a parse of its result. +type ToolError struct { + Name string // MCP tool name + Message string + Err error // transport/SDK/parse cause; may be nil +} + +func (e *ToolError) Error() string { + return "mcp " + e.Name + ": " + e.Message +} + +func (e *ToolError) Unwrap() error { + return e.Err +} + +// ToolErrorf builds a ToolError. format may use %w so Unwrap works. +func ToolErrorf(name, format string, args ...any) *ToolError { + err := fmt.Errorf(format, args...) + return &ToolError{ + Name: name, + Message: err.Error(), + Err: err, + } +} diff --git a/client/error_test.go b/client/error_test.go new file mode 100644 index 0000000..69f0b06 --- /dev/null +++ b/client/error_test.go @@ -0,0 +1,24 @@ +package client_test + +import ( + "errors" + "testing" + + "s1d3sw1ped/robinhood-agentic-mcp/client" +) + +func TestToolError_formatAndAs(t *testing.T) { + t.Parallel() + inner := errors.New("boom") + err := client.ToolErrorf("get_accounts", "parse quotes: %w", inner) + if err.Error() != "mcp get_accounts: parse quotes: boom" { + t.Fatalf("%q", err.Error()) + } + var te *client.ToolError + if !errors.As(err, &te) || te.Name != "get_accounts" { + t.Fatalf("%v", err) + } + if !errors.Is(err, inner) { + t.Fatal("unwrap") + } +}