Skip to main content
Glama
finance-mcp

finance-mcp

by dfrysinger

finance-mcp

Local, privacy-first access to your bank and credit-card transactions for Copilot, backed by SimpleFIN. It pulls transactions from your institutions into a normalized on-disk cache and exposes them to Copilot as an MCP server. Nothing leaves your machine except the single HTTPS call to SimpleFIN — no third-party SaaS sees your data, and no fake/sample data is ever shown.

Why SimpleFIN

All of these institutions are supported by SimpleFIN (verified against its live institution search): Target credit card, Nordstrom Card Services, Amazon Chase card (via Chase Bank), Fidelity credit card, Fidelity NetBenefits (via Fidelity Investments), Charles Schwab, and Cyprus Credit Union. SimpleFIN costs ~$15/yr flat with no per-account billing, and you hold the access token.

Related MCP server: finance-mcp

Security model

The SimpleFIN access URL embeds Basic-Auth credentials that can read your transactions. It is therefore stored outside this project directory (this repo may live in a synced folder like Dropbox):

  • Access URL + transaction cache live in ~/.finance-mcp/ (dir 0700, files 0600).

  • Override the location with FINANCE_MCP_HOME.

  • The access URL may instead be supplied via the SIMPLEFIN_ACCESS_URL env var (takes precedence over the saved file), so it never has to touch disk.

Setup

  1. Get a SimpleFIN setup token: sign up at https://bridge.simplefin.org/ and generate one (it is a base64 string).

  2. Claim it (one-time — the token dies after a successful claim):

    uv run finance-mcp claim            # prompts for the token
    # or: uv run finance-mcp claim <SETUP_TOKEN>

    The resulting access URL is saved to ~/.finance-mcp/access_url (mode 0600).

  3. Pull your transactions into the cache:

    uv run finance-mcp sync --days 120

CLI

uv run finance-mcp accounts                          # balances per account
uv run finance-mcp transactions --search grocery     # search the full archive
uv run finance-mcp transactions --start 2026-01-01 --account <id> --json
uv run finance-mcp summary --group-by month          # inflow/outflow aggregation
uv run finance-mcp summary                            # defaults to group-by category, excludes transfers
uv run finance-mcp networth                          # net-worth total per snapshot date
uv run finance-mcp stats                             # archive size + date coverage
uv run finance-mcp categorize                        # seed default rules + show coverage
uv run finance-mcp uncategorized                     # top still-uncategorized merchants
uv run finance-mcp rules list                        # show categorization rules
uv run finance-mcp rules add --pattern "trader joe" --category Groceries
uv run finance-mcp rules rm --rule-id <id>           # remove a rule
uv run finance-mcp set-category <txn_id> Travel      # pin one transaction's category
uv run finance-mcp sync --days 120                    # refresh from SimpleFIN
uv run finance-mcp subscriptions detect               # save recurring charges as a tracked list
uv run finance-mcp subscriptions                      # audit: did tracked bills post? + new candidates
uv run finance-mcp subscriptions mark --name Sketch --lifecycle canceled --effective 2026-04-01
uv run finance-mcp web                                # local read-only review UI in the browser

subscriptions detect scans the archive for recurring monthly charges and saves them into the budget config so your subscriptions become a durable list rather than something re-inferred on every run; it is idempotent and skips merchants already tracked. subscriptions (audit) then reports any tracked bill that did not post in its cycle — a possible billing problem or cancellation — and surfaces untracked recurring merchants as candidates to add. A saved subscription needs no envelope budget: a match keyword pins it to its merchant. Detection is a heuristic starting point — review the saved list and remove any false positives.

subscriptions mark is the cancellation watch: after you cancel (or try to cancel) a subscription, mark it canceling (attempted, unconfirmed) or canceled (confirmed) with the effective date. The audit then stops reporting its expected charges as missing — the absence is what you wanted — and instead warns you if a charge posts on or after that date, so a cancellation that didn't take is surfaced rather than silently billed. --lifecycle active reactivates a bill. Recurring bills and subscriptions are one list: anything in the budget's recurring calendar can be watched this way.

SimpleFIN caps a request at 90 days and expects <=24 requests/day, so sync chunks long ranges into <=89-day windows and you should rely on the archive for day-to-day queries rather than re-syncing constantly. Any SimpleFIN warnings or errors (errors/errlist) are always surfaced.

Web UI (review in the browser)

finance-mcp web starts a local, read-only web UI for reviewing the archive and the budgeting reports without leaving the terminal-driven workflow:

uv run finance-mcp web                 # serves http://127.0.0.1:8765/
uv run finance-mcp web --port 9000     # pick a different port

