diff --git a/.cursor/rules/project-conventions.mdc b/.cursor/rules/project-conventions.mdc index 835451c28a8..c660b05bfb7 100644 --- a/.cursor/rules/project-conventions.mdc +++ b/.cursor/rules/project-conventions.mdc @@ -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 @@ -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 diff --git a/.cursor/rules/project-design.mdc b/.cursor/rules/project-design.mdc index 3c4ea34710b..395f32ca920 100644 --- a/.cursor/rules/project-design.mdc +++ b/.cursor/rules/project-design.mdc @@ -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 @@ -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 @@ -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 @@ -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. @@ -126,7 +126,7 @@ 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 @@ -134,11 +134,11 @@ A family sync happens once daily via [auto_sync.rb](mdc:app/controllers/concerns ## 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 diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc index 9fc95f82fb5..7431dcbbcb4 100644 --- a/.cursor/rules/testing.mdc +++ b/.cursor/rules/testing.mdc @@ -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. diff --git a/Gemfile b/Gemfile index 6cf4eebe595..ff557007656 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 7edacc99fb2..e411ad23cbc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 @@ -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) @@ -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 @@ -645,6 +685,7 @@ DEPENDENCIES faraday faraday-multipart faraday-retry + fast-mcp foreman hotwire-livereload hotwire_combobox diff --git a/app/resources/application_resource.rb b/app/resources/application_resource.rb new file mode 100644 index 00000000000..7b30eb4d281 --- /dev/null +++ b/app/resources/application_resource.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationResource < ActionResource::Base + # write your custom logic to be shared across all resources here +end diff --git a/app/tools/application_tool.rb b/app/tools/application_tool.rb new file mode 100644 index 00000000000..7d848641b2e --- /dev/null +++ b/app/tools/application_tool.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationTool < ActionTool::Base + # write your custom logic to be shared across all tools here +end diff --git a/app/tools/fetch_accounts_balance_tool.rb b/app/tools/fetch_accounts_balance_tool.rb new file mode 100644 index 00000000000..01ed314f7a5 --- /dev/null +++ b/app/tools/fetch_accounts_balance_tool.rb @@ -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 diff --git a/config/initializers/fast_mcp.rb b/config/initializers/fast_mcp.rb new file mode 100644 index 00000000000..6e2b1b2d475 --- /dev/null +++ b/config/initializers/fast_mcp.rb @@ -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) + # alternatively, you can register tools and resources manually: + # server.register_tool(MyTool) + # server.register_resource(MyResource) + end +end diff --git a/docs/api/MCP-plan.md b/docs/api/MCP-plan.md new file mode 100644 index 00000000000..e8a372c46f3 --- /dev/null +++ b/docs/api/MCP-plan.md @@ -0,0 +1,395 @@ +# MCP (Model Context Protocol) Plan + +This document outlines the Claude Code implementation plan for MCP (Model Context Protocol) in the Sure Finance application using the fast-mcp gem. MCP enables AI models to interact with Rails applications through a standardized protocol for tools and resources. + +## Overview + +The Model Context Protocol (MCP) provides a standardized way for AI models to interact with our application by: +- Exposing tools that AI can call to perform actions +- Providing resources that AI can query for information +- Supporting multiple transport protocols (STDIO, HTTP, SSE) + +## Architecture + +### Integration Approach + +We use the Rack Middleware approach for MCP integration, which: +- Embeds MCP directly in the Rails application +- Simplifies deployment (no separate process management) +- Enables easy resource sharing with the Rails app +- Provides HTTP/SSE endpoints for AI interaction + +### Directory Structure + +``` +app/ +├── tools/ # MCP tool definitions +├── resources/ # MCP resource definitions +└── mcp/ # MCP-specific configurations and helpers + +config/ +└── initializers/ + └── fast_mcp.rb # MCP server configuration +``` + +## Implementation Plan + +### Phase 1: Basic Setup + +1. **Install Dependencies** + ```ruby + # Gemfile + gem "fast-mcp" + ``` + ```bash + bundle install + bin/rails generate fast_mcp:install + ``` + +2. **Configure MCP Server** + ```ruby + # config/initializers/fast_mcp.rb + FastMcp.configure do |config| + config.server_name = "Sure Finance MCP" + config.server_version = "1.0.0" + config.description = "AI-powered financial management tools" + + # Enable authentication + config.authenticate = true + config.auth_strategy = :api_key + + # Configure transport + config.transport = :http + config.mount_path = "/mcp" + end + ``` + +### Phase 2: Financial Tools Implementation + +#### Account Management Tools + +1. **FetchAccountsBalanceTool** + ```ruby + # app/tools/fetch_accounts_balance_tool.rb + class FetchAccountsBalanceTool < FastMcp::Tool + description "Fetch current balances for all accounts or specific account" + + arguments do + optional(:account_id).filled(:string) + optional(:account_type).filled(:string) + end + + def call(account_id: nil, account_type: nil) + scope = Current.family.accounts + scope = scope.where(id: account_id) if account_id + scope = scope.where(accountable_type: account_type) if account_type + + accounts = scope.includes(:accountable).map do |account| + { + id: account.id, + name: account.name, + type: account.accountable_type, + balance: account.balance.to_f, + currency: account.currency + } + end + + { accounts: accounts, total_balance: accounts.sum { |a| a[:balance] } } + end + end + ``` + +2. **AnalyzeSpendingTool** + ```ruby + # app/tools/analyze_spending_tool.rb + class AnalyzeSpendingTool < FastMcp::Tool + description "Analyze spending patterns for a given time period" + + arguments do + optional(:period).filled(:string).value(included_in?: %w[day week month year]) + optional(:category_id).filled(:string) + end + + def call(period: "month", category_id: nil) + transactions = Current.family.transactions + .expense + .where(date: period_range(period)) + + transactions = transactions.where(category_id: category_id) if category_id + + by_category = transactions.joins(:category) + .group("categories.name") + .sum(:amount) + + { + total_spent: transactions.sum(:amount).to_f, + by_category: by_category.transform_values(&:to_f), + transaction_count: transactions.count, + average_transaction: (transactions.average(:amount) || 0).to_f + } + end + + private + + def period_range(period) + case period + when "day" then Date.current.beginning_of_day..Date.current.end_of_day + when "week" then Date.current.beginning_of_week..Date.current.end_of_week + when "month" then Date.current.beginning_of_month..Date.current.end_of_month + when "year" then Date.current.beginning_of_year..Date.current.end_of_year + end + end + end + ``` + +3. **CreateTransactionTool** + ```ruby + # app/tools/create_transaction_tool.rb + class CreateTransactionTool < FastMcp::Tool + description "Create a new transaction" + + arguments do + required(:account_id).filled(:string) + required(:amount).filled(:float) + required(:name).filled(:string) + required(:date).filled(:string) + optional(:category_id).filled(:string) + optional(:notes).filled(:string) + end + + def call(account_id:, amount:, name:, date:, category_id: nil, notes: nil) + account = Current.family.accounts.find(account_id) + + transaction = account.transactions.create!( + amount: amount, + name: name, + date: Date.parse(date), + category_id: category_id, + notes: notes + ) + + { + success: true, + transaction: { + id: transaction.id, + name: transaction.name, + amount: transaction.amount.to_f, + date: transaction.date.to_s + } + } + rescue => e + { success: false, error: e.message } + end + end + ``` + +### Phase 3: Financial Resources Implementation + +1. **AccountSummaryResource** + ```ruby + # app/resources/account_summary_resource.rb + class AccountSummaryResource < FastMcp::Resource + uri "finance/account-summary" + resource_name "Account Summary" + description "Current financial overview including all accounts and balances" + mime_type "application/json" + + def content + summary = { + total_assets: Current.family.assets.sum(:balance).to_f, + total_liabilities: Current.family.liabilities.sum(:balance).to_f, + net_worth: Current.family.net_worth.to_f, + accounts_by_type: Current.family.accounts + .group(:accountable_type) + .sum(:balance) + .transform_values(&:to_f), + last_updated: Time.current.iso8601 + } + + JSON.generate(summary) + end + end + ``` + +2. **BudgetStatusResource** + ```ruby + # app/resources/budget_status_resource.rb + class BudgetStatusResource < FastMcp::Resource + uri "finance/budget-status" + resource_name "Budget Status" + description "Current month budget status and spending" + mime_type "application/json" + + def content + # Implement budget tracking logic + budget_data = { + month: Date.current.strftime("%B %Y"), + categories: calculate_budget_by_category, + total_budgeted: 5000.0, # Example + total_spent: calculate_total_spent, + remaining: calculate_remaining_budget + } + + JSON.generate(budget_data) + end + + private + + def calculate_budget_by_category + # Implementation details + end + + def calculate_total_spent + Current.family.transactions + .expense + .where(date: Date.current.beginning_of_month..Date.current.end_of_month) + .sum(:amount).to_f + end + + def calculate_remaining_budget + # Implementation details + end + end + ``` + +### Phase 4: Server Registration + +```ruby +# config/initializers/fast_mcp.rb (extended) +Rails.application.config.after_initialize do + # Register tools + FastMcp.server.register_tool(FetchAccountsBalanceTool) + FastMcp.server.register_tool(AnalyzeSpendingTool) + FastMcp.server.register_tool(CreateTransactionTool) + + # Register resources + FastMcp.server.register_resource(AccountSummaryResource) + FastMcp.server.register_resource(BudgetStatusResource) + + # Mount middleware + Rails.application.routes.draw do + mount FastMcp.server => "/mcp" + end +end +``` + +## Usage Examples + +### Using with Claude Desktop + +1. Configure Claude Desktop's MCP settings: + ```json + { + "mcpServers": { + "sure-finance": { + "url": "http://localhost:3000/mcp", + "apiKey": "your-api-key-here" + } + } + } + ``` + +2. Example interactions: + ``` + User: "What's my current account balance?" + Claude: [Uses FetchAccountsBalanceTool to retrieve balances] + + User: "How much did I spend on groceries this month?" + Claude: [Uses AnalyzeSpendingTool with category filter] + + User: "Add a transaction for $45.99 at the grocery store" + Claude: [Uses CreateTransactionTool to create the transaction] + ``` + +### Using with API Clients + +```ruby +# Example Ruby client +require 'faraday' +require 'json' + +client = Faraday.new(url: 'http://localhost:3000/mcp') do |f| + f.headers['Authorization'] = "Bearer #{api_key}" + f.headers['Content-Type'] = 'application/json' +end + +# Call a tool +response = client.post('/tools/execute') do |req| + req.body = { + tool: 'FetchAccountsBalanceTool', + arguments: { account_type: 'Checking' } + }.to_json +end + +result = JSON.parse(response.body) +``` + +## Security Considerations + +1. **Authentication**: All MCP endpoints require authentication via API key +2. **Authorization**: Tools respect Current.user and Current.family context +3. **Input Validation**: Dry-Schema validates all tool arguments +4. **Rate Limiting**: Inherits from application's Rack::Attack configuration + +## Testing + +```ruby +# test/mcp/tools/fetch_accounts_balance_tool_test.rb +require "test_helper" + +class FetchAccountsBalanceToolTest < ActiveSupport::TestCase + setup do + @user = users(:john) + @family = @user.family + Current.user = @user + Current.family = @family + end + + test "fetches all account balances" do + tool = FetchAccountsBalanceTool.new + result = tool.call + + assert_equal @family.accounts.count, result[:accounts].size + assert result[:total_balance].positive? + end + + test "filters by account type" do + tool = FetchAccountsBalanceTool.new + result = tool.call(account_type: "Checking") + + assert result[:accounts].all? { |a| a[:type] == "Checking" } + end +end +``` + +## Monitoring and Debugging + +1. **Logging**: All MCP requests logged to Rails.logger +2. **Testing Tools**: Use MCP Inspector for debugging + ```bash + npx @modelcontextprotocol/inspector bin/rails runner "FastMcp.server.start" + ``` + +3. **Performance Monitoring**: Integrate with existing Skylight/Sentry setup + +## Future Enhancements + +1. **Advanced Tools** + - Investment portfolio analysis + - Tax calculation assistance + - Financial goal tracking + - Automated categorization + +2. **Real-time Updates** + - WebSocket support for live data + - Push notifications for AI insights + +3. **Multi-user Support** + - Family member context switching + - Permission-based tool access + +## References + +- [Fast-MCP GitHub Repository](https://github.com/yjacquin/fast-mcp) +- [Model Context Protocol Specification](https://modelcontextprotocol.io) +- [Fast-MCP Documentation](https://rubydoc.info/gems/fast-mcp) \ No newline at end of file diff --git a/docs/api/MCP.md b/docs/api/MCP.md new file mode 100644 index 00000000000..289b3336f21 --- /dev/null +++ b/docs/api/MCP.md @@ -0,0 +1,32 @@ +# Model Context Protocol (MCP) + +This application ships with [fast-mcp](https://github.com/yjacquin/fast-mcp), a Ruby implementation of the Model Context Protocol. The middleware is mounted at `/mcp` and exposes two endpoints: + +- `POST /mcp/messages` – JSON‑RPC endpoint for MCP requests +- `GET /mcp/sse` – Server‑sent events stream + +## Defining tools and resources + +Add tools under `app/tools/` and resources under `app/resources/`. Tools inherit from `ApplicationTool` and resources from `ApplicationResource`. + +Any subclasses of these base classes are automatically registered with the MCP server on boot. + +## Example request + +Once the Rails server is running, an MCP client can send a JSON‑RPC message to invoke a tool: + +```bash +curl -X POST http://localhost:3000/mcp/messages \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":"1","method":"tools/execute","params":{"name":"your_tool","arguments":{}}}' +``` + +Replace `your_tool` with the name of a tool you defined. + +Use the SSE endpoint to subscribe to resource updates or other MCP events: + +```bash +curl http://localhost:3000/mcp/sse +``` + +Consult the [fast-mcp documentation](https://github.com/yjacquin/fast-mcp) for full protocol details. diff --git a/package-lock.json b/package-lock.json index 3bdae723388..7737b28acf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "maybe", + "name": "sure", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "maybe", + "name": "sure", "version": "1.0.0", "license": "ISC", "devDependencies": { diff --git a/test/mcp/tools/fetch_accounts_balance_tools_test.rb b/test/mcp/tools/fetch_accounts_balance_tools_test.rb new file mode 100644 index 00000000000..4bb2892e4fd --- /dev/null +++ b/test/mcp/tools/fetch_accounts_balance_tools_test.rb @@ -0,0 +1,24 @@ +require "test_helper" + +class FetchAccountsBalanceToolTest < ActiveSupport::TestCase + setup do + @user = users(:family_admin) + @family = @user.family + Current.session = @user.sessions.create! + end + + test "fetches all account balances" do + tool = FetchAccountsBalanceTool.new + result = tool.call + + assert_equal @family.accounts.count, result[:accounts].size + assert result[:total_balance].positive? + end + + test "filters by account type" do + tool = FetchAccountsBalanceTool.new + result = tool.call(account_type: "Depository") + + assert result[:accounts].all? { |a| a[:type] == "Depository" } + end +end