Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,10 @@ mcp-monarch
cmd/mcp-server/mcp-monarch

# Test files
test_*.go
test_*.go

# MCP server binary (go build output)
cmd/mcp-server/mcp-server

# Local agent config (may contain tokens)
.codex/
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- MCP server: new `get_recurring_transactions` tool exposing recurring/upcoming transactions
(merchant, amount, frequency, next date, category, account). Accepts an optional
`startDate`/`endDate`; defaults to the next 30 days.

## [2.1.0] - 2026-09-21

### Added
Expand Down
5 changes: 5 additions & 0 deletions cmd/mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,9 @@ func registerTools(server *mcp.Server, client *monarch.Client) {
Name: "get_tags",
Description: "Get all available transaction tags.",
}, tools.GetTags)

mcp.AddTool(server, &mcp.Tool{
Name: "get_recurring_transactions",
Description: "Get recurring/upcoming transactions (subscriptions, bills, etc.), including merchant, amount, frequency, and next expected date. Defaults to the next 30 days if no date range is given.",
}, tools.GetRecurringTransactions)
}
84 changes: 84 additions & 0 deletions cmd/mcp-server/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,90 @@ func (t *monarchTools) GetTransactions(ctx context.Context, req *mcp.CallToolReq
}, nil
}

// GetRecurringTransactions tool - retrieves recurring transactions
type GetRecurringTransactionsInput struct {
StartDate string `json:"startDate,omitempty" jsonschema:"Start date in YYYY-MM-DD format (optional, defaults to today)"`
EndDate string `json:"endDate,omitempty" jsonschema:"End date in YYYY-MM-DD format (optional, defaults to one month from start date)"`
}

type RecurringTransactionEntry struct {
ID string `json:"id" jsonschema:"Recurring transaction stream ID"`
Merchant string `json:"merchant" jsonschema:"Merchant name"`
Amount float64 `json:"amount" jsonschema:"Recurring amount (negative for expenses)"`
Frequency string `json:"frequency" jsonschema:"Recurrence frequency (e.g. monthly, weekly)"`
NextDate string `json:"nextDate" jsonschema:"Next expected occurrence date"`
Category string `json:"category,omitempty" jsonschema:"Transaction category"`
Account string `json:"account,omitempty" jsonschema:"Account name"`
IsActive bool `json:"isActive" jsonschema:"Whether this recurring stream is still active"`
IsApproximate bool `json:"isApproximate" jsonschema:"Whether the amount/date is approximate"`
}

type GetRecurringTransactionsOutput struct {
RecurringTransactions []RecurringTransactionEntry `json:"recurringTransactions" jsonschema:"List of recurring transactions"`
Count int `json:"count" jsonschema:"Number of recurring transactions returned"`
}

func (t *monarchTools) GetRecurringTransactions(ctx context.Context, req *mcp.CallToolRequest, input GetRecurringTransactionsInput) (*mcp.CallToolResult, GetRecurringTransactionsOutput, error) {
var (
transactions []*monarch.RecurringTransaction
err error
)

if input.StartDate != "" {
startDate, parseErr := time.Parse("2006-01-02", input.StartDate)
if parseErr != nil {
return nil, GetRecurringTransactionsOutput{}, fmt.Errorf("invalid startDate format (expected YYYY-MM-DD): %w", parseErr)
}

endDate := startDate.AddDate(0, 1, 0)
if input.EndDate != "" {
endDate, err = time.Parse("2006-01-02", input.EndDate)
if err != nil {
return nil, GetRecurringTransactionsOutput{}, fmt.Errorf("invalid endDate format (expected YYYY-MM-DD): %w", err)
}
}

transactions, err = t.client.Recurring.ListWithDateRange(ctx, startDate, endDate)
} else {
transactions, err = t.client.Recurring.List(ctx)
}

if err != nil {
return nil, GetRecurringTransactionsOutput{}, fmt.Errorf("failed to fetch recurring transactions: %w", err)
}

var entries []RecurringTransactionEntry
for _, rt := range transactions {
entry := RecurringTransactionEntry{
ID: rt.ID,
Amount: rt.Amount,
Frequency: rt.Frequency,
NextDate: rt.NextDate.Time.Format("2006-01-02"),
IsActive: rt.IsActive,
IsApproximate: rt.IsApproximate,
}

if rt.Merchant != nil {
entry.Merchant = rt.Merchant.Name
}

if rt.Category != nil {
entry.Category = rt.Category.Name
}

if rt.Account != nil {
entry.Account = rt.Account.DisplayName
}

entries = append(entries, entry)
}

return nil, GetRecurringTransactionsOutput{
RecurringTransactions: entries,
Count: len(entries),
}, nil
}

// GetAccounts tool - retrieves all accounts
type GetAccountsInput struct {
// No input parameters needed
Expand Down
30 changes: 30 additions & 0 deletions cmd/mcp-server/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,36 @@ func TestGetBudgetTool(t *testing.T) {
}
}

func TestGetRecurringTransactionsTool(t *testing.T) {
token := os.Getenv("MONARCH_TOKEN")
if token == "" {
t.Skip("MONARCH_TOKEN not set")
}

client, err := monarch.NewClient(&monarch.ClientOptions{
Token: token,
})
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}

tools := &monarchTools{client: client}

callResult, output, err := tools.GetRecurringTransactions(context.Background(), nil, GetRecurringTransactionsInput{})

if err != nil {
t.Fatalf("GetRecurringTransactions failed: %v", err)
}

t.Logf("✓ GetRecurringTransactions returned %d recurring transactions (callResult=%v)", output.Count, callResult)

// Pretty print first recurring transaction
if len(output.RecurringTransactions) > 0 {
jsonData, _ := json.MarshalIndent(output.RecurringTransactions[0], "", " ")
t.Logf("First recurring transaction:\n%s", string(jsonData))
}
}

func TestGetTransactionsTool(t *testing.T) {
token := os.Getenv("MONARCH_TOKEN")
if token == "" {
Expand Down
Loading