It binds to 127.0.0.1 only by default because it serves private financial data (override with --host only if you understand the exposure). The request Host header is always checked against an allowlist, so a 127.0.0.1 bind is not defeated by DNS-rebinding; to reach a non-loopback or wildcard bind from another device, name that device-facing host with --allow-host (every other Host is refused). Every page is backed by the same functions the MCP server exposes, so the browser view and Copilot see identical data, and nothing is mutated — there is no sync or confirm action in the UI. Tabs cover accounts, transactions, spending, net worth, transfers, and the burn-down / forecast / allocation / subscription reports; each view also exposes the raw JSON it rendered.

Local archive (multi-year history)

Every sync does two things locally, both in ~/.finance-mcp/ (mode 0600):

  • updates cache.json — the latest normalized snapshot, and

  • folds the data into archive.db, a SQLite database that is the durable, searchable, multi-year history.

The archive is append-only: transactions are upserted by their stable SimpleFIN id (a pending charge is later promoted to posted without duplicating), first_seen is preserved, and nothing is ever deleted — so a transaction stays in the archive even after it ages out of SimpleFIN's rolling window. Each sync also records a balance snapshot per account, which is what powers networth / net_worth_history trends over time.

All read commands and MCP tools serve from archive.db (falling back to cache.json only before the first sync on this version). You can also query it with any SQLite tool:

sqlite3 ~/.finance-mcp/archive.db \
  "SELECT posted, amount, description FROM transactions ORDER BY posted_ts DESC LIMIT 10;"

MCP server (use it from Copilot)

The server runs over stdio and exposes these tools:

Tool

Network?

Purpose

list_accounts

no

accounts + balances + institution

account_balances

no

just balances and as-of dates

get_transactions

no

filter by date / account / search / amount (includes category)

spending_summary

no

inflow/outflow grouped by category, account, org, or month

categorization_status

no

category coverage + spend-by-category breakdown

list_category_rules

no

the active substring → category rules

add_category_rule

no

add a rule (optionally flag matches as transfers)

remove_category_rule

no

delete a rule by id

set_transaction_category

no

pin one transaction's category (survives sync)

net_worth_history

no

total balance per snapshot date (net-worth trend)

archive_stats

no

archive size + earliest/latest transaction

sync_now

yes

refresh the cache + archive from SimpleFIN

Install for others (no clone needed)

With uv installed, you can claim/sync and run the server straight from this repo — uvx fetches and builds it on demand:

# one-time claim + first sync
uvx --from git+https://github.com/dfrysinger/finance-mcp finance-mcp claim
uvx --from git+https://github.com/dfrysinger/finance-mcp finance-mcp sync --days 120

Then register it with Copilot CLI by adding this to ~/.copilot/mcp-config.json under mcpServers (or run /mcp in Copilot to manage):

{
  "mcpServers": {
    "finance": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/dfrysinger/finance-mcp",
        "finance-mcp-server"
      ],
      "tools": ["*"]
    }
  }
}

Local checkout alternative

If you cloned the repo and prefer running from the working tree:

{
  "mcpServers": {
    "finance": {
      "command": "uv",
      "args": ["--directory", "<ABSOLUTE_PATH_TO>/finance-mcp", "run", "finance-mcp-server"],
      "tools": ["*"]
    }
  }
}

get_transactions/spending_summary serve the durable archive (archive.db), which sync (CLI) or sync_now keeps up to date.

Notes / limitations

  • SimpleFIN does not provide spending categories, so categories are assigned locally by a rule engine (case-insensitive substring match on description/payee) plus per-transaction manual overrides — nothing is guessed from outside your data. Internal movements (transfers, card payments, P2P) are flagged so honest spend totals exclude them; pass --include-transfers / include_transfers=true to count them.

  • Amounts are signed: negative = money out. Use max_amount=0 for spending-only, min_amount=0 for income-only.

Development

uv run pytest -q

Verified end-to-end against SimpleFIN's public demo dataset (claim → fetch → normalize → cache → query, plus an MCP stdio round-trip).

Available Tools

22 tools
account_balancesA

Return just the balance and as-of date for each cached account.

Inputs: no named parameters. Returns: object

add_category_ruleA

Add a category rule: a merchant match (+ optional predicates) -> category.

field is description, payee, or any. priority is lowest-wins. Set is_transfer=True for internal transfers / card payments so they are excluded from spending totals. Set account_id to scope the rule to a single account (so a generic descriptor like "FUNDS TRAN" can be reclassified on one account without affecting the same text elsewhere); leave it None to apply to every account.

The merchant match defaults to a case-insensitive substring; set match_mode='regex' to match pattern as a case-insensitive regular expression instead (useful when a store number splits a merchant name). Optional predicates further narrow a rule and must all hold to match: amount_min/amount_max bound the amount magnitude (abs(amount), so 200-350 matches a $304 charge), and day_min/day_max bound the posted day-of-month (1-31) — together they isolate a recurring charge like a mid-month insurance premium from other charges at the same merchant.

