Skip to content
Draft
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
4 changes: 2 additions & 2 deletions .cursor/rules/project-conventions.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description:
globs:
alwaysApply: true
---
This rule serves as high-level documentation for how you should write code for the Maybe codebase.
This rule serves as high-level documentation for how you should write code for this codebase.

## Project Tech Stack

Expand All @@ -22,7 +22,7 @@ This rule serves as high-level documentation for how you should write code for t

## Project conventions

These conventions should be used when writing code for Maybe.
These conventions should be used when writing code for this codebase.

### Convention 1: Minimize dependencies, vanilla Rails is plenty

Expand Down
24 changes: 12 additions & 12 deletions .cursor/rules/project-design.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ This is a personal finance application built in Ruby on Rails. The primary doma

## App Modes

The Maybe app runs in two distinct "modes", dictated by `Rails.application.config.app_mode`, which can be `managed` or `self_hosted`.
This app runs in two distinct "modes", dictated by `Rails.application.config.app_mode`, which can be `managed` or `self_hosted`.

- "Managed" - in managed mode, the Maybe team operates and manages servers for users
- "Self Hosted" - in self hosted mode, users host the Maybe app on their own infrastructure, typically through Docker Compose. We have an example [docker-compose.example.yml](mdc:docker-compose.example.yml) file that runs [Dockerfile](mdc:Dockerfile) for this mode.
- "Managed" - in managed mode, a team operates and manages servers for users
- "Self Hosted" - in self hosted mode, users host the app on their own infrastructure, typically through Docker Compose. We have an example [docker-compose.example.yml](mdc:docker-compose.example.yml) file that runs [Dockerfile](mdc:Dockerfile) for this mode.

## Families and Users

