Skip to main content
Glama
Praneeth2383

Family Expense MCP

by Praneeth2383

Family Expense AI — MCP-Based Expense Assistant

A real, working family expense/income/budget assistant whose financial capabilities are exposed as MCP (Model Context Protocol) tools through a FastMCP server, backed by PostgreSQL. This is not a chatbot with a database bolted on — MCP is the core of the architecture: any MCP-compatible LLM client (Claude Desktop, or Claude via a remote connector) discovers and invokes these tools directly.

Architecture

                    User
                     |
                AI / LLM (e.g. Claude)
                     |
              MCP Protocol
                     |
             FastMCP Server (main.py)
                     |
               MCP Tools (23 tools)
          +----------+----------+-----------+
          |          |          |           |
      Family     Expenses    Income   Reports/Budgets/Insights
          |          |          |           |
          +----------+----------+-----------+
                     |
                 PostgreSQL

Remote usage (Claude connecting to a deployed server):

Claude -> MCP Connector -> HTTPS -> Remote MCP Server (Render)
                                       -> FastMCP -> MCP Tools -> PostgreSQL

The LLM only ever understands the request, selects a tool, and generates parameters. Validation, business logic, calculations, and all persistence happen in app/services/*, with PostgreSQL as the single source of truth.

Multi-user family usage (one shared database, many Claude accounts)

Each family member can run their own Claude account/client connected to the same deployed MCP server (https://<your-app>.onrender.com/mcp). When someone says "I spent ₹500 on lunch," their Claude calls add_expense with member_name set to that person — the server resolves the name to a member_id and writes it into the one shared Postgres database. So the whole family's spending naturally converges into a single source of truth, per person, without needing separate databases per user.

Tips for this setup:

  • Register everyone once with add_family_member (e.g. Praneeth → Self, Rahul → Brother, Mom → Mother).

  • Give each person's Claude a system/custom instruction like "My name is Rahul — when using Family Expense tools, use member_name='Rahul' unless I say otherwise." so they don't have to repeat their name every time.

  • Every report/insight/budget tool accepts an optional member_name to scope results to one person, or omit it for the whole family.

Project structure

mcp-famil/
├── main.py                # FastMCP server: 23 MCP tools, thin wrappers
├── chatbot.py              # Local NL test harness (no MCP client needed)
├── tool_router.py          # Dispatches tool name + params -> service call
├── formatter.py             # Raw results -> clean, INR-formatted text
├── requirements.txt
├── .env.example
├── .gitignore
├── render.yaml              # One-file Render deployment config
│
├── app/
│   ├── db/
│   │   ├── connection.py    # psycopg2 pool + init_db()
│   │   └── schema.sql       # Table definitions
│   ├── services/            # Business logic + validation (DB access only here)
│   │   ├── errors.py
│   │   ├── validators.py
│   │   ├── date_utils.py
│   │   ├── family_service.py
│   │   ├── expense_service.py
│   │   ├── income_service.py
│   │   ├── summary_service.py
│   │   ├── report_service.py
│   │   ├── budget_service.py
│   │   └── insight_service.py
│   └── ai/
│       ├── schemas.py           # Anthropic tool-use schemas (mirrors main.py)
│       ├── intent_parser.py     # NL -> (tool, params) via Claude tool-use
│       ├── category_detector.py # Keyword-based expense category guesser
│       ├── reports.py           # Rule-based report phrase parsing
│       ├── budget.py            # Rule-based budget phrase parsing
│       └── insights.py          # Suggestion generation from real DB numbers
│
└── tests/

Database schema

  • family_members(id, name, relationship, created_at)

  • expenses(id, member_id → family_members, amount, category, expense_date, description, created_at, updated_at)

  • income(id, member_id → family_members, amount, source, income_date, notes, created_at, updated_at)

  • budgets(id, category, amount, created_at, updated_at)

Applied automatically on startup via init_db() (idempotent CREATE TABLE IF NOT EXISTS), so there's no separate migration step to run.

MCP tools (23)

Area

Tools

Family

add_family_member, list_family_members

Expenses

add_expense, list_expenses, update_expense, delete_expense

Income

add_income, list_income, update_income, delete_income

Summary

total_income, total_expenses, current_balance, expenses_by_category, expenses_by_member

Reports

monthly_income, monthly_expenses, monthly_summary, compare_months

Budgets

set_budget, list_budgets, budget_status

Insights

financial_insights

Every tool is a thin wrapper: it calls tool_router.route() (which calls into app/services/*) and formats the result with formatter.py before returning it — so the response an LLM/user sees is already a clean, ₹-formatted message, not a raw JSON blob.

Currency

Everything is INR (₹), formatted with Indian digit grouping (e.g. ₹12,34,567.00, not ₹1,234,567.00). Amounts are stored as NUMERIC(12,2) in Postgres and only formatted at the presentation layer.

Setup

  1. Install dependencies

    pip install -r requirements.txt
  2. Configure environment

    cp .env.example .env
    # edit .env: set DATABASE_URL to your PostgreSQL connection string
    # (LLM_API_KEY is only needed for chatbot.py, not for the MCP server itself)
  3. Run the MCP server

    python main.py
    # Tables are created automatically. MCP endpoint: http://localhost:8000/mcp
  4. (Optional) Try the local natural-language chatbot

    python chatbot.py

    Example session:

    You: Rahul spent ₹700 on petrol today.
    AI: ✅ Expense Added
    
        👤 Rahul
        💸 ₹700.00
        📂 Petrol
        📅 2026-09-06

Local testing with an MCP client

With python main.py running, point any MCP-compatible client at http://localhost:8000/mcp (Streamable HTTP transport) and confirm all 23 tools are discoverable. For example, using the FastMCP CLI:

fastmcp dev main.py

Note: depending on your installed fastmcp version, the transport name passed to mcp.run() in main.py may need to be "streamable-http" instead of "http". Check fastmcp --version / release notes if the server fails to start.

Using Supabase as the database

Supabase's database is plain PostgreSQL, so no code changes are needed — just point DATABASE_URL at it:

  1. Create a project at supabase.com.

  2. Go to Project Settings → Database → Connection string → URI. Use the Session pooler (or Transaction pooler) connection string, not the direct connection — it's the one meant for long-running apps and works fine with this project's connection pool.

  3. Copy it into DATABASE_URL in your .env (or your host's environment variables), replacing [YOUR-PASSWORD] with your actual DB password:

    DATABASE_URL=postgresql://postgres.xxxxxxxxxxxx:[YOUR-PASSWORD]@aws-0-xx-xxxx-x.pooler.supabase.com:5432/postgres
  4. Run python main.py (or redeploy your host) — init_db() creates the family_members, expenses, income, and budgets tables on Supabase automatically on first boot. SUPABASE_URL/SUPABASE_KEY are not needed for this — those are only for the Supabase client SDK, which this project doesn't use.

Deployment

You need somewhere to run the always-on Python process — Supabase only hosts the database, not the app. Any host that gives you a public HTTPS URL works (Claude's remote connector requires HTTPS, not plain HTTP). Whatever you choose, the server just needs:

  • DATABASE_URL set to your Supabase connection string

  • To listen on 0.0.0.0:$PORT (already handled by main.py)

  • The /mcp path reachable over HTTPS at the root of your public URL

render.yaml is included for a one-file Render deploy if you want it:

  • Build command: pip install -r requirements.txt

  • Start command: python main.py

  • Environment variables: DATABASE_URL, LLM_API_KEY (optional), ANTHROPIC_MODEL (optional)

If you're self-hosting on your own server/VPS instead, make sure a reverse proxy (Caddy, nginx + certbot, etc.) terminates TLS in front of main.py and forwards to it — Claude cannot connect to a bare http:// URL or a self-signed certificate.

Verify the deployed server before adding it to Claude

curl -s -D - -o /dev/null -X POST https://<your-domain>/mcp \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'

You should get HTTP/1.1 200 back with a serverInfo block naming "Family Expense MCP". If this fails, fix it before adding the connector — Claude will hit the same URL.

Add it to Claude as a custom connector

  1. On claude.ai, go to Settings → Connectors.

  2. Click Add custom connector.

  3. Enter a name (e.g. "Family Expense AI") and the URL: https://<your-domain>/mcp

  4. Save, then enable the connector in a chat (the tool/connector picker below the message box).

  5. Test it: ask Claude something like "Add a family member named Praneeth, relationship Self" or "I spent ₹500 on lunch today" — Claude should call the corresponding MCP tool and show the formatted result.

Every family member can add the same URL as their own custom connector from their own claude.ai account — they all talk to the same hosted server and the same Supabase database, so everyone's expenses land in one shared, per-person-tracked source of truth (see "Multi-user family usage" above).

Example natural-language flow

User: "Rahul spent ₹700 on petrol today."

  1. LLM determines intent: add_expense, member_name=Rahul, amount=700, category=Petrol, expense_date=today

  2. add_expense tool → tool_router.route()expense_service.add_expense()

  3. Resolves "Rahul" → member_id, validates the amount, inserts the row

  4. Result formatted and returned:

    ✅ Expense Added
    
    👤 Rahul
    💸 ₹700.00
    📂 Petrol

Error handling

Every tool call is wrapped so it never crashes the server. Domain errors (ValidationError, NotFoundError) are caught and returned as a friendly ⚠️ ... message — covering: unknown family member, invalid/negative amount, missing required parameter, unparseable date, expense/income not found, no budget set for a category, unknown tool, and any unexpected exception (❌ Unexpected error: ...).

Security

No credentials are hardcoded anywhere. DATABASE_URL and LLM_API_KEY are read from the environment (.env, gitignored). .env.example documents every variable without real values.

Running tests

pytest

Unit tests (formatter, validators, date parsing, category detection, rule-based NL parsing, tool routing) run with no external services. tests/test_integration.py exercises real database round-trips and is skipped automatically unless DATABASE_URL points at a database.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Praneeth2383/mcp-famil'

If you have feedback or need assistance with the MCP directory API, please join our Discord server