{"slug": "show-hn-ledgerline-local-first-finance-mcp-server", "title": "Show HN: Ledgerline – local-first finance MCP server", "summary": "Ledgerline, a local-first finance MCP server, gives AI agents read-only access to financial data via a single SQLite file without cloud storage or exposing credentials. The tool supports bank sync through SimpleFIN Bridge and CSV/OFX/QFX file imports, running entirely locally with no API keys required for core functionality.", "body_md": "Give AI agents read-only access to your finances without giving anyone your data: one SQLite file on your machine, no cloud, exact integer-cent answers over MCP.\n\nEverything runs locally. Bank access (via SimpleFIN Bridge) is read-only by\nconstruction — Ledgerline never sees your banking credentials and cannot\nmove money. Account numbers are dropped at parse time, so the model can\nnever see what the database never contains. Delete the one `.db`\n\nfile and\nevery trace is gone.\n\nNo clone, no signup, no API key, no real financial data — the demo seeds six\nmonths of clearly fabricated transactions so you can evaluate everything\nbefore connecting anything. [uv](https://docs.astral.sh/uv/getting-started/installation/)\nis the only prerequisite.\n\n```\nuvx --from ledgerline ledgerline demo\nuvx --from ledgerline ledgerline summary    # income/outflow by category\nuvx --from ledgerline ledgerline upcoming   # expected charges, next 30 days\n```\n\n`demo`\n\nprints copy-paste one-liners that connect the MCP server to Codex or\nClaude Code; then ask things like \"What recurring charges are coming up?\" or\n\"Why was last month so expensive?\". When you're done evaluating, delete\n`data/ledgerline.db`\n\nand start fresh with real data below. (`demo`\n\nrefuses\nto write into a database that already has transactions.)\n\n```\ngit clone https://github.com/jeraldhu-yuan/ledgerline\ncd ledgerline\nuv sync\n```\n\nThen get transactions in. Both paths work, and they can be mixed freely — the importer deduplicates.\n\n**Bank sync.** Sign up at [https://bridge.simplefin.org](https://bridge.simplefin.org) (SimpleFIN Bridge,\na small paid service that turns your bank logins into read-only transaction\nfeeds — Ledgerline never sees your banking credentials), link your bank(s),\nand create a new app on your account page to get a one-time setup token.\nThen:\n\n```\nuv run ledgerline connect    # paste the setup token when prompted\nuv run ledgerline sync       # pull your transactions\n```\n\n`connect`\n\nstores the resulting access URL owner-only in\n`~/.config/ledgerline/simplefin.env`\n\n. The first `sync`\n\nprompts to map each\nbank account to a local label; re-running is always safe, and a stale\ndatabase catches up in provider-friendly 45-day windows. If an institution\nis missing from SimpleFIN's catalog, that account just stays on file\nimport — mixing both paths is a supported steady state.\n\n**File import.** Download a CSV/OFX/QFX export from your bank's website:\n\n```\nuv run ledgerline ingest export.csv --account \"Checking\"\n```\n\nThe database lives at `data/ledgerline.db`\n\n(gitignored); override with\n`--db`\n\nor `LEDGERLINE_DB`\n\n. No API key is needed for any of this — the two\noptional embedded LLM commands (`categorize`\n\n, `ask`\n\n) read\n`ANTHROPIC_API_KEY`\n\nfrom the environment, and everything else runs keyless.\n\nLedgerline runs as a local stdio MCP server exposing read-only tools: data freshness, transaction search, spending summaries, period comparisons, account balances, upcoming payments, and constrained SQL — plus a small set of local-metadata writers so the everyday upkeep works from chat: \"I just signed up for a $850/month course, track it\", \"I cancelled Netflix\", \"that charge is business travel, not dining\", \"mark this card as business\". Your agent does the categorizing too, so no Anthropic API key is needed on the MCP path. Nothing can touch the bank. The contract is deliberately small and uniform — exact integer cents, totals always per currency and never combined, and limitations (staleness, uncategorized spend, unknown account purpose) reported as data rather than prescriptive workflow text. The reasoning is the client model's job; the server's job is exact, truthful primitives.\n\nThe one cache-writing tool, `refresh_data`\n\n, pulls from SimpleFIN at most\nonce an hour. A refresh that hits provider errors is recorded as an attempt\nbut not a success, and `data_status`\n\ndiscloses the difference.\n\n```\n# Codex (user scope)\ncodex mcp add ledgerline --env LEDGERLINE_DB=/absolute/path/to/ledgerline.db -- \\\n  uvx --from ledgerline ledgerline-mcp\n\n# Claude Code (user scope)\nclaude mcp add ledgerline --scope user --transport stdio \\\n  --env LEDGERLINE_DB=/absolute/path/to/ledgerline.db -- \\\n  uvx --from ledgerline ledgerline-mcp\n```\n\n(From a repo checkout, point the command at\n`/path/to/ledgerline/.venv/bin/ledgerline-mcp`\n\ninstead of `uvx`\n\n.) Restart\nthe client, then ask things like \"How much did I spend on dining in\nJanuary?\" or \"What recurring charges are coming up?\"\n\nIf an AI agent is doing the setup, every step is non-interactive except the ones that belong to the human:\n\n**Human only:** signing up at SimpleFIN Bridge, linking banks, and creating the setup token all happen on SimpleFIN's website with real banking credentials. Never give an agent your bank login; the tool never sees it either.- The human hands the agent only the one-time setup token. Then:\n\n```\nledgerline connect --token \"$SETUP_TOKEN\"   # claim it, no prompt\nledgerline sync --accept-default-labels     # map new accounts automatically\n```\n\n- The setup token is single-use: it is consumed by\n`connect`\n\nand worthless afterward, so it transiting an agent's context is bounded risk. The resulting access URL is stored owner-only on disk and never enters the model. - The demo path (\n`ledgerline demo`\n\n) and file import (`ledgerline ingest`\n\n) have no prompts at all, and`demo`\n\nprints the exact`codex mcp add`\n\n/`claude mcp add`\n\ncommands to wire up the MCP server. When registering, pick a server name that doesn't collide with one the user already has.\n\n```\n# Monthly summary: income/outflow by category, top merchants, deltas\nuv run ledgerline summary --month 2026-06\n\n# Resolve uncached merchants with ONE batched LLM call\nuv run ledgerline categorize\n\n# Confirm/correct categories; corrections apply retroactively\nuv run ledgerline review\n\n# Recurring payments\nuv run ledgerline recurring detect\nuv run ledgerline recurring add --label \"Course tuition installment\" \\\n    --amount 850.00 --cadence monthly --day 21\nuv run ledgerline upcoming --days 30\n\n# Embedded Q&A for use without an MCP client (needs ANTHROPIC_API_KEY)\nuv run ledgerline ask \"why was June so expensive?\"\n\n# CSV dump for analysis elsewhere\nuv run ledgerline export --month 2026-06 --out june.csv\n\n# Durable account context for agents and reports\nuv run ledgerline accounts set-context \"Chequing\" --purpose mixed \\\n  --entity \"Northwind Consulting\" --business-use-percent 70 \\\n  --context \"Business income plus personal spending\"\n```\n\nAccount context (`personal`\n\n/`business`\n\n/`mixed`\n\n/`unknown`\n\n, owning entity,\nbusiness-use percentage, free-form note) persists in SQLite and rides along\non every MCP result, so agents segment cash flow before judging it.\n\nIf your bank's CSV doesn't auto-detect, the fix is a ~10-line pull request:\nadd one dict to `PROFILES`\n\nin\n[ ledgerline/ingest/profiles.py](/jeraldhu-yuan/ledgerline/blob/main/ledgerline/ingest/profiles.py). OFX/QFX\nneeds no profile.\n\n```\n\"us_checking\": {\n    \"columns\": {\"date\": \"Posting Date\", \"amount\": \"Amount\", \"description\": \"Description\"},\n    \"date_format\": \"%m/%d/%Y\",\n    \"sign\": 1,            # -1 if the export shows charges as positive\n    \"skip_rows\": 0,\n    \"external_id_column\": None,  # column with a bank-side unique id, if any\n},\n```\n\nInclude a small fabricated CSV fixture (invented merchants, never real\naccount data) in `tests/fixtures/`\n\nand a test asserting it ingests with the\nright sign convention — see `test_sign_convention_profile`\n\nin\n[ tests/test_ingest.py](/jeraldhu-yuan/ledgerline/blob/main/tests/test_ingest.py) for the pattern.\n\nRe-importing a file, overlapping export ranges, and sync + file import of the\nsame period all produce zero duplicates (tested in `tests/test_ingest.py`\n\nand\n`tests/test_sync.py`\n\n).\n\n**Design note — one deliberate deviation from the spec:** the spec folds\nFITID into `dedupe_hash`\n\nwhen present. Done literally, that would *create*\nduplicates in mixed mode: a CSV row (no FITID) and a SimpleFIN row (with id)\nfor the same transaction would hash differently. Instead:\n\n`dedupe_hash = sha256(account_id | posted_date | amount_cents | merchant_raw | occurrence_index)`\n\nwith occurrence counting — the Nth identical row in a batch is a duplicate only if the DB already holds more than N such rows. Two genuinely distinct same-day, same-amount, same-merchant transactions survive because they arrive in the same export with occurrence indexes 0 and 1.- Bank-side ids (OFX FITID, SimpleFIN txn id) are stored in\n`external_id`\n\nwith a unique per-account index, short-circuit re-imports, and are backfilled onto rows that originally arrived without one.\n\nThis satisfies every acceptance test, including both orders of mixed-mode. Caveat: cross-source dedupe matches on the raw description, so it works when both sources export the same description string (typical for OFX/SimpleFIN from the same institution).\n\n`data/`\n\n,`*.db`\n\n,`*.csv`\n\n,`*.ofx`\n\n,`*.qfx`\n\n,`.env`\n\n,`analysis/`\n\n, and`*.ipynb`\n\ngitignored from the first commit; test fixtures and`demo`\n\ndata are fabricated only.- Account numbers are never parsed: the OFX reader and SimpleFIN connector\ndrop\n`ACCTID`\n\n/`BANKID`\n\n-class fields at parse time. Only short labels (\"US Checking\") identify accounts. Asserted in`tests/test_security.py`\n\n. - The model gets full transaction detail through\n`run_sql`\n\n— by design. What it can never see is what the DB never contains: account numbers, credentials, raw export files. `run_sql`\n\n: read-only connection (`mode=ro`\n\nURI), single-statement SELECT/WITH only, keyword denylist, SQLite authorizer denying everything but reads, 200-row cap, 5-second time limit, statement/result size limits. Literals and comments are stripped before the keyword scan (a merchant named \"UPDATE\" is not a false positive); the authorizer and read-only mode are the real guards. Tested with hostile inputs.- SimpleFIN access URL from\n`SIMPLEFIN_ACCESS_URL`\n\nor a`0600`\n\nconfig file only — never the repo, the DB, or the LLM context.`https`\n\nis required, HTTP redirects are refused (credentials are never replayed to another host), and loose file permissions produce a warning. - New database files are created owner-only (\n`0600`\n\n). `ANTHROPIC_API_KEY`\n\nfrom env only; LLM steps fail loudly without it, everything else runs keyless.\n\n```\nuv run pytest\n```\n\nThe suite covers the acceptance checklist: mixed-mode dedupe in both\norders, quarantine of malformed rows, integer-cents math, per-currency\nreporting, `run_sql`\n\nhardening against hostile inputs, recurring detection\nwith gap tolerance, the MCP tools, the demo seeder, and the security\ninvariants above.\n\nMIT — see [LICENSE](/jeraldhu-yuan/ledgerline/blob/main/LICENSE).", "url": "https://wpnews.pro/news/show-hn-ledgerline-local-first-finance-mcp-server", "canonical_source": "https://github.com/jeraldhu-yuan/ledgerline", "published_at": "2026-06-16 15:32:32+00:00", "updated_at": "2026-06-16 15:49:23.788147+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-safety", "ai-products"], "entities": ["Ledgerline", "SimpleFIN Bridge", "SQLite", "Claude Code", "Codex", "Anthropic"], "alternates": {"html": "https://wpnews.pro/news/show-hn-ledgerline-local-first-finance-mcp-server", "markdown": "https://wpnews.pro/news/show-hn-ledgerline-local-first-finance-mcp-server.md", "text": "https://wpnews.pro/news/show-hn-ledgerline-local-first-finance-mcp-server.txt", "jsonld": "https://wpnews.pro/news/show-hn-ledgerline-local-first-finance-mcp-server.jsonld"}}