Open Net Worth
Open Net Worth는 직접 운영하는 오픈소스 개인 AI 자산 비서로, 전체 재무를 하나의 원장에 기록하고 사용자가 제어하는 AI가 관리
banking-mcp: Enable Banking 기반 읽기 전용 다중 은행 MCP 서버
Read-only multi-bank MCP server for Claude and Codex via Enable Banking (PSD2 AIS). Self-hosted on Cloudflare Workers, AGPL-3.0.
금융JavaScript · TypeScriptGitHub @matcha-ai-test직접 설치운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
설치형 프로젝트는 배포 도메인 없이도 확인할 수 있습니다. 개인 저장소는 GitHub 로그인으로, 조직 저장소는 위의 저장소 확인 파일로 직접 확인해 주세요.
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Private, read-only bank access for Claude and Codex, powered by Enable Banking. It reads balances and transaction history from accounts you approve. It has no bank-side write capability: it cannot create, change, or delete bank data, move money, or initiate payments. It writes only to its own private cache.
Designed for EU/EEA banking through Enable Banking. Availability depends on the country, bank, account type, and application access; support for every bank is not guaranteed.README 전문 보기 ↗Private, read-only bank access for Claude and Codex, powered by Enable Banking. It reads balances and transaction history from accounts you approve. It has no bank-side write capability: it cannot create, change, or delete bank data, move money, or initiate payments. It writes only to its own private cache.
Navigation: Installation · Connect a client · Tools · Configuration · Security
Designed for EU/EEA banking through Enable Banking. Availability depends on the country, bank, account type, and application access; support for every bank is not guaranteed.
node:sqlite, --experimental-strip-types, and node:module hooks) and npm.workers.dev subdomain. New accounts do not have one; set it under Workers & Pages in the Cloudflare dashboard first, otherwise the deploy succeeds but no public URL exists. Not needed for Local mode.openssl, only if your Enable Banking key is PKCS#1 (starts with -----BEGIN RSA PRIVATE KEY-----) and needs converting.A GitHub account is optional. It is needed for the deploy-button workflow below. You can clone this public repository without an account, or download its source archive, and deploy from your terminal with a Cloudflare account.
There are three ways in. Each one stops at the same place: you register the Enable Banking application yourself, then connect a bank with auth:link.
| Path | What you need | What it does | What it does not do |
|---|---|---|---|
| Deploy button | GitHub and Cloudflare accounts | Clones the repository into your GitHub account, provisions D1, KV, and the Durable Object, deploys an unconfigured Worker | Does not register Enable Banking, set secrets, or connect a bank. You still clone locally and run npm run install:mcp -- --cloud |
| AI agent | Codex, Claude Code, or another coding agent with a terminal | Runs the installer for you, following AGENTS.md, and pauses when it needs your Application ID and key file | Cannot open the Enable Banking Control Panel or approve at your bank for you. Never give it bank credentials |
| Manual | A terminal | You run npm ci and npm run install:mcp yourself and follow the printed steps | Nothing is done for you, which is also the easiest path to debug |
The button starts a guided deployment into your GitHub and Cloudflare accounts. Bank data is handled by your bank, Enable Banking, your deployment, and the MCP clients you authorize. The repository creator does not receive your bank data through this server.
It does not finish configuration. The Worker needs its deployed URL before you can register the Enable Banking application, so secrets are set afterwards, not on the deploy page. Until they are, /mcp returns 503 Not configured and the bank endpoints stay closed. After the deploy finishes, clone your new repository locally and run npm run install:mcp -- --cloud to register the application and set the secrets. Then follow Set up Enable Banking and Connect a bank.
Prefer to provision everything from the command line instead? Skip the button and follow Install manually.
Give this repository URL to Codex, Claude Code, or another coding agent and say:
Install this repository as an MCP server. Follow AGENTS.md. Ask me whether I
want Cloud, Local, or Both before making changes. Never ask for bank login
credentials, and stop when you need my Enable Banking Application ID and the
local path to its downloaded .pem private-key file.
The agent procedure is in AGENTS.md. There is deliberately no browser setup page. Installation, secrets, deployment, and verification are handled from the cloned repository.
See Requirements first.
git clone https://github.com/matcha-ai-test/banking-mcp.git
cd banking-mcp
npm ci
npm run install:mcp
The installer asks first:
| Mode | Use it for |
|---|---|
| Cloud | Claude.ai and other remote MCP clients, phone, or several devices. Recommended. |
| Local | Claude Code or Codex on one computer. |
| Both | Local and cloud clients sharing the cloud database. |
Explicit commands are also available:
npm run install:mcp -- --cloud
npm run install:mcp -- --local
npm run install:mcp -- --both
Without a flag the installer asks in the terminal, but when there is no TTY (an agent, a CI job, a piped shell) it silently chooses Local. Agents should always pass a flag.
The installer stores the MCP URL and connection password (MCP_SECRET) in Git-ignored .mcp-credentials with mode 0600; for these client values the terminal prints only the file path. The operator token (START_TOKEN) is not written there; it stays in .dev.vars, and npm run auth:link reads it to print a bank link on demand. See Connect a bank.
Cloud setup keeps the tracked wrangler.jsonc template unchanged by writing discovered deployment values to Git-ignored wrangler.local.jsonc; npm run dev and npm run deploy select that local config automatically when it exists.
The tracked template names the Worker banking-mcp and the D1 database banking-mcp-db, so a second install in the same Cloudflare account would overwrite the first. Pass --worker-name on the first cloud install to choose different names; the database name is derived as <name>-db, and both are printed before anything is deployed.
npm run install:mcp -- --cloud --worker-name=banking-mcp-personal
The names are fixed when wrangler.local.jsonc is created. To change them later, delete that file and install again. Worker names are lowercase letters, digits and hyphens.
Before any secret is written, the installer signs the same RS256 token the Worker uses and calls Enable Banking's /application endpoint. A mismatched Application ID and private key is Enable Banking's 401 "Wrong signature", and catching it here avoids a working-looking install that fails on the first bank call. On success it prints the application name and environment, and warns when the application is not active. Skip the check with --skip-preflight.
For a non-interactive agent resuming after Enable Banking registration:
npm run install:mcp -- --cloud --yes --app-id=<id> --key-file="/path/to/key.pem"
This is the Cloud and Both flow. Each step names who does it.
Installer: opens Cloudflare sign-in if needed and deploys an unconfigured Worker to obtain its final URL.
Installer: prints the exact Redirect, Privacy, and Terms URLs required by Enable Banking, then stops and asks for the Application ID and key file. In a non-interactive terminal this first run exits with code 1 and "Missing Application ID" after printing the URLs. That is the intended pause, not a failure.
You, in the Control Panel: create the application at Enable Banking API applications:
Linking accounts in Enable Banking only restricts which accounts the application may access. The later bank authorization in Connect a bank is a separate step that creates an active API session.
You, in the Control Panel: activate the application. A new Production application starts inactive and every API call fails with "Internal error" until it is activated. In the Control Panel open the application, choose Activate by linking accounts, and link the bank accounts banking-mcp may read. This is what makes it Restricted: it can only see the accounts you link here.
You, in the terminal: return with only the Application ID and the local path to the downloaded .pem file, and run the installer again (see the non-interactive command above, or rerun npm run install:mcp -- --cloud and answer the prompts). The installer stores them as encrypted Cloudflare Worker secrets and deploys the configured server.
The Application ID is the UUID on the application's page in the Control Panel; the downloaded key file is usually named after it, but copy the ID from the page, not the file name.
You, in the terminal: connect a bank. See Connect a bank.
You, in your MCP client: connect using MCP_URL and CONNECTION_PASSWORD from . See .
The .pem file is the Enable Banking application's private RSA key, used to sign API calls. It is not a bank login credential. Keep it secret and never commit it. It normally begins with -----BEGIN PRIVATE KEY-----. If it begins with -----BEGIN RSA PRIVATE KEY-----, convert a copy to PKCS#8:
openssl pkcs8 -topk8 -nocrypt -in your-key.pem -out key-pkcs8.pem
On the operator machine (the one holding .dev.vars), run:
npm run auth:link -- --bank=<ASPSP name> --country=<ISO code>
Add --psu=business for company accounts. The bank name must match Enable Banking's ASPSP name exactly, as shown in the Control Panel or on the markets page. Always pass --country: some providers are listed in several countries. The server rejects an ambiguous name when the country is omitted; selecting the wrong country can leave a session with zero accounts.
The command prints a link that carries the operator token in its URL fragment. Open it in a browser on any device, approve at your bank, and wait for the result page:
auth:link with the right --country.The bank must already be linked to the application under Restricted access before you connect it here; linking is the step that permits access, this step creates the session.
You can connect several banks by running auth:link once per bank. Connecting the same bank again replaces its existing session, which is also how you renew an expiring consent.
After installation, keep the server running:
npm start
The MCP endpoint is http://127.0.0.1:8787/mcp. Cloud clients cannot reach a local address. The bank-link cookie is set with Secure only over https, so the auth:link flow works over http://127.0.0.1 as well.
For Local-only installs, register the Enable Banking application with these URLs:
http://127.0.0.1:8787/auth/callbackhttp://127.0.0.1:8787/privacyhttp://127.0.0.1:8787/termsIt is not verified that Enable Banking accepts an http loopback address on a Production application. If the Control Panel rejects these URLs, register the application through Cloud mode or --both instead, which gives it a public https URL.
Take MCP_URL and CONNECTION_PASSWORD from .mcp-credentials. Never commit either value or paste it into a shared file.
There are two ways to authenticate the connector. Both send the same CONNECTION_PASSWORD.
Option A: Sign in (OAuth, recommended)
/mcp URL as a custom connector and set Authentication to Sign in now.client_id) as well as dynamic registration. If the published identity fails with "Invalid OAuth request", switch to Register automatically.CONNECTION_PASSWORD there and press Approve. Claude then exchanges the code and attaches the connector.Deployments made before 2026-09-16 blocked this flow in Chrome: the approval page sent a Content-Security-Policy with form-action 'self', which Chrome also applies to the redirect that follows the form POST, so the browser dropped the 302 back to claude.ai and Claude restarted the flow indefinitely (issue #14). The page now allows the client's redirect origin. If you deployed earlier and see the approval page reappear after Approve, pull and redeploy, or use option B.
Option B: No sign-in with a request header
/mcp URL as a custom connector and set Authentication to No sign-in.x-api-key with the value of CONNECTION_PASSWORD from .mcp-credentials.list_accounts.No browser redirect is involved, so none can fail.
Approving the connector only proves the connection password. Ask Claude to call list_accounts: the connection is complete when it returns at least one account. An empty list means no bank session exists yet or the session returned no accounts.
Keep the password in an environment variable rather than in ~/.codex/config.toml. Reading it interactively avoids leaving it in your shell history:
read -s BANKING_MCP_TOKEN
export BANKING_MCP_TOKEN
Then register the server:
codex mcp add banking --url "<MCP_URL>" --bearer-token-env-var BANKING_MCP_TOKEN
The variable must be set whenever Codex starts. Verify with codex mcp get banking --json, then ask Codex to call list_accounts: a successful tool call is the real proof, the config check only shows what was saved.
Not documented yet. The CLI configuration above does not by itself configure a cloud task, so do not copy it into a cloud environment before checking the current official Codex documentation for MCP and secret handling there.
| Tool | Purpose |
|---|---|
list_accounts | Cached accounts, masked IBANs, latest cached balances, and last sync time |
set_account_label | Attach or clear a personal label on one linked account, keyed on its stable account_ref. Stored locally, survives re-authorization, never writes to the bank |
get_balances | Cached balances, optionally filtered by account name, last four IBAN digits, or bank |
get_transactions | Up to 500 cached transactions, pending included by default, with account, date, and text filters. Each row carries its local category and the account_ref / transaction_key selectors |
get_transaction_details | Details for a matching transaction. Uses cached details when available; otherwise makes a budgeted bank request and caches the result |
refresh_now | Live refresh from the bank, limited to 2 per bank session per UTC day and shared across all connected clients. Adds a hint field when a session syncs zero accounts. Enrichment during the refresh is configurable (see below) and can be previewed with enrichment_dry_run at zero cost |
get_auth_status | Cached session metadata by default. Optional verify: true checks sessions live, with a verification cooldown. Returns no token or secret link |
export_statements | Bulk JSON export of cached booked transactions since a date, default 2025-01-01, with a running balance and the local category per row. Can be filtered by bank and by account |
spending_summary | Server-side totals of cached booked transactions per currency, grouped by month, counterparty, account, or local category (category covers at most 5,000 rows per call). Use it for sums and breakdowns so the model never adds up rows itself |
amount_as_work_time | Expresses an amount as hours of work from an explicit monthly net income or an estimate from cached inflows. A reflection aid, not advice |
list_banks | Banks Enable Banking supports, with country and personal/business support, from a local cache refreshed weekly by the nightly sync and on every auth:link. Finds the exact ASPSP name for auth:link |
list_accounts also shows account metadata the bank supplied at link time (account type, usage, BIC, credit limit, and the last four digits of a card number); fields a bank did not supply are omitted. get_transactions accepts compact: true to drop null and empty fields from each row.
list_accounts returns a stable account_ref (survives re-authorization; absent when the bank supplied neither an IBAN nor an identification hash) and an optional label, and every tool's account filter also accepts a label or an account_ref. An exact label or account_ref selects only that account. Labels are 3 to 60 characters and are rejected when they look like an account number or would collide with another account's name, bank, label, identifier, or last four digits. History is folded across re-authorizations only for rows with the same IBAN (or, for IBAN-less accounts such as cards, the same identification hash); anything ambiguous is left unfolded rather than merged. An account whose identity is ambiguous (typically a card first seen without an IBAN and later with one) gets no account_ref and is not retried until the bank reports different details. The operator resolves it from the repository: npm run identity:resolve -- --remote list shows parked accounts, then either attach --account-uid <uid> --identity <account_ref> (same account: it takes over that identity and its label) or new --account-uid <uid> (different account: fresh identity, no label). Add --dry-run to print the SQL first. No MCP tool can change identities.
Categories are local annotations stored in the Worker's own D1 database; nothing is written to the bank and no tool here makes a bank call or spends refresh budget. They are decided when you read, so a new or edited rule applies to history and future transactions alike, and no transaction row is ever rewritten.
categorize_transaction) wins; otherwise the highest-priority enabled matching rule (ties go to the older rule); otherwise uncategorized. category_source says which (manual, rule, uncategorized) and category_rule_id names the winning rule. A manual categorization with category_id: null keeps a transaction uncategorized even when rules match; clear_transaction_category hands it back to the rules.scope), direction (in, out, or any, required and never guessed from the amount sign), counterparty and description text (exact or contains, compared after Unicode NFKC normalization, case-insensitively by simple lowercasing rather than full case folding, so ß does not match SS), an amount range in cents on the absolute amount (requires currency), and a day-of-month range. There is no regex. A rule for all accounts needs a text or amount predicate. Example: {"category_id": "…", "scope": {"type": "all_accounts"}, "direction": "out", "counterparty": {"mode": "contains", "value": "grocery"}}.account_ref plus a transaction_key derived from the cached transaction, so they survive re-authorization and history folding. Pending rows show provisional rule categories (category_provisional: true) and cannot be categorized manually. Accounts without an account_ref get rule categories but no transaction_key.unchanged: true even with a stale expected_revision, so retrying a landed change is safe. A deleted rule's rule_id is not reserved: retrying add_rule after delete_rule creates the rule again.get_transactions rows carry amount_cents (signed integer cents) to pass straight to categorize_transaction. With compact: true rows keep only category and category_source; the write selectors (account_ref, transaction_key, category and rule ids, override revision) are in the full shape only. pages with an opaque ().Bank availability comes from Enable Banking: live during auth:link, and from the local list_banks cache in clients. Its documentation covers country-specific Open Banking support across EU/EEA markets; available countries, banks, and Personal/Business support can change.
The Worker caches accounts, balances, and transactions in its D1 database. Sessions are managed per bank and personal/business account type. Most tools read the cache; refresh_now, an uncached get_transaction_details, and get_auth_status with verify: true can contact Enable Banking.
auth:link for that bank again.Rows whose bank text is only the account holder's own name are enriched from the bank's detail record during sync, within a backfill window and per-account/per-session caps. These are configurable settings with recommended starting values, not documented bank limits. Optional tool inputs have no schema defaults, but the runtime uses the values below when overrides are omitted:
0 disables the relevant behavior (backfill, or enrichment detail calls entirely).The window can be overridden with enrichment_backfill_days. enrichment_max sets both the per-account and per-session cap to the same value for that refresh_now call. Omit these inputs to use the built-in fallback values. enrichment_dry_run: true previews the
candidate count for the requested scope from the local cache only: no bank call, no refresh/detail budget
spent, no write. This previews existing cached candidates; a live refresh may discover additional transactions.
The nightly cron reads three optional, non-secret Worker variables. Set them in the vars section of your deployment configuration (the installer's wrangler.local.jsonc, if present), then deploy with npm run deploy. These variables configure scheduled sync, not omitted inputs on a manual refresh_now call:
| Var | Meaning | Falls back to |
|---|---|---|
ENRICH_BACKFILL_DAYS | Backfill window in days | 45 |
ENRICH_MAX_PER_ACCOUNT | Detail-call cap per account per run | 3 |
ENRICH_MAX_PER_SESSION | Detail-call cap per bank session per run | 6 |
Each must be a non-negative integer; a missing or invalid value falls back to the recommendation above, so an unconfigured deployment behaves exactly as before.
| Symptom | Cause | Fix |
|---|---|---|
| "Internal error" when opening the bank link | The Enable Banking application is not activated, or the Application ID or key is wrong | Activate the application in the Control Panel (Activate by linking accounts), then check the Application ID and .pem and rerun install:mcp |
| "Connected, but no accounts" | The account is not linked to the application under Restricted access, or the bank was chosen for the wrong country | Link the account in the Control Panel, then rerun auth:link with --country |
503 Not configured on /mcp | Secrets are missing on the Worker | Run npm run install:mcp -- --cloud from the cloned repository |
| "Deploy succeeded but no workers.dev URL was printed" | New Cloudflare account without a workers.dev subdomain | Register the subdomain under Workers & Pages in the dashboard and rerun |
| "Unknown bank" page | The name does not match Enable Banking's ASPSP name, or --country is missing | Ask the client to call list_banks, or copy the exact ASPSP name from the Control Panel, and pass --country |
| "Bank exists in several countries" page | The name matches in more than one country | Rerun auth:link with --country=<ISO code> |
Connector approved but list_accounts is empty | No bank session exists yet | Run auth:link and complete the bank approval |
| "session expires in N days" in tool responses | The bank consent is about to expire | Run auth:link for that bank again; the new session replaces the old one |
/mcp requires the generated connection password (MCP_SECRET), accepted either as Authorization: Bearer <password> or in one of the request headers claude.ai allows: x-api-key, api-key, apikey, x-apikey, x-api-token, api-token, or x-auth-token./auth/start requires the operator token (START_TOKEN), carried in the link fragment and exchanged for a short-lived cookie./mcp answers 429 after 20 wrong connection passwords from one client within 10 minutes. A bearer token the OAuth provider rejects counts as a wrong password in the same bucket. /authorize allows 10 attempts per hour and /register 10 client registrations per hour. /authorize, /token and /register answer 503 until the deployment is configured..dev.vars; cloud secrets are uploaded as encrypted Cloudflare Worker secrets. With --both, the same secrets are also written to the local .dev.vars..dev.vars, .mcp-credentials, wrangler.local.jsonc, .pem, and .key files are excluded from Git.To rotate the connection password or operator token: remove MCP_SECRET and/or START_TOKEN from .dev.vars, run npm run install:mcp again with the same mode flag, and reconnect your clients with the new password. OAuth sign-ins approved under the old password stop working at their next request and must approve again. Sign-ins approved before this binding existed (September 2026) carry no password fingerprint, so they survive a rotation and are bound to whichever password is current at their next token refresh. To cut every OAuth client off at once, after upgrading or whenever you rotate because of a suspected leak, run npm run oauth:revoke -- --remote --yes (without --yes it only counts). It deletes OAuth grants and tokens, never client registrations. To rotate the Enable Banking key: generate a new key for the application in the Control Panel and run the installer again with the new .pem file.
npx wrangler delete from the repository..dev.vars, .mcp-credentials, and wrangler.local.jsonc locally, and the downloaded .pem file if you no longer need it.For a single operator, Cloudflare's free tier is normally enough for Workers, D1, KV, and SQLite-backed Durable Objects; check the current limits on Cloudflare's pricing pages if you connect many banks. For Enable Banking, check their current pricing. Restricted access to your own accounts has been free of charge, but verify that before you rely on it.
Bug reports, security reports, and pull requests are welcome. See CONTRIBUTING.md for the workflow and SECURITY.md for how to report a vulnerability privately.
AGPL-3.0-or-later · Copyright © 2026 Leon.
See the license text for source-distribution and remote-network interaction requirements. For an older version, consult the license included with that commit.
.mcp-credentialscreate_category |
| Create a local category, or return the existing one with the same normalized name. At most 200 |
rename_category | Rename a category by its stable id; rules and manual categorizations follow. Needs expected_revision |
list_categories | Local categories with id, name, revision, and rule count |
add_rule | Add a categorization rule. rule_id is a client-generated UUID, so a retry with identical content is a no-op. At most 500 rules |
update_rule | Replace a rule's fields at a given expected_revision; its id and creation order stay |
list_rules | Rules in evaluation order with revision and rank; filter by account, category, or enabled; paginate with cursor |
delete_rule | Delete one rule at a given revision. Manual categorizations are unaffected |
preview_rule | Cache-only impact sample for a rule: matches, rows it would win, rows kept by manual categorizations, and rules that outrank it. Writes nothing |
categorize_transaction | Manually set (or explicitly clear to uncategorized) one booked transaction's category, verified against its date, signed amount, and currency. Beats every rule. At most 10,000 |
clear_transaction_category | Remove a manual categorization so rules apply again |
list_rulescursornext_cursordry_run, edits need expected_revision (optimistic locking; a stale value returns revision_conflict), writes share a budget of 30 per minute with set_account_label, and arguments are capped at 16 KiB. A manual categorization whose stored date, amount, currency, or direction no longer matches its transaction is not applied (category_warning: "override_identity_conflict") and does not fall through to a rule. Category names and rule text that look like an account number are refused.