open-banking-mcp
# open-banking-mcp
An MCP server that gives Claude read-only access to UK bank accounts through
[TrueLayer's Data API](https://docs.truelayer.com/docs/data-api-basics) — one
integration covering Monzo, Starling, HSBC, Barclays, Lloyds, NatWest,
Nationwide, Santander and most other UK banks.
Read-only by design: this server requests Data API scopes only. There is no
code path here that can move money.
> **Status:** working against TrueLayer's sandbox. Not yet tried against a
> real bank connection.
## Setup
### 1. Create a TrueLayer console account
1. Sign up at [console.truelayer.com](https://console.truelayer.com) — free,
instant, Google/GitHub SSO or email.
2. New accounts start in the **sandbox** environment, which is what you want
first. Sandbox has a mock bank, no rate limits and no real money.
3. Click **Create App +** and give it a client id: 4–30 lowercase letters and
digits, no special characters. **It cannot be changed later.** Sandbox apps
get a `sandbox-` prefix automatically.
4. Copy the **client secret** from the credentials screen — it is shown once.
You can mint more later under the app's Settings page.
5. Under the app's settings, add a **redirect URI** of
`http://localhost:8080/callback`. This must match byte for byte what the
server sends, including the scheme and path; a mismatch is the single most
common cause of auth failures.
### 2. Configure
```bash
export TRUELAYER_CLIENT_ID=sandbox-yourapp
export TRUELAYER_CLIENT_SECRET=...
export TRUELAYER_REDIRECT_URI=http://localhost:8080/callback
export TRUELAYER_ENV=sandbox # or "production"
```
Optional:
| Variable | Default | Purpose |
|---|---|---|
| `TRUELAYER_SCOPES` | all Data API scopes | Space-separated scope list |
| `TRUELAYER_PROVIDERS` | `uk-cs-mock` (sandbox) | Which banks to offer at consent |
| `TRUELAYER_USE_KEYRING` | `1` | Set `0` to store tokens in a file |
| `TRUELAYER_TOKEN_FILE` | `~/.open-banking-mcp/tokens.json` | Implies file storage |
| `TRUELAYER_CACHE_FILE` | `~/.open-banking-mcp/cache.db` | Local SQLite cache |
| `TRUELAYER_MAX_AGE_HOURS` | `25` | Age past which cached data is flagged stale |
| `TRUELAYER_HISTORY_DAYS` | `365` | How far back a full sync pulls |
| `TRUELAYER_PSU_IP` | unset | End user's IP; lifts rate limits for user-present calls |
### 3. Connect a bank
```bash
open-banking-mcp auth
```
This opens a browser, you pick a bank and consent, and the tokens land in your
macOS Keychain (or a `0600` JSON file on systems without a keyring).
In the sandbox, choose the mock bank and log in with username `john`, password
`doe` — `john1`/`doe1` through `john100`/`doe100` also work for testing
different account shapes. Note that the sandbox reports its provider id as
`mock`, not `uk-cs-mock`, so that is the name `status` and `logout` expect.
```bash
open-banking-mcp status # connected banks + consent countdown
open-banking-mcp logout uk-ob-monzo
```
## How it reads data
Every read is served from a **local SQLite cache** by default, refreshed by a
daily sync. This is not just a speed trick: TrueLayer caches responses for an
hour and throttles unattended callers to roughly 4 calls a day unless the
request carries an `X-PSU-IP` header saying a human is present. An agent
checking your accounts on a schedule is exactly the throttled case, so it reads
from the cache instead.
```bash
open-banking-mcp sync # pull everything; run this daily
open-banking-mcp cache # what's stored, and when it last synced
```
Every response says how old it is:
```json
{
"data": { "current": 12.0, "available": 112.0, "currency": "GBP" },
"as_of": "2026-09-11T16:26:19Z",
"age": "3 hours old",
"source": "cache",
"stale": false
}
```
Past `TRUELAYER_MAX_AGE_HOURS` (default 25 — a daily sync plus slack) `stale`
flips to true and a `warning` field is added, so an agent reporting a balance
can say how current it is rather than implying it is live.
**When you need live figures** — you just sent a payment and want to know if it
landed — every tool takes `fresh=true`, which bypasses the cache, queries the
bank, and writes the result back:
```
get_balance(account_id="...", fresh=true)
get_transactions(account_id="...", fresh=true) # also pulls pending
```
`fresh=true` on transactions needs a specific `account_id`: a live refresh is
per-account, not a whole-portfolio sweep. It pulls pending transactions too,
which is usually what "did my payment go through" actually means.
### Scheduling the daily sync
On macOS, a launchd agent or a cron line is enough:
```
17 6 * * * cd ~/dev/openbanking-mcp && .venv/bin/open-banking-mcp sync >> ~/.open-banking-mcp/sync.log 2>&1
```
## MCP tools
| Tool | What it does |
|---|---|
| `list_banks` | Connected banks and last sync time |
| `list_accounts` / `list_cards` | Accounts and cards |
| `get_balance` / `get_card_balance` | One balance |
| `get_balances` | Every balance plus per-currency totals |
| `get_transactions` | Query by date range, account, or text search |
| `list_standing_orders` / `list_direct_debits` | Recurring payments |
| `get_identity` | Account holder details |
| `sync_now` | Refresh the whole cache |
| `cache_status` | What the cache holds |
All except `list_banks`, `sync_now` and `cache_status` accept `fresh`.
### Connecting it to Claude
```json
{
"mcpServers": {
"open-banking": {
"command": "/absolute/path/to/.venv/bin/open-banking-mcp-server",
"env": {
"TRUELAYER_CLIENT_ID": "sandbox-yourapp",
"TRUELAYER_CLIENT_SECRET": "...",
"TRUELAYER_ENV": "sandbox"
}
}
}
}
```
## Consent expiry
Open Banking consent lasts **90 days** under FCA rules, then you must
re-authorise in a browser — there is no way around this. `status` shows the
countdown per bank and colours it amber at 21 days and red at 7.
## Token storage
Tokens go in the OS keychain by default (service name `open-banking-mcp`, one
entry per bank). A plaintext list of *which* banks are connected — no secrets —
lives at `~/.open-banking-mcp/providers.json`, because keyrings can't be
enumerated.
Set `TRUELAYER_TOKEN_FILE` to use a `0600` JSON file instead, on Linux or CI
where no keyring daemon is running. The server falls back to this automatically
if it finds no usable keyring backend.
## Development
```bash
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
```
## Licence
MIT
TDQS
Scored across 12 tools
Each tool targets a distinct resource or aggregation level: banks, accounts, cards, individual balances, aggregate balances, transactions, standing orders, direct debits, identity, and cache operations. Although get_balance, get_card_balance, and get_balances all relate to balances, their scopes are clearly separated by description.
Most tools follow a predictable verb_noun pattern (list_banks, get_balance, list_standing_orders). Two outliers—sync_now and cache_status—break the pattern slightly, but the overall set remains readable and consistent.
12 tools is well within the typical 3-15 range for a focused domain server and each tool earns its place by covering a distinct part of the open banking read surface.
The server provides broad read-only coverage for open banking data: banks, accounts, cards, balances, transactions, standing orders, direct debits, identity, and cache management. Minor gaps exist—such as payment initiation, payee/beneficiary listing, or individual transaction lookup—but these are likely outside the intended read-only scope.