DEV Community

John Frandsen
John Frandsen

Posted on

Build an MCP server that lets Claude read EU bank data (no eIDAS certificate needed)

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:

  1. Read-only by design. AIS (Account Information Services) is inherently a read operation. Mapping get_balances / get_transactions to MCP tools makes the blast radius obvious in a way that a raw HTTP client does not.
  2. 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.
  3. 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   │
                                                                         └─────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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")… found nordfyns-bank. Calling connect_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_bank rather than silently failing.
  • Log the agent's tool calls. The first time Claude calls get_transactions on 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)