Home · Guides

Categorize and reconcile a bank CSV export from chat

A bank export is a CSV with a preamble the bank added for humans, a header row somewhere under it, and amounts written the way that bank's country writes numbers. "Import this Revolut export and categorize it" is one sentence into the MCP Bank Statement server, and what comes back is a stored ledger: one row per transaction, a sign that means the same thing on every row, and a category on the ones your rules already cover. Nothing is uploaded. The file is read once, on your machine, into ~/.local/share/mcp-servers/bank-statement/.

Install

claude mcp add bank-statement -- npx -y @theluckystrike/mcp-bank-statement

Cursor, in ~/.cursor/mcp.json:

{
  "mcpServers": {
    "bank-statement": {
      "command": "npx",
      "args": ["-y", "@theluckystrike/mcp-bank-statement"]
    }
  }
}

Claude Desktop and the other clients take the same block under their own config file; see the setup pages for the exact file and key per client.

Seven shapes of the same CSV

statement_import ships a profile for Revolut, Wise, mBank, PKO BP, ING and N26, plus a generic reader that works from the headers alone when a bank matches none of the six. Each one is a real, measured quirk read off that bank's own export, not a guess:

statement_import reports back what it detected (the bank, the header line, the date order, the number locale), what it stored, how many lines were duplicates of what is already there, and every line it skipped with the reason, rather than silently dropping a row it could not parse.

Locale amounts, read correctly once

A number is only unambiguous if you know which country wrote it. 1 234,56 from an mBank export and 1,234.56 from a US bank both mean one thousand two hundred and thirty four units and a bit, but a parser that assumes one style over the other turns a Polish grocery bill into a number a thousand times too small or too large. The importer sniffs the locale from the file's own delimiter and decimal mark rather than assuming a locale from the bank name, and every amount is stored the same way regardless of source: a debit negative and a credit positive, once, at import, so nothing downstream has to re-derive the sign from a column that might not exist for a different bank.

The dedupe insight: an occurrence index, not a row index

Re-importing the same file should add nothing, and two identical purchases on the same day should still be two transactions, not one. Those two requirements pull in opposite directions, and the naive fix for each breaks the other. A key built from date, amount and description alone collapses two identical EUR 3.50 coffees bought on the same day into a single stored transaction, silently understating the month by one coffee. Adding a plain row index instead breaks re-import, because a bank that reorders its export, or inserts one late-settling line, shifts every index after it, so the same transaction gets a different key the second time the file is imported and is stored twice.

The key that works is date, amount, currency, account and description, plus the count of identical rows seen so far in this file, compared against the count of identical rows already stored: the Nth identical line in the new file matches the Nth identical line already on record. Measured on a 60-row fixture: 60 transactions stored on the first import, 60 duplicates reported and 0 stored on re-importing the exact same file. Two identical coffees on one day still come out as two rows. Eight concurrent imports of one file across two server processes on the same data directory still leave exactly the transactions that file actually contains, because the occurrence count is read inside the same file lock as the write, not computed and then written separately.

Rules that categorize as transactions arrive

category_rules sets or lists the rules that assign a category. A plain rule is a case-insensitive substring match against the description or counterparty. Setting regex: true compiles the pattern, but only if it cannot backtrack exponentially against a crafted description; a pattern shaped like (a+)+ is refused and the rule falls back to matching as a literal substring instead of hanging the server on one bad rule. Setting rules re-applies them to every stored transaction immediately, so a rule added after import still categorizes the month you already brought in. transaction_categorize sets the category directly by transaction id when a rule is not worth writing for a one-off.

Summaries that never mix currencies

statement_summary reports money in, money out and the net for a date range, grouped by category, month, account or counterparty. Every total is kept per currency rather than added together, because 100 EUR plus 100 USD is not 200 of anything real. Import a Revolut account holding EUR and USD alongside a PKO BP account in PLN and the summary comes back as three separate totals, not one wrong one.

Reconciling against the expense tracker

The point of a receipt is that it should show up on the bank statement, and the point of a bank line is that some receipt should explain it. reconcile_expenses matches bank debits against the receipts logged in the expense tracker by the same currency, the same amount, inside a date window, and reports three things: matches, bank lines with no receipt behind them, and receipts that never reached the bank at all, which usually means a card payment that has not settled yet or an expense logged against the wrong account. The expense ledger is only ever read; reconciling never writes anything into it. On a seeded test store this found one match one day apart and one expense with no matching bank line, which is exactly the shape of a receipt logged on the day of purchase against a card payment that settled the next day.

Finding the subscriptions

recurring_detect looks for the same counterparty charging an amount that barely moves on a steady cadence, and reports the cadence, the typical amount, the next expected date and the cost annualised. On the 60-row test fixture it found a monthly Spotify charge, three occurrences, EUR 9.99, annualised to EUR 119.88. That annualised figure is the number worth asking for once a quarter: a monthly charge nobody looks at again is the easiest line item to forget you are still paying.

Free tier and Pro

Import always stores every row regardless of tier; silently dropping lines on the way in would make the stored ledger disagree with the actual bank statement. Free covers 2 accounts, the last 12 months read back, 5 category rules, and unlimited import, list, search and summary. Pro ($19 once, lifetime) removes the account and history limits, opens unlimited rules, reconcile_expenses, recurring_detect and statement_export to csv or json. Full detail on the MCP Bank Statement page, and the general free versus Pro comparison.

Questions

Which banks does it read directly?

Revolut, Wise, mBank, PKO BP, ING and N26 each have a dedicated profile tuned to that bank's own column names, sign convention and preamble. Any other bank falls back to a generic reader that finds the header row and either a signed amount column or separate debit and credit columns.

Will importing the same file twice double my numbers?

No. Every row is keyed on its date, amount, currency, account, description and the count of identical rows seen so far, so a second import of an unchanged file stores 0 new transactions and reports the rest as duplicates. Two genuinely identical purchases on the same day still both count.

How are amounts in a different locale handled, like 1 234,56 from mBank?

The importer detects the file's own delimiter and decimal mark rather than assuming one locale for every bank, so 1 234,56 and 1,234.56 both resolve to the same underlying amount. A debit is stored negative and a credit positive at import, once, so nothing downstream has to guess the sign again.

Can it tell me which receipts never showed up on the bank statement?

Yes, with reconcile_expenses, a Pro feature. It matches bank debits against the receipts logged in the expense tracker by currency, amount and a date window, and reports matches, unmatched bank lines and unmatched receipts separately. The expense ledger is only ever read.

Is any of this uploaded anywhere?

No. The server makes no network calls at all, not for import, not for licensing. Statements are parsed locally and stored in plain JSON under ~/.local/share/mcp-servers/bank-statement/; deleting that directory resets it.

Related

All MCP servers and prices · All guides