# Goetta Finance Mcp

> Goetta is your goetta partner to meeting your goetta goals

- **Type:** MCP server
- **Install:** `agentstack add mcp-griffin-goepper-goetta-finance-mcp`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [griffin-goepper](https://agentstack.voostack.com/s/griffin-goepper)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [griffin-goepper](https://github.com/griffin-goepper)
- **Source:** https://github.com/griffin-goepper/goetta-finance-mcp

## Install

```sh
agentstack add mcp-griffin-goepper-goetta-finance-mcp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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](https://bridge.simplefin.org/) 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

```bash
pipx install goetta-finance      # recommended
# or:  pip install goetta-finance
```

[`pipx`](https://pipx.pypa.io/) 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)

```bash
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

```bash
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:

1. Claim a SimpleFIN setup token → access URL (stored at `~/.local/share/goetta-finance/config.json`, mode `0600` on POSIX).
2. Initialize the DuckDB store.
3. Pull initial history (up to 90 days).
4. 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

```bash
# 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:

```sql
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 = -500` and you've flipped it to `is_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:

```bash
# 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-balance` stays 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-of` date 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 through `set-balance` true-ups.

### Heads-up

- **Retroactive flag.** Toggling `is_liability` re-treats all historical `balance_snapshots` for 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=true` and a *positive* balance (you overpaid and now have a credit) computes as `-balance` instead of `+balance`. Rare; `set-liability false` while 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-balance` from 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:

1. **Manual override** — a row in `transaction_overrides` for that transaction id.
2. **Rule match** — the lowest-priority rule in `category_rules` whose pattern matches the transaction's description *and* whose optional amount bounds (compared against the absolute amount; min inclusive, max exclusive) are satisfied.
3. **`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

```bash
# 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`:

```sql
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_category` and 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 2000` on 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 sync` prints a yellow `goal:` line for each goal at status `over`; the daemon logs the same at WARNING after scheduled syncs. `at_risk` never 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-at` local 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](https://github.com/griffin-goepper)
- **Source:** [griffin-goepper/goetta-finance-mcp](https://github.com/griffin-goepper/goetta-finance-mcp)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-griffin-goepper-goetta-finance-mcp
- Seller: https://agentstack.voostack.com/s/griffin-goepper
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
