finance-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@finance-mcp show my spending summary for last month"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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/(dir0700, files0600).Override the location with
FINANCE_MCP_HOME.The access URL may instead be supplied via the
SIMPLEFIN_ACCESS_URLenv var (takes precedence over the saved file), so it never has to touch disk.
Setup
Get a SimpleFIN setup token: sign up at https://bridge.simplefin.org/ and generate one (it is a base64 string).
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).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 browsersubscriptions 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 portIt 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, andfolds 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 |
| no | accounts + balances + institution |
| no | just balances and as-of dates |
| no | filter by date / account / search / amount (includes category) |
| no | inflow/outflow grouped by category, account, org, or month |
| no | category coverage + spend-by-category breakdown |
| no | the active substring → category rules |
| no | add a rule (optionally flag matches as transfers) |
| no | delete a rule by id |
| no | pin one transaction's category (survives sync) |
| no | total balance per snapshot date (net-worth trend) |
| no | archive size + earliest/latest transaction |
| 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 120Then 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=trueto count them.Amounts are signed: negative = money out. Use
max_amount=0for spending-only,min_amount=0for income-only.
Development
uv run pytest -qVerified end-to-end against SimpleFIN's public demo dataset (claim → fetch → normalize → cache → query, plus an MCP stdio round-trip).
Available Tools
22 toolsaccount_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
Scored across 22 tools
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.
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.
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.
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
Related MCP Connectors
Personal finance assistant — track transactions, budgets, net worth via Plaid
Personal finance, bank account, and shared memory connector for Claude, ChatGPT, Gemini Spark & more
Ask your AI about your own accounts, debts, budgets and goals. OAuth sign-in, read-only by default.
Read-only access to your own bank, card and brokerage accounts for Claude, ChatGPT and other AI agents: balances, transactions, spending, subscriptions, cashflow and investment holdings. It can never move money.
Related MCP Servers
- FlicenseAqualityDmaintenanceExposes SimpleFIN bank data (accounts and transactions) as tools for Claude Code, enabling users to connect to bank accounts, list balances, and fetch transactions.4-
- AlicenseAqualityBmaintenanceLocal-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.12MIT
- AlicenseBqualityCmaintenanceEnables local-first personal finance management through deterministic tools for importing, categorizing, and analyzing bank transactions.36MIT
- AlicenseNot gradedqualityCmaintenanceEnables 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.1MIT