If you've tried to give an LLM agent access to your own bank account, you've probably hit the same wall every time: PSD2 says banks must expose account information APIs, but actually calling one usually requires an eIDAS QWAC certificate — a €500–700/year artefact gated behind KYC, a qualified trust service provider, and a CA that treats developers like compliance risks. Agents can read your GitHub, your calendar, your email… and then bounce off the bank.
I maintain open-banking.io, and I wrote previously about why agents can't reach banks. This post is the constructive follow-up: how to actually do it, by wiring a cert-free open-banking API into an MCP server that Claude (or any MCP-aware client) can call as a tool. Disclosure up front — I'm the maintainer, so judge the bias accordingly. The pattern works with any redirect-based AIS provider; I'm just using the one I know best.
Why MCP is the right shape for this
Model Context Protocol tools are a good fit for bank data for three reasons:
-
Read-only by design. AIS (Account Information Services) is inherently a read operation. Mapping
get_balances/get_transactionsto MCP tools makes the blast radius obvious in a way that a raw HTTP client does not. - Consent is a distinct step. Open banking uses an OAuth-style redirect flow where the human logs into their bank and approves scopes. MCP's tool-call boundary matches this: the agent never sees credentials, it only sees the result of an approved session.
- Composability. Once balances and transactions are tools, the agent can reason across them ("sum last 30 days of grocery spend") without you writing a single analytic endpoint.
The catch is that you still need someone to hold the eIDAS cert and run the consent redirect. That's the wedge cert-free aggregators occupy: they hold the certificate and expose a plain REST API to you, so your MCP server never touches eIDAS at all.
The architecture
┌──────────┐ MCP (stdio/HTTP) ┌─────────────────┐ REST + OAuth2 ┌────────────────┐
│ Claude │ ───────────────────► │ your MCP server │ ───────────────► │ cert-free AIS │
│ Desktop │ ◄─────────────────── │ (FastMCP, Py) │ ◄─────────────── │ (open-banking │
└──────────┘ tool results └─────────────────┘ JSON │ .io) │
└───────┬────────┘
│ holds eIDAS QWAC
┌──────▼──────┐
│ EU bank │
│ PSD2 API │
└─────────────┘
The bank still sees a PSD2-regulated call with a valid QWAC. Your agent sees a JSON object. The certificate "tax" is paid once, upstream, by the aggregator — not per developer.
A minimal MCP server
Here's a working FastMCP server in ~60 lines. It exposes three tools: list supported banks, start a consent flow, and read data once consent is granted.
# eu_bank_mcp.py
import os, webbrowser
from mcp.server.fastmcp import FastMCP
import httpx
API_BASE = os.environ["OBI_API_BASE"] # e.g. https://api.open-banking.io
API_KEY = os.environ["OBI_API_KEY"]
SESSION_TOKEN = None # cached after consent; in prod, persist to a secrets store
mcp = FastMCP("eu-bank")
def _headers():
h = {"Authorization": f"Bearer {API_KEY}"}
if SESSION_TOKEN:
h["X-Consent-Token"] = SESSION_TOKEN
return h
@mcp.tool()
def list_banks(country: str = "DK") -> list[dict]:
"""List banks available for connection in a given ISO country code."""
r = httpx.get(f"{API_BASE}/v1/banks", params={"country": country}, headers=_headers(), timeout=15)
r.raise_for_status()
return [{"id": b["id"], "name": b["name"]} for b in r.json()["banks"]]
@mcp.tool()
def connect_bank(bank_id: str) -> str:
"""Start the redirect consent flow for a bank. Opens the user's browser.
Returns the consent URL so the agent can surface it to the human."""
global SESSION_TOKEN
r = httpx.post(f"{API_BASE}/v1/consent", json={"bank_id": bank_id}, headers=_headers(), timeout=15)
r.raise_for_status()
consent_url = r.json()["consent_url"]
SESSION_TOKEN = r.json().get("pending_token")
webbrowser.open(consent_url) # the HUMAN authenticates here, not the agent
return f"Open this URL in a browser and approve: {consent_url}"
@mcp.tool()
def get_balances() -> list[dict]:
"""Return balances for all connected accounts. Requires prior consent."""
r = httpx.get(f"{API_BASE}/v1/balances", headers=_headers(), timeout=15)
r.raise_for_status()
return r.json()["balances"]
@mcp.tool()
def get_transactions(account_id: str, days: int = 30) -> list[dict]:
"""Return transactions for an account over the last N days."""
r = httpx.get(
f"{API_BASE}/v1/transactions",
params={"account_id": account_id, "days": days},
headers=_headers(), timeout=15,
)
r.raise_for_status()
return r.json()["transactions"]
if __name__ == "__main__":
mcp.run() # stdio transport by default
Note what's not here: no private key, no CSR, no certificate renewal cron, no openssl incantations. The eIDAS burden is a problem the aggregator already solved.
Point Claude Desktop at it
Drop this into claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/):
{
"mcpServers": {
"eu-bank": {
"command": "python",
"args": ["/absolute/path/to/eu_bank_mcp.py"],
"env": {
"OBI_API_BASE": "https://api.open-banking.io",
"OBI_API_KEY": "sk_live_your_key"
}
}
}
}
Restart Claude, and the tools appear in the function list. A natural conversation now works:
You: Connect my Danish bank, then tell me how much I spent on groceries last month.
Claude: I'll call
list_banks("DK")… foundnordfyns-bank. Callingconnect_bank— please approve in the browser that just opened. Once you're back, I'll pull balances and 30 days of transactions and categorize them.
The redirect flow is the key safety property: the agent never sees the bank login. It only receives a consent token after a human completes authentication at the bank's own domain.
Cert-based vs cert-free MCP access
| Direct PSD2 API (you hold eIDAS) | Cert-free aggregator + MCP | |
|---|---|---|
| Upfront cost | €500–700/yr + KYC | ~€3/mo per bank, no KYC on you |
| Time to first call | 2–8 weeks (CA issuance) | minutes |
| Certificate ops | renewals, key rotation, HSM | none |
| What the agent sees | raw SCA + signed calls | clean REST → MCP tools |
| Compliance surface | you're the regulated-ish party | aggregator is regulated |
| Best for | large regulated TPPs | indie builders, agents, personal finance |
For an agent that just needs to read balances and transactions to answer questions, the right column wins almost every time.
Security notes worth stating plainly
- Scope to AIS only. Never wire PIS (payment initiation) into an agent tool casually. Read access is recoverable; initiated payments are not. If you need payment tools, gate them behind a human-in-the-loop confirmation step, not silent agent autonomy.
- Cache the consent token securely. The sketch above uses a global for clarity; in production, persist it to the OS keychain or a secrets manager. Treat it like a session cookie — it is one.
-
Consent expires. EU PSD2 consents max out at 90 days by default and many banks cap at 30. Your server should detect a 401 on read calls and re-trigger
connect_bankrather than silently failing. -
Log the agent's tool calls. The first time Claude calls
get_transactionson its own initiative, you'll want an audit trail. FastMCP emits per-call events; ship them somewhere you control.
What this unlocks
Once bank data is just tool calls, the things agents can do get interesting fast: recurring-transfer detection ("you have three subscriptions you haven't used in 60 days"), cash-flow forecasting from transaction history, cross-account net-worth summaries, and — the one I'm most excited about — letting a personal-finance agent reconcile against a budgeting tool without screen-scraping or CSV exports.
The barrier was never the AI. It was the certificate. Removing it from the agent's path is what makes bank-aware agents practical today.
Disclosure: I maintain open-banking.io, the cert-free aggregation API used in the examples above. The MCP pattern works with any redirect-based AIS provider. Docs and a free tier to try the consent flow end-to-end are at open-banking.io.
Top comments (0)