Expand All @@ -22,7 +22,7 @@ The Maybe app runs in two distinct "modes", dictated by `Rails.application.confi

## Currency Preference

Each `Family` selects a currency preference. This becomes the "main" currency in which all records are "normalized" to via [exchange_rate.rb](mdc:app/models/exchange_rate.rb) records so that the Maybe app can calculate metrics, historical graphs, and other insights in a single family currency.
Each `Family` selects a currency preference. This becomes the "main" currency in which all records are "normalized" to via [exchange_rate.rb](mdc:app/models/exchange_rate.rb) records so that the app can calculate metrics, historical graphs, and other insights in a single family currency.

## Accounts

Expand Down Expand Up @@ -77,7 +77,7 @@ There are 3 entry types, defined as [entryable.rb](mdc:app/models/entryable.rb)

### Account Transfers

A [transfer.rb](mdc:app/models/transfer.rb) represents a movement of money between two accounts. A transfer has an inflow [transaction.rb](mdc:app/models/transaction.rb) and an outflow [transaction.rb](mdc:app/models/transaction.rb). The Maybe system auto-matches transfers based on the following criteria:
A [transfer.rb](mdc:app/models/transfer.rb) represents a movement of money between two accounts. A transfer has an inflow [transaction.rb](mdc:app/models/transaction.rb) and an outflow [transaction.rb](mdc:app/models/transaction.rb). The system auto-matches transfers based on the following criteria:

- Must be from different accounts
- Must be within 4 days of each other
Expand All @@ -93,16 +93,16 @@ Regular transfers are typically _excluded_ from income and expense calculations

## Plaid Items

A [plaid_item.rb](mdc:app/models/plaid_item.rb) represents a "connection" maintained by our external data provider, Plaid in the "hosted" mode of the app. An "Item" has 1 or more [plaid_account.rb](mdc:app/models/plaid_account.rb) records, which are each associated 1:1 with an internal Maybe [account.rb](mdc:app/models/account.rb).
A [plaid_item.rb](mdc:app/models/plaid_item.rb) represents a "connection" maintained by our external data provider, Plaid in the "hosted" mode of the app. An "Item" has 1 or more [plaid_account.rb](mdc:app/models/plaid_account.rb) records, which are each associated 1:1 with an internal [account.rb](mdc:app/models/account.rb).

All relevant metadata about the item and its underlying accounts are stored on [plaid_item.rb](mdc:app/models/plaid_item.rb) and [plaid_account.rb](mdc:app/models/plaid_account.rb), while the "normalized" data is then stored on internal Maybe domain models.
All relevant metadata about the item and its underlying accounts are stored on [plaid_item.rb](mdc:app/models/plaid_item.rb) and [plaid_account.rb](mdc:app/models/plaid_account.rb), while the "normalized" data is then stored on internal domain models.

## "Syncs"

The Maybe app has the concept of a [syncable.rb](mdc:app/models/concerns/syncable.rb), which represents any model which can have its data "synced" in the background. "Syncables" include:
The app has the concept of a [syncable.rb](mdc:app/models/concerns/syncable.rb), which represents any model which can have its data "synced" in the background. "Syncables" include:

- `Account` - an account "sync" will sync account holdings, balances, and enhance transaction metadata
- `PlaidItem` - a Plaid Item "sync" fetches data from Plaid APIs, normalizes that data, stores it on internal Maybe models, and then finally performs an "Account sync" for each of the underlying accounts created from the Plaid Item.
- `PlaidItem` - a Plaid Item "sync" fetches data from Plaid APIs, normalizes that data, stores it on internal models, and then finally performs an "Account sync" for each of the underlying accounts created from the Plaid Item.
- `Family` - a Family "sync" loops through the family's Plaid Items and individual Accounts and "syncs" each of them. A family is synced once per day, automatically through [auto_sync.rb](mdc:app/controllers/concerns/auto_sync.rb).

Each "sync" creates a [sync.rb](mdc:app/models/sync.rb) record in the database, which keeps track of the status of the sync, any errors that it encounters, and acts as an "audit table" for synced data.
Expand All @@ -126,19 +126,19 @@ A Plaid Item sync is an ETL (extract, transform, load) operation:

1. [plaid_item.rb](mdc:app/models/plaid_item.rb) fetches data from the external Plaid API
2. [plaid_item.rb](mdc:app/models/plaid_item.rb) creates and loads this data to [plaid_account.rb](mdc:app/models/plaid_account.rb) records
3. [plaid_item.rb](mdc:app/models/plaid_item.rb) and [plaid_account.rb](mdc:app/models/plaid_account.rb) transform and load data to [account.rb](mdc:app/models/account.rb) and [entry.rb](mdc:app/models/entry.rb), the internal Maybe representations of the data.
3. [plaid_item.rb](mdc:app/models/plaid_item.rb) and [plaid_account.rb](mdc:app/models/plaid_account.rb) transform and load data to [account.rb](mdc:app/models/account.rb) and [entry.rb](mdc:app/models/entry.rb), the internal representations of the data.

### Family Syncs

A family sync happens once daily via [auto_sync.rb](mdc:app/controllers/concerns/auto_sync.rb). A family sync is an "orchestrator" of Account and Plaid Item syncs.

## Data Providers

The Maybe app utilizes several 3rd party data services to calculate historical account balances, enrich data, and more. Since the app can be run in both "hosted" and "self hosted" mode, this means that data providers are _optional_ for self hosted users and must be configured.
The app utilizes several 3rd party data services to calculate historical account balances, enrich data, and more. Since the app can be run in both "hosted" and "self hosted" mode, this means that data providers are _optional_ for self hosted users and must be configured.

Because of this optionality, data providers must be configured at _runtime_ through [registry.rb](mdc:app/models/provider/registry.rb) utilizing [setting.rb](mdc:app/models/setting.rb) for runtime parameters like API keys:

There are two types of 3rd party data in the Maybe app:
There are two types of 3rd party data in the app:

1. "Concept" data
2. One-off data
Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/testing.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description:
globs: test/**
alwaysApply: false
---
Use this rule to learn how to write tests for the Maybe codebase.
Use this rule to learn how to write tests for this codebase.

Due to the open-source nature of this project, we have chosen Minitest + Fixtures for testing to maximize familiarity and predictability.

Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ gem "after_commit_everywhere", "~> 1.0"

# AI
gem "ruby-openai"
gem "fast-mcp"
gem "langfuse-ruby", "~> 0.1.4", require: "langfuse"

group :development, :test do
Expand Down
41 changes: 41 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,35 @@ GEM
dotenv (= 3.1.8)
railties (>= 6.1)
drb (2.2.3)
dry-configurable (1.3.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-core (1.1.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.2.0)
dry-initializer (3.2.0)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-schema (1.14.1)
concurrent-ruby (~> 1.0)
dry-configurable (~> 1.0, >= 1.0.1)
dry-core (~> 1.1)
dry-initializer (~> 3.2)
dry-logic (~> 1.5)
dry-types (~> 1.8)
zeitwerk (~> 2.6)
dry-types (1.8.3)
bigdecimal (~> 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
erb (5.0.1)
erb_lint (0.9.0)
activesupport
Expand All @@ -206,6 +235,13 @@ GEM
net-http (>= 0.5.0)
faraday-retry (2.3.2)
faraday (~> 2.0)
fast-mcp (1.5.0)
addressable (~> 2.8)
base64
dry-schema (~> 1.14)
json (~> 2.0)
mime-types (~> 3.4)
rack (~> 3.1)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
Expand Down Expand Up @@ -326,6 +362,10 @@ GEM
matrix (0.4.2)
memory_profiler (1.1.0)
method_source (1.1.0)
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0729)
mini_histogram (0.3.1)
mini_magick (5.2.0)
benchmark
Expand Down Expand Up @@ -645,6 +685,7 @@ DEPENDENCIES
faraday
faraday-multipart
faraday-retry
fast-mcp
foreman
hotwire-livereload
hotwire_combobox
Expand Down
5 changes: 5 additions & 0 deletions app/resources/application_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class ApplicationResource < ActionResource::Base
# write your custom logic to be shared across all resources here
end
5 changes: 5 additions & 0 deletions app/tools/application_tool.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class ApplicationTool < ActionTool::Base
# write your custom logic to be shared across all tools here
end
27 changes: 27 additions & 0 deletions app/tools/fetch_accounts_balance_tool.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

class FetchAccountsBalanceTool < ApplicationTool
def call(account_type: nil)
family = Current.family
return { error: "No family found" } unless family

accounts = family.accounts
accounts = accounts.where(accountable_type: account_type) if account_type.present?

account_data = accounts.map do |account|
{
id: account.id,
name: account.name,
type: account.accountable_type,
balance: account.balance.to_f
}
end

total_balance = accounts.sum(&:balance).to_f

{
accounts: account_data,
total_balance: total_balance
}
end
end
43 changes: 43 additions & 0 deletions config/initializers/fast_mcp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

# FastMcp - Model Context Protocol for Rails
# This initializer sets up the MCP middleware in your Rails application.
#
# In Rails applications, you can use:
# - ActionTool::Base as an alias for FastMcp::Tool
# - ActionResource::Base as an alias for FastMcp::Resource
#
# All your tools should inherit from ApplicationTool which already uses ActionTool::Base,
# and all your resources should inherit from ApplicationResource which uses ActionResource::Base.

# Mount the MCP middleware in your Rails application
# You can customize the options below to fit your needs.
require "fast_mcp"

FastMcp.mount_in_rails(
Rails.application,
allowed_origins: Rails.application.config.hosts.to_a,
name: Rails.application.class.module_parent_name.underscore.dasherize,
version: "1.0.0",
path_prefix: "/mcp", # This is the default path prefix
messages_route: "messages", # This is the default route for the messages endpoint
sse_route: "sse" # This is the default route for the SSE endpoint
# Add allowed origins below, it defaults to Rails.application.config.hosts
# allowed_origins: ['localhost', '127.0.0.1', '[::1]', 'example.com', /.*\.example\.com/],
# localhost_only: true, # Set to false to allow connections from other hosts
# whitelist specific ips to if you want to run on localhost and allow connections from other IPs
# allowed_ips: ['127.0.0.1', '::1']
# authenticate: true, # Uncomment to enable authentication
# auth_token: 'your-token', # Required if authenticate: true
) do |server|
Rails.application.config.after_initialize do
# FastMcp will automatically discover and register:
# - All classes that inherit from ApplicationTool (which uses ActionTool::Base)
# - All classes that inherit from ApplicationResource (which uses ActionResource::Base)
server.register_tools(*ApplicationTool.descendants)
server.register_resources(*ApplicationResource.descendants)
Comment on lines +37 to +38

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In the development environment, Rails lazy-loads classes, so ApplicationTool.descendants and ApplicationResource.descendants will likely be empty when this initializer runs. This will prevent your tools and resources from being registered. You should explicitly eager-load these directories to ensure all tool and resource classes are loaded before registration.

    # Eager-load tools and resources to ensure they are discovered, especially in development.
    Rails.autoloaders.main.eager_load_dir(Rails.root.join("app/tools"))
    Rails.autoloaders.main.eager_load_dir(Rails.root.join("app/resources"))

    server.register_tools(*ApplicationTool.descendants)
    server.register_resources(*ApplicationResource.descendants)

# alternatively, you can register tools and resources manually:
# server.register_tool(MyTool)
# server.register_resource(MyResource)
end
end
Loading