Read, search and export your bank statements and balances from your AI
assistant, through Enable Banking, a licensed
PSD2 provider for banks across Europe. It moves no money: besides reading, it
only starts, stores and ends consents. start_auth is its one money tool,
because it asks your bank for 180 days of read access; your client asks you
before each call.
You need an Enable Banking application (its id, a registered redirect URL and its private key), and the account holder, to approve each consent in their bank's app every 180 days. A bank allows about four reads of an account a day without the holder present, shared by every program on that consent, so read a period once and narrow it here.
What you can ask
- "Which bank consents do we have, and when does each one run out?"
- "What did we pay Example Supplier SRL this year?"
- "Show incoming payments over 1,000 RON in September, across all our accounts."
- "What is the available balance on our EUR account?"
- "Export last quarter's transactions as CSV for our accountant."
- "Our consent with Example Bank ends next week. Renew it."
Install
Install in VS Code Install in Cursor Download for Claude Desktop
With Claude Code:
claude mcp add enable-banking -e ENABLE_BANKING_CONFIG="<path to Enable Banking app.json>" -- npx -y @foreverworks/mcp-enable-banking
Claude Desktop (Settings, Developer, Edit Config) and most other clients read the same command as JSON:
{
"mcpServers": {
"enable-banking": {
"command": "npx",
"args": ["-y", "@foreverworks/mcp-enable-banking"],
"env": {
"ENABLE_BANKING_CONFIG": "<path to Enable Banking app.json>"
}
}
}
}
Each runs the published package on Node 22.19 or newer. VS Code and the Claude Desktop bundle ask for each setting as they add the server; leave an optional one empty. Cursor reads each from your environment instead (${env:ENABLE_BANKING_CONFIG} and so on), so set them before you start Cursor. The commands above set only what is required; Configuration has the rest.
From a clone
git clone https://github.com/foreverworks/mcps.git && cd mcps
npm ci && npm run build
claude mcp add enable-banking -e ENABLE_BANKING_CONFIG="<path to Enable Banking app.json>" -- node "$PWD/servers/enable-banking/dist/index.js"
In a JSON configuration, the command is node and its one argument the full path of servers/enable-banking/dist/index.js. To share one running copy between several clients over HTTP, see docs/deploying.md.
Configuration
| Variable | Required | Meaning |
|---|---|---|
ENABLE_BANKING_CONFIG |
yes | Path of app.json (below), which the server only reads. |
ENABLE_BANKING_KEY |
no | Path of the application's private key (PEM). When unset, app.json's private_key names it, relative to app.json's folder. |
ENABLE_BANKING_STATE_DIR |
no | Where the consents (sessions.json), their backup and their lock live; the default is in docs/configuration.md. |
ENABLE_BANKING_EXPORTS_DIR |
no | Where export_transactions writes its CSV files. Default: exports/ in the state folder. |
app.json holds the application's details from the control panel:
{
"application_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"application_name": "Example Books",
"environment": "sandbox",
"redirect_url": "https://example.com/bank-callback",
"private_key": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.pem"
}
application_id and redirect_url are required, and private_key too unless
ENABLE_BANKING_KEY is set. redirect_url must match a registered one
exactly. application_name and environment only label health's answer;
the application_id decides sandbox or production. api_base overrides
https://api.enablebanking.com for tests or a proxy; other keys are ignored.
Under systemd, a credentials directory renames files to their credential
names, so set ENABLE_BANKING_KEY there. The variables every server shares
are in docs/configuration.md.
Getting the application and its key
- In Enable Banking's Control Panel, open API applications from the top menu. In "Add a new application", keep the Sandbox environment: its test banks let you try every tool without a real account.
- Name the application (the account holder sees the name) and enter a
redirect URL. Any page you control works:
complete_authonly needs the address the browser lands on. - Keep the default private key option and press Register. Your browser
generates the key and saves it as
<application id>.pem; move it where only this server can read it, and writeapp.jsonbeside it.
Production is a second application, registered with the Production environment. It starts Inactive; "Activate by linking accounts" makes it active in restricted mode, which reads only your own linked accounts until you sign a contract with Enable Banking.
First run
- Ask your assistant to run Enable Banking's
healthcheck. It names the application, its environment, whether it is active and its redirect URLs, and counts the stored consents. A key that does not belong to theapplication_idfails here. - Find your bank with
list_aspspsfor its country.start_authtakes itsname;psuTypeslists its account types (business,personal), andmaxConsentDaysits longest consent. - Ask for
start_authwith that name, the country and thepsu_type, and approve it. The link it returns is for the account holder: plan five minutes with the phone that holds the bank's app. - Open the link, sign in to the bank, and tick every account it offers. The request names no accounts, which asks for all of them.
- The bank sends you back to the redirect URL with
?code=.... Give that address (or the code) tocomplete_authat once: the code works once, for minutes. Its answer lists the accounts and the consent's end date.
To renew a consent (list_stored_sessions says how many days it has left),
repeat steps 3 to 5 with its bank, country and type. Without a label, the
new consent takes the old one's place and label.
From a shell, node dist/index.js --health prints one ENABLEBANKING: line
and exits 1 when a person has to act or 2 when Enable Banking was unwell,
which suits a daily timer.
Tools
read changes nothing at the bank or in the stored consents (an export writes only its file); write changes the stored consents; money asks a bank for access to your accounts.
| Tool | Access | What it does |
|---|---|---|
health |
read | Whether Enable Banking accepts the key, as which application and in which environment, where the files are, and how many consents are stored. |
list_aspsps |
read | The banks Enable Banking reaches in a country (empty for every market): the name start_auth takes, the account types each offers and its longest consent. Cached an hour. |
list_stored_sessions |
read | Every stored consent by label, with its bank, its accounts and when it expires, as a date and as days left. |
start_auth |
money | Starts a consent with a bank, for every account it offers, for 180 days. Returns a link the account holder must open. |
complete_auth |
write | Finishes a consent from the bank's redirect, stores it under a label, and reports its accounts and its end date. |
revoke_session |
write | Ends a stored consent at the bank and removes it here, keeping the previous file as a backup. |
list_accounts |
read | The accounts a consent reaches today, asked of the bank, each with the uid the other tools take. |
get_transactions |
read | A statement for one account, every account on a consent, or every stored account, with exact signed amounts, narrowed by text, direction and amount without another read at the bank. |
export_transactions |
read | The same statement written as a CSV file, for a long period, many accounts or an accountant. Returns the file's path, size and SHA-256. |
get_balances |
read | Every balance the bank reports for one account (booked, available and the others it sends), each an exact amount with its PSD2 code and its meaning. |
Money
start_auth moves no money, but it grants 180 days of read access, which
only the account holder should decide. It is marked money in tools/list,
so your client asks you before each call (Claude Code does, in every
permission mode). With MCP_CONFIRM=elicit the server asks you itself, in one
line computed from the arguments:
enable-banking: Bank consent (PSD2, read-only: balances and transactions) with Example Bank (DE), personal accounts, every account the bank offers, valid 180 days: until 2030-07-01 if confirmed today
The end date is computed when you are asked and again when the call runs, so a later confirmation moves it by the delay. Access starts when the account holder approves in their bank.
Troubleshooting
config: "ENABLE_BANKING_CONFIG is not set", or a message naming
app.json. Set ENABLE_BANKING_CONFIG to a readable app.json, and
ENABLE_BANKING_KEY (or private_key) to the key.
auth: "cannot use the private key at ...". The key file is missing,
unreadable or not an RS256 private key. Point ENABLE_BANKING_KEY or
private_key at the .pem the control panel saved; the next call reads it
again, with no restart.
api with status 401 or 403. Enable Banking refused the signed token:
the key does not belong to the application_id (a sandbox key beside a
production id, say).
api with reason EXPIRED_SESSION, REVOKED_SESSION, CLOSED_SESSION
or SESSION_DOES_NOT_EXIST, or status 422 or 404, on a call that worked the
day before. The consent lapsed or was revoked in the bank's app, and
retrying will not bring it back. Renew it with steps 3 to 5 of
First run.
api from complete_auth with reason EXPIRED_AUTHORIZATION_CODE,
WRONG_AUTHORIZATION_CODE or outcome_unknown. The code works once, for
minutes, and a dropped exchange cannot be repeated. Run start_auth again and
call complete_auth right after approving.
api with reason: "ASPSP_RATE_LIMIT_EXCEEDED". The account's reads are
spent for the day. Wait a day, then read each period once and narrow it with
the filters.
input: "the bank refused the consent: ...". The account holder declined,
or the bank failed; start again with start_auth. "no code parameter
there" means you pasted start_auth's link, not the address the bank sent
you back to.
How it works
Statements
Amounts stay decimal strings, never JavaScript numbers, which keep only 15
to 17 significant digits (9007199254740993.01 becomes 9007199254740992).
Enable Banking sends an unsigned amount and a credit_debit_indicator;
get_transactions signs the text, negative for money out. A row whose amount
came as a JSON number, or not as a plain decimal, carries an amountWarning.
Search runs here, after the read: a bank only returns every row in a date
range. A filtered answer echoes the filter, with fetched (rows read) beside
count (rows matched).
textlooks in the counterparty's name and IBAN, the payment reference and the entry reference, ignoring case, diacritics and an IBAN's spaces:santierfindsȘANTIER EXEMPLU SRLand the cedillaŞantiersome banks still send.directioniscredit(money in) ordebit(money out).amount_minandamount_maxbound the size, sign ignored, inclusive, as exact decimals; a row whose amount could not be read exactly is left out and counted inamountUnreadable.date_fromanddate_toare inclusive; empty is unbounded.transaction_statusdefaults tobookedand takes a name (pending,cancelled, ...) or a PSD2 code; empty leaves the bank's default.
Several accounts are read one at a time, since banks throttle bursts,
and each row names its account. A refused account appears in accounts with
its error and remedy, beside the others' rows. Arguments are checked before
the first read, an account uid (a UUID that changes with every consent) for
its form, so a typo spends nothing. Pages are followed to the end, up to 200.
The counterparty is the other side. On a debit, debtor_account is your
own IBAN, so the side is picked by direction. Remittance lines are joined with
one space, because banks split references anywhere.
Exports have the columns account_label, account_iban, booking_date,
value_date, direction, amount, currency, counterparty_name,
counterparty_iban, remittance, reference, status, bank_code,
merchant_category_code, amount_warning. Every cell is quoted; one a
spreadsheet would run as a formula (starting with =, + or @) gets a
leading apostrophe, but a negative amount does not. Each file is named for its
scope, dates and creation time, so it never replaces one someone has open,
and is written at mode 0640.
Consents and sessions.json
A consent is stored before anything else, because Enable Banking reveals
a session id exactly once, and another costs the account holder a new
authentication. complete_auth exchanges the code, stores the result, then
asks for each account's details; if that fails, the consent stands and the
account carries a detailsError. A new bank's consent is labelled
<bank>_<country> (example_bank_de) unless you give a label.
sessions.json is a format other programs can read, defined in
src/domain/sessionsFile.ts:
- an object keyed by label (
a-z 0-9 _ -, an identifier anywhere); - each entry the
POST /sessionsresponse verbatim:session_id,accounts(withuid,account_id.iban,identification_hash, ...),aspsp,psu_type,access.valid_until; - two-space JSON at mode 0600, written atomically under a lock, other labels
carried over. The previous file is re-serialised into
sessions.json.bakfirst, or kept aside assessions.json.unreadable-<ms>if it is not a JSON object.
identification_hash identifies an account across consents, linking a
renewal to the history of the one it replaced. When an exchange lacks such a
field, complete_auth stores the consent anyway and reports formatWarnings.
Ending a consent closes the session at Enable Banking, which ends the bank's consent, then removes the label; one the bank already ended is removed too. If the bank refuses, nothing changes here.
Requests
There is no grant to refresh. Each attempt carries a JWT signed on the
spot with the private key (RS256, valid five minutes, the application_id as
kid), so a failing health means the key or the id, never an expiry.
src/infra/http.ts tries a read up to four times on a 5xx or a dropped
connection, takes a 401 or a 422 as final, and never resends a POST Enable
Banking may have acted on.
Wire types are generated by openapi-typescript from
Enable Banking's OpenAPI description
(npm run gen:types -w @foreverworks/mcp-enable-banking; read the diff).
src/domain/schema.ts adds two measured adjustments: an optional field may be
null (a statement's last page has continuation_key: null), and an amount
is string | number.
Only the bank catalogue is cached (an hour per country, in memory): it is
the same for everyone. npx tsx scripts/smoke.ts reads one statement over the
last three days from a real consent, and with --consent also renews the
consent, which needs the account holder.
Limitations
- Account information only (AIS): no payment initiation, none planned.
- A consent needs the account holder, to make it and to renew it: the bank's authentication is the point of PSD2.
- Every consent asks for 180 days and every account. A bank with a shorter
maxConsentDaysrefuses it rather than shortening it. - No history of its own (no cache, alerts or categories), so every
statement is a read at the bank. A program that needs fewer reads should
keep its own ledger, which
sessions.jsonis designed to feed. - One application per server: sandbox and production need a configuration each.
Other servers for Enable Banking
Searched 2026-10-21. Enable Banking publishes none: its GitHub organisation has API samples, a command line tool and an eIDAS broker, and its site does not mention MCP. Four community servers read accounts through it:
enable-banking-mcpon npm, the most downloaded (752 in the month to 2026-10-18): stdio on macOS, secrets in the Keychain, for personal, noncommercial use only.bankmcpon npm (555 that month): local, or hosted behind its own sign-in, with watches that call a webhook. Amounts become floating-point numbers.banking-mcpon GitHub: a Cloudflare Worker behind OAuth that keeps balances and transactions in its own database, with categories and exports; banks are linked from a command line, not a tool.bank-mcpby elcukro on GitHub: Enable Banking beside Plaid, Tink and Teller. Amounts become floating-point numbers.
This one exists for what none of them does: amounts stay exact decimal
strings from the bank to the answer; starting a consent is a money tool
whose confirmation names the bank, the accounts and the end date; and
consents are kept by label in a file other programs can read, with a backup of
the previous one and, per account, the hash that ties a renewal to its
history. It lacks what the others add: alerts (bankmcp), a local cache and
categories (banking-mcp), and banks beyond Enable Banking (bank-mcp).