Install
$ agentstack add mcp-griffin-goepper-goetta-finance-mcp ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README — it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming — see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps — measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
goetta-finance
[](https://pypi.org/project/goetta-finance/) [](https://github.com/griffin-goepper/goetta-finance-mcp/actions/workflows/ci.yml) [](https://github.com/griffin-goepper/goetta-finance-mcp/actions/workflows/security.yml) [](./LICENSE)
[](https://github.com/astral-sh/ruff)
A local-first tool that connects SimpleFIN to Claude. Your bank data lives only on your machine, in a DuckDB file you own. Claude reads it through an MCP server; you read it through a small web dashboard at localhost:8765.
See [PROJECT_PLAN.md](./PROJECT_PLAN.md) for the full vision and roadmap.
Requirements
- Python 3.11+
- A SimpleFIN Bridge account (about $1.50/month) — sign up at
Install
pipx install goetta-finance # recommended
# or: pip install goetta-finance
pipx is the cleanest way to install a Python CLI: it lives in its own isolated environment and puts the goetta-finance command on your PATH. That global path is also the stable thing to point Claude Desktop's MCP config at — no project venv path to chase.
From source (for development)
git clone https://github.com/griffin-goepper/goetta-finance-mcp.git
cd goetta-finance-mcp
python -m venv .venv
.venv/Scripts/activate # Windows
# source .venv/bin/activate # macOS/Linux
pip install -e ".[dev]"
Quick start
goetta-finance init # interactive setup wizard
goetta-finance sync # pull fresh data from SimpleFIN
goetta-finance web # open the dashboard at http://127.0.0.1:8765
The init wizard walks four steps:
- Claim a SimpleFIN setup token → access URL (stored at
~/.local/share/goetta-finance/config.json, mode0600on POSIX). - Initialize the DuckDB store.
- Pull initial history (up to 90 days).
- Auto-write the Claude Desktop MCP config (merges into the existing file; preserves any other servers you've configured).
Re-running init is safe — each step detects existing state and offers to skip or replace.
Commands
| Command | What it does | |---|---| | goetta-finance init | Interactive setup. Re-runnable. | | goetta-finance sync | One-shot pull from SimpleFIN. Idempotent — safe to run repeatedly. | | goetta-finance status | Show last sync, account list with balances, recent warnings/errors. | | goetta-finance serve | Start the MCP server over stdio (used by Claude Desktop). | | goetta-finance web | Start the local web dashboard. --port 8765 and --host 127.0.0.1 by default. | | goetta-finance daemon | Long-lived process: dashboard + MCP HTTP endpoint + daily scheduled sync, from one process. See "Daemon mode" below. | | goetta-finance account add\|list\|set-balance\|set-liability\|remove | Manage manual accounts and liability flags. See "Manual accounts and liabilities" below. | | goetta-finance account link\|links\|unlink | Roll a manual account's balance forward from matching transfers on a synced account. See "Linked transfers" below. | | goetta-finance category list\|add\|set-rule\|remove-rule\|default-rules | Manage categories and the rules that map descriptions to them. See "Transaction categorization" below. | | goetta-finance transaction categorize\|uncategorize | Manual per-transaction category overrides. See "Transaction categorization" below. | | goetta-finance goal add-spending\|add-balance\|list\|remove | Spending caps and balance targets, evaluated at read time. See "Goals" below. |
Manual accounts and liabilities
SimpleFIN can't reach every account — 401(k) providers, HSAs, brokerages outside its bank list, and student-loan servicers all sit outside. goetta-finance account lets you track those by hand so they show up in MCP queries and the dashboard alongside synced accounts.
The four subcommands
# Create a manual account. Prompts interactively for any missing flags.
goetta-finance account add \
--name "Apple Savings" \
--org "Apple" \
--type savings \
--balance 30000 \
[--as-of 2026-05-17] # observation date, defaults to today (UTC)
# Mark an account as a liability (or clear the flag). Works on any account id.
goetta-finance account add ... --liability # at creation time
goetta-finance account set-liability MANUAL- true # after the fact
goetta-finance account set-liability ACT- true # SimpleFIN accounts too
# Update the balance on a manual account (also writes a balance_snapshot).
goetta-finance account set-balance MANUAL- 32500 [--as-of 2026-05-17]
# List all accounts. Manual + liability rows are tagged in the output.
goetta-finance account list
# Remove a manual account. Two layers of safety: cascade-delete its snapshots
# (--force) AND type the account name to confirm (skip with --yes for scripts).
goetta-finance account remove MANUAL- --force
Sign convention for liabilities
A liability always reduces net worth, regardless of how the source signs the balance. The signed-balance formula is:
CASE WHEN is_liability THEN -ABS(balance) ELSE balance END
So you can enter a student loan either way and the math comes out right:
account add --type loan --balance 22500 --liability(positive amount owed)- A SimpleFIN credit card showing
balance = -500and you've flipped it tois_liability=true
Both contribute -500 and -22500 respectively to net worth — collapsing the loan-servicer convention and the SimpleFIN convention to one answer. The dashboard's net-worth chart and the accounts page footer respect the formula. When writing sql_query SELECTs against the accounts table, reach for the same CASE WHEN expression to compute totals correctly.
is_liability is independent of type on purpose — type describes what kind of account it is (loan, credit, investment), while is_liability controls how net-worth math treats it. A margin account is type=investment but functionally a liability; you can flip the flag without changing the type.
Linked transfers
Manual balances go stale the moment money actually moves — but the contributions are usually already in your data, as the transfer legs on a synced checking account. A transfer link connects the two so the manual balance rolls forward automatically:
# See what looks linkable. Candidates are detected when a synced account's
# transactions carry a payee exactly matching a manual account's name —
# each comes with the ready-made link command.
goetta-finance account links
# Link it: from then on, matching transactions on the synced account roll
# the manual balance forward on every sync (and immediately at link time).
goetta-finance account link MANUAL- --from ACT- \
--pattern "Apple Savings" [--match contains|regex]
# Remove a link by id. Already-applied transfers stay in the balance.
goetta-finance account unlink
How it behaves:
- A debit out of the source credits the manual account (and money moving back debits it). The pattern is matched against the transaction's payee and description; pending transactions wait until they settle.
- Everything posted at or before the account's balance date is assumed to already be in that balance — linking never double-counts history, and an applications ledger guarantees each transaction applies at most once ever, across re-syncs and re-links.
set-balancestays the true-up. Transfer sums can't see interest, so update the balance from a real statement occasionally: the true-up re-anchors the link at its--as-ofdate and re-applies anything posted after it against your fresh number.- Every consumer updates for free. The roll-forward writes a genuine balance + snapshot through the same path as
set-balance, so net worth, the over-time chart, balance goals, and the goal pace math all follow without special cases. - Liability accounts can't be linked yet — a manual loan's stored sign is ambiguous (everything reads it through
ABS()), so paydown tracking still goes throughset-balancetrue-ups.
Heads-up
- Retroactive flag. Toggling
is_liabilityre-treats all historicalbalance_snapshotsfor that account under the new value in net-worth-over-time charts. This is almost always what you want; if it isn't, flip the flag back. - CC-credit edge case. A credit card with
is_liability=trueand a positive balance (you overpaid and now have a credit) computes as-balanceinstead of+balance. Rare;set-liability falsewhile the credit exists, then re-enable, is the workaround. - Balance is authoritative. Payments to a manual loan don't auto-decrement the balance — re-run
set-balancefrom your servicer's monthly statement. For asset accounts fed by visible transfers, a linked transfer (above) automates exactly that.
Transaction categorization
Every transaction resolves to a category at read time through a SQL view (transactions_with_category). Three layers, outermost wins:
- Manual override — a row in
transaction_overridesfor that transaction id. - Rule match — the lowest-priority rule in
category_ruleswhose pattern matches the transaction's description and whose optional amount bounds (compared against the absolute amount; min inclusive, max exclusive) are satisfied. Uncategorized— the fallback when nothing else matches.
Read-time resolution is the feature, not an optimization: adding or editing a rule applies retroactively to every existing transaction with zero data migration. A category_id column on transactions would silently break that.
Migration 0004 ships 14 default categories (Groceries, Dining, Transportation, Gas, Utilities, Subscriptions, Rent/Mortgage, Healthcare, Entertainment, Shopping, Travel, Transfers, Income, Uncategorized). Migration 0007 trims the default rule seed to a deliberately minimal universal set: a single (?i)transfer regex → Transfers (every bank uses "transfer" somewhere in inter-account descriptions) and five global subscriptions (Spotify, Netflix, Hulu, Disney Plus, Amazon Prime). Earlier versions shipped 38 US-merchant-specific defaults (Kroger, Starbucks, Shell, etc.) — they were noise for non-US users and bias for the rest. Expect most of your spending to land in Uncategorized on first install. That's the design: curate by adding rules for your descriptions. The MCP top_uncategorized_patterns tool (or the category set-rule CLI) is the curation path.
CLI
# Inspect what was seeded vs. what you've added.
goetta-finance category list # all categories with txn + rule counts
goetta-finance category default-rules # the is_default=TRUE rule set
# Add a rule. Pattern matches case-insensitively against transaction description.
goetta-finance category set-rule Dining --match contains --pattern 'CHIPOTLE'
goetta-finance category set-rule Dining --match regex --pattern '(?i)venmo.*lunch'
# Amount bounds refine a pattern match by abs(amount) — dual-use merchants
# split cleanly (small gas-station buys are snacks, big ones are fuel).
# min is inclusive, max exclusive: no gap or overlap at exactly $20.
goetta-finance category set-rule Vice --pattern 'SPEEDWAY' --max-amount 20
goetta-finance category set-rule Gas --pattern 'SPEEDWAY' --min-amount 20
# Remove a rule. Defaults require --force AND a typed-pattern confirmation;
# user-added rules just need the id.
goetta-finance category remove-rule 42
goetta-finance category remove-rule 7 --force # default rule, prompts for the pattern
# Add a custom category.
goetta-finance category add --name "Gardening" --color "#4ade80"
# Recategorize a single transaction (manual override beats any rule).
goetta-finance transaction categorize Groceries
goetta-finance transaction uncategorize # back to rule resolution
# Category names are case-insensitive ("dining" → "Dining") and typos get
# a "Did you mean?" suggestion via difflib.
From Claude
The spending_by_category(start, end) MCP tool aggregates per-category totals over a date range. By default it returns spending only (amount You: "what's still uncategorized this month?" > Claude calls top_uncategorized_patterns → "$85 CRUMBL COOKIES (3×), $60 NEW GYM LLC (2×)..." > You: "Crumbl is dining, the gym is healthcare" > Claude calls add_category_rule twice. Rules apply retroactively; done.
One-off fixes use categorize_transaction (override beats any rule) and uncategorize_transaction (undo). The MCP rule-write path runs the same pattern validation as the CLI.
For anything custom, query the view directly via sql_query:
SELECT category, COUNT(*), SUM(-amount) AS total
FROM transactions_with_category
WHERE posted >= '2026-01-01' AND amount `). Both surfaces run the same best-effort validator (refuses uncompilable regexes, nested quantifiers like `(X+)+`, large counted repetitions like `(.*a){25}`) but CPython's `re` engine doesn't release the GIL so a runtime regex timeout isn't possible. The load-bearing runtime defense is the existing `query_sql` statement-timeout watchdog (`GOETTA_FINANCE_SQL_TIMEOUT_SECONDS`, default 30s). See [`CLAUDE.md`](./CLAUDE.md) for the threat model.
- **One category per transaction.** Costco-style mixed purchases get one label. No splits in v1.
- **Default rules don't re-seed if you delete them.** Migrations run once per database; the slate stays where you leave it. New defaults arrive only via new migration files — never edits to shipped ones.
See [`CUSTOMIZATION.md`](./CUSTOMIZATION.md) for the full map of user-tunable surfaces (rules, prefix list, categories, flags, colors).
## Goals
Lightweight thresholds, not envelope budgeting: cap a category's spending per calendar month/year, or track an account balance toward a target. Progress is **computed at read time** — nothing is stored, so recategorizing transactions or a fresh sync changes goal progress retroactively, exactly like the categorization view.
```bash
# Cap net spending in a category per calendar month (or --period year).
goetta-finance goal add-spending Groceries --limit 400 --period month
# Track a balance: at_least = savings target / emergency-fund floor,
# at_most = debt ceiling / paydown. --by adds required-per-month pace math.
goetta-finance goal add-balance --target 10000 --direction at_least --by 2027-06-01
goetta-finance goal add-balance --target 2000 --direction at_most
goetta-finance goal list # progress, status, and pace per goal
goetta-finance goal remove 3 # confirms unless --yes
Semantics worth knowing:
- Cap math matches the pie exactly. Spending caps reuse the same net-spending SQL as
spending_by_categoryand the dashboard pie: refunds reduce the total, hidden accounts are excluded, pending transactions count, and periods are UTC calendar buckets. - Liability accounts evaluate the absolute balance (amount owed):
--direction at_most --target 2000on a credit card means "owe under 2000" whichever way the institution signs the balance. - Status is
on_track/at_risk(ahead of linear pace, or trend projects past--by) /over(cap blown, ceiling breached) /met. Balance goals derive pace from the last 90 days of balance snapshots. - Breach summary after sync.
goetta-finance syncprints a yellowgoal:line for each goal at statusover; the daemon logs the same at WARNING after scheduled syncs.at_risknever fires a warning — it's pace noise by design. - From Claude:
list_goals(progress + pace),set_goal,remove_goal. The dashboard has a Goals page with progress bars.
Daemon mode
goetta-finance daemon runs one long-lived process that hosts:
- The dashboard at
http://127.0.0.1:8765/ - The MCP endpoint at
http://127.0.0.1:8765/api/mcp(streamable-HTTP transport — Claude Code and Claude Desktop both support this) - An internal scheduler that runs
collect()daily at--sync-atlocal time (default `06:00
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: griffin-goepper
- Source: griffin-goepper/goetta-finance-mcp
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.