Inputs: pattern (required), category (required), field, day_max, +7 more. Returns: object

allocation_audit_reportA

Audit each scheduled transfer: did it fire on time, late, early, or not at all.

Dates are YYYY-MM-DD; end defaults to today and start to a year back. day_tolerance is how far a transfer may drift and still count as fired. A genuinely-ambiguous allocation surfaces as missing until its transfer link is confirmed via confirm_transfer.

Inputs: end, start, day_tolerance. Returns: object

archive_statsA

Report archive size and date coverage (transaction count, earliest/latest).

Inputs: no named parameters. Returns: object

budget_burndownA

Per-envelope planned target vs. actual spend for one YYYY-MM month.

Reads the budget config and the categorized archive. Returns each envelope's target, actual spend, and remaining (negative = over budget), plus unmapped spend on accounts in no envelope so nothing is silently dropped.

Inputs: month (required). Returns: object

budget_forecastA

Per-envelope sufficiency over a window: will each cover its upcoming bills.

Dates are YYYY-MM-DD. as_of defaults to today and through to 60 days later. Each envelope gets a verdict (ok / at_risk / balance_unknown) with the projected minimum balance and, when at risk, the date and shortfall.

Inputs: as_of, through. Returns: object

categorization_statusB

Report category coverage and the breakdown of transactions per category.

Inputs: no named parameters. Returns: object

confirm_transferA

Confirm one transfer link by id, locking the pairing as authoritative.

A confirmed link is excluded from every future reconcile, so the user's decision is never silently recomputed. Only a two-leg link can be confirmed; confirming an unmatched single leg is rejected. Returns the updated link or an error.

Inputs: link_id (required). Returns: object

get_transactionsA

Query cached transactions.

Dates are YYYY-MM-DD. Amounts are signed (negative = money out), so use max_amount=0 for spending only or min_amount=0 for income only. Each transaction carries a derived category and is_transfer flag; pass category to filter to one, or include_transfers=False to drop internal transfers and card payments. Returns the matching transactions plus the count.

Inputs: limit, search, category, end_date, +6 more. Returns: object

list_accountsA

List cached accounts with balances and the institution each belongs to.

Inputs: no named parameters. Returns: object

list_category_rulesB

List the category rules (pattern -> category) currently in effect.

Inputs: no named parameters. Returns: object

list_transfersA

List reconstructed transfer links as from_account -> to_account $amount [why].

The raw feed names only the product type a transfer went to, never the named account; each link here recovers the hidden counterparty and records why it was drawn. status optionally restricts to one lifecycle state (confirmed / inferred / unconfirmed / unmatched); unconfirmed links are the ones awaiting the user's review and sort first.

Inputs: status. Returns: object

net_worth_historyA

Total balance across all accounts per as-of date, from the archive.

Each sync records a balance snapshot, so this builds a net-worth trend over time (oldest first). Loan/credit balances are negative, so the total is true net worth.

Inputs: no named parameters. Returns: object

reconcile_transfersA

Rebuild internal-transfer links from the archive (idempotent).

Re-runs the matcher over the categorized archive and persists the links, preserving every confirmed link and recomputing the rest. Returns counts of inferred / needs-confirm / unmatched links plus promotions and downgrades. Run this after a sync so list_transfers reflects the latest data.

Inputs: no named parameters. Returns: object

red_flags_reportA

Loud alerts for debt payments that were returned or missed.

as_of is YYYY-MM-DD, defaulting to today. Audits each configured debt account (loans, mortgages, financed purchases) directly from its own transactions, so a payment is caught no matter which account funded it. A returned payment (posted then reversed) and a month whose payments net to zero or less (none posted, or fully reversed) each surface as a red flag; the payment amount is never compared, so any positive net counts as paid. A returned or missed payment that is later re-made or covered by an extra payment is downgraded to made_good and drops out of the red count. A debt that syncs only a balance surfaces as an explicit unauditable note rather than a silent gap. With no debt_accounts configured the report is simply empty.

Inputs: as_of. Returns: object

remove_category_ruleC

Delete a category rule by its id.

Inputs: rule_id (required). Returns: object

set_transaction_categoryA

Pin a category to a single transaction.

Manual overrides win over every rule and survive re-syncs, so use this to fix a one-off that the rules get wrong.

Inputs: txn_id (required), category (required), is_transfer. Returns: object

spending_summaryA

Aggregate net spending over a date range, grouped for budgeting.

group_by is category (default), account, org, month, or envelope. Categories come from a local rules engine plus manual overrides. Internal transfers and income (payroll, benefits, investment income) are excluded by default so the totals reflect real spending; set exclude_transfers=False or exclude_income=False to include them.

Per group, outflow is spend, inflow is refunds/returns that net against it, and unclassified_inflow is positive amounts with no spending category (surfaced but not netted). net is outflow plus refunds.

envelope rolls spend up by the configured budget envelope that owns each account (so spend on a non-envelope account such as a loan or brokerage falls into an (unmapped) bucket). It needs at least one envelope in the budget config; with none configured it returns an error rather than an empty view.

Inputs: end_date, group_by, start_date, exclude_income, +2 more. Returns: object

subscription_audit_reportA

Flag tracked bills that did not post (billing problem / cancellation) and surface untracked recurring merchants as candidates for the assistant to judge.

Dates are YYYY-MM-DD; end defaults to today and start to a year back so a monthly charge clears min_occurrences. tracked is the full roster of configured recurring bills with each one's amount, due day, last-seen date, and next due date. expected_missing is the deterministic high-stakes alert; candidate_new is advisory — the assistant decides which candidates are real subscriptions.

Inputs: end, start, day_tolerance, min_occurrences. Returns: object

subscriptions_detectA

Detect recurring charges from history and save them as tracked bills.

Scans the archive for merchants with repeated, same-amount, monthly-spaced debits and writes each as a recurring bill in the budget config (creating the config if absent), so your subscriptions become a saved list rather than something re-inferred on every audit. Idempotent: a merchant already tracked is skipped. Dates are YYYY-MM-DD; end defaults to today and start to a year back. day_tolerance (default 7) is the day-of-month drift allowed when deciding whether a charge is already covered by an existing bill. Weekly/yearly merchants are reported under unsupported_cadence (only monthly bills are tracked). Monthly merchants that could not be auto-tracked — text too generic/variable to pin, or a recurring charge at a different price from an already-tracked subscription — are reported under needs_review (each with a reason) rather than written.

Inputs: end, start, day_tolerance, min_occurrences. Returns: object

subscriptions_markA

Mark a tracked recurring bill as canceling, canceled, or active again.

The cancellation watch: after you cancel (or try to cancel) a subscription, record it here so the audit stops reporting its expected charges as missing and instead warns you if it charges again. lifecycle is canceling (a cancellation was attempted but not confirmed), canceled (confirmed), or active (reactivate a bill you'd marked). cancel_effective is the YYYY-MM-DD date the cancellation takes effect and is required for canceling and canceled — any matching charge on or after it is surfaced as the bill "coming back". Omit it when reactivating. variable optionally flags the bill as one whose amount changes every cycle (a usage-based or escrow bill): true matches it by merchant and date regardless of amount and reports the actual charged amount, false restores exact-amount matching, null leaves the setting unchanged. The bill is found by name (case-insensitive); the name must match exactly one bill.

Inputs: name (required), lifecycle (required), variable, cancel_effective. Returns: object

sync_nowA

Pull fresh data from SimpleFIN into the cache (network call).

Respect SimpleFIN's ~24 requests/day budget: a sync of >89 days makes one request per ~89-day window. Returns a summary including any SimpleFIN errors.

Inputs: days. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 22 tools

    Disambiguation5/5

    Each tool targets a distinct function or domain: accounts, transactions, categories, transfers, subscriptions, budgets, audits, etc. Overlaps are minimal (e.g., multiple audit reports but for different areas) and descriptions are detailed enough to disambiguate.

    Naming Consistency5/5

    Tool names consistently use snake_case and follow a descriptive noun_verb or noun_noun pattern (e.g., list_accounts, sync_now, budget_burndown). No mixing of conventions or cryptic abbreviations.

    Tool Count4/5

    22 tools is above average but justified given the broad domain of personal finance (syncing, accounts, transactions, categories, budgets, transfers, subscriptions, audits). Each tool serves a specific purpose; however, a few report tools could potentially be consolidated.

    Completeness4/5

    The tool surface covers core operations (sync, read, categorize, budget, audit) and advanced features (subscription detection, transfer reconciliation). Missing is the ability to create/update/delete transactions or budgets directly, but the server is primarily analytical, so this is acceptable.

    Maintenance

    ActivityInactive
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      A
      quality
      D
      maintenance
      Exposes SimpleFIN bank data (accounts and transactions) as tools for Claude Code, enabling users to connect to bank accounts, list balances, and fetch transactions.
      4
      -
    • A
      license
      A
      quality
      B
      maintenance
      Local-first personal finance MCP that aggregates bank, brokerage, credit, and loan accounts via Plaid and writes balances, holdings, and transactions to an Obsidian-style markdown vault, with access tokens stored securely in macOS Keychain.
      12
      MIT
    • A
      license
      B
      quality
      C
      maintenance
      Enables local-first personal finance management through deterministic tools for importing, categorizing, and analyzing bank transactions.
      36
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables local read-only exploration of Quicken Simplifi financial data through MCP, with tools for searching transactions, categories, tags, and merchants, using a local SQLite cache and token-based authentication.
      1
      MIT