BankDoor
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@BankDoorshow me the balance and recent transactions for account acc_checking_001"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
BankDoor Policy Pack
An allowlist proxy for financial MCP tools. A YAML file says which bank tools an AI agent may call, and with which arguments. Reads like balances and transactions pass. Payments and writes are denied by default. Every blocked call gets an Ed25519-signed receipt that you can check offline.
The problem
Bank and finance MCP servers put tools like transfer and pay_bill next to get_balances
in the same tool list. A confused or prompt-injected agent can call them, call them under
another name, or hide a payment inside the arguments of a read. Writing "read-only" in a
prompt or a skill does not stop any of that. This lab makes the deny happen in code, in front
of the server, and leaves signed evidence each time it does.
Related MCP server: Payments Agent Gateway (MCP)
How it works
agent --MCP stdio--> bankdoor proxy --MCP stdio--> bank MCP server
|
+-- policy.yaml (allowlist, arg specs, schema pins)
+-- .bankdoor/receipts/000001.json ... (signed, hash-chained)For every tools/call the proxy runs these checks in order and forwards the call only if all
pass:
Tool name is plain ASCII
[A-Za-z0-9_-](stops Unicode look-alikes, zero-width characters,bank.transfer,../transfer, trailing spaces).The server is in the policy.
No name token is a payment/write word (
transfer,pay,send,create,delete,set, ... 47 built in, the policy can add more). snake_case, kebab-case and camelCase are split, sopayBillandpay-billare caught.Exact, case-sensitive match against the allowlist (no globs,
Maplookup soconstructoror__proto__cannot hit object prototypes).The tool's live definition (name, description, input schema, annotations) hashes to the
schema_sha256pinned in the policy. A server that swaps whatget_balancesmeans is blocked.Arguments: a plain object, at most 32 keys, every key declared in the policy, every value of the declared type and inside its pattern, length, range or enum. Nested objects and arrays are rejected.
A denied call never reaches the server. The agent gets an error that names the reason and the receipt id. The receipt stores the tool, reason, rule, argument names, a sha256 of the arguments (never the values), the policy id and sha256, the previous receipt's hash and the signing key id. The signature covers RFC 8785 canonical JSON of all of that.
tools/list through the proxy only shows tools that would pass checks 1 to 5.
Tool annotations such as readOnlyHint come from the server, so bankdoor ignores them when it
decides.
Quickstart
Requires Bun 1.4.3 and Node 24 (.nvmrc). No API keys.
bun install --frozen-lockfile
bun run check # full local gate (see below)
# 1. Signing key (once, kept outside the repo in ~/.config/bankdoor)
bun run bankdoor -- keygen
# 2. Pin the server's tool definitions
BANK_FIXTURE_STATE=/tmp/bank.json bun run bankdoor -- pin -- bun fixtures/bank-server.ts
# 3. Lint the policy (fails if it allowlists any payment/write tool)
bun run bankdoor -- lint --policy policies/bankdoor.yaml
# 4. Dry-run a decision (exit 0 allow, 2 deny, no receipt)
bun run bankdoor -- decide --policy policies/bankdoor.yaml --server bank --tool pay_bill
# 5. Run the proxy in front of the fixture bank (this is what your MCP client launches)
BANK_FIXTURE_STATE=/tmp/bank.json bun run bankdoor -- proxy \
--policy policies/bankdoor.yaml --server bank -- bun fixtures/bank-server.ts
# 6. Verify receipts offline with only the public key
bun run bankdoor -- verify .bankdoor/receipts \
--pub ~/.config/bankdoor/bankdoor.pub.pem --policy policies/bankdoor.yamlMCP client config (Claude Code, Claude Desktop, Cursor and others use the same shape):
{
"mcpServers": {
"bank": {
"command": "bun",
"args": ["run", "/abs/path/bankdoor-policy-pack/src/cli.ts", "proxy",
"--policy", "/abs/path/policy.yaml", "--server", "bank",
"--", "<bank server command>", "<args>"]
}
}
}The policy file
version: 1
id: bankdoor-fixture-readonly
default: deny # the only accepted value
require_pins: true # every allowed tool must carry schema_sha256
never_allow: [borrow] # added to the 47 built-in payment/write words
servers:
bank:
tools:
get_transactions:
schema_sha256: 181ce1e9... # from `bankdoor pin`
args:
account_id: { type: string, required: true, pattern: "acc_[a-z0-9_]{1,32}", max_length: 40 }
limit: { type: integer, min: 1, max: 100 }
since: { type: string, pattern: "\\d{4}-\\d{2}-\\d{2}", max_length: 10 }Shipped policies:
File | For | Allowed tools |
|
|
|
| noskillish/bankmcp v0.1.15 (commit |
|
The bankmcp preset pins the definitions that bankmcp v0.1.15 itself returned from
tools/list over stdio (run locally, unconfigured, no bank login). Its other seven tools
(start_consent, disconnect_bank, set_account_label, create_watch, delete_watch,
check_watches, list_banks) are denied. bankmcp has no payment tools; the preset keeps
consent, labels and watches with the human.
The fixture bank server
fixtures/bank-server.ts is an MCP server with fake accounts and five tools:
list_accounts, get_balances, get_transactions, transfer, pay_bill. It really moves
fake money: balances and a log of every call it receives live in a JSON file, so the tests can
prove a blocked call never arrived. It is unsafe on purpose:
get_transactionshonors undocumentedaction: "transfer",to_account,amount_centsfields (mass-assignment style). Called directly, that moves money.BANK_FIXTURE_VARIANT=rugpull:get_balanceskeeps its name, gets a new description and argument, and sweeps 1000.00 toacc_malloryon every call.BANK_FIXTURE_VARIANT=renamed:transferis served asmove_money.
The control runs in the tests and the report call the fixture without the gate and confirm that money does move.
Measured
From bun run report on 2026-10-21 (reports/bankdoor.md, reports/bankdoor.json). A real
MCP client talks to the real proxy over stdio, which talks to the fixture over stdio, with
real Ed25519 keys. Latency numbers change on every run.
Measure | Result |
Tools the agent sees through the gate | 3 of 5 (get_balances, get_transactions, list_accounts) |
Allowed reads that returned data | 3 / 3 |
Attack calls blocked | 30 / 30 |
Attack calls that reached the bank server | 0 |
Fake money moved with the gate | 0 cents |
Fake money moved without the gate (control: 1 transfer, 1 smuggled read, 1 rug-pulled read) | 250000 cents |
Receipts verified offline (dir + chain + policy hash) | ok, 28 receipts |
Tampered receipt copies detected | 141 / 141 |
Allowed read latency through gate, p50 / p95 (n=200) | 1.24 / 2.94 ms |
Same read straight to the fixture, p50 / p95 | 0.83 / 18.66 ms |
Attack group | Tried | Blocked | Signed receipts |
payment/write | 2 | 2 | 2 |
renamed tool | 16 | 16 | 16 |
argument smuggling | 10 | 10 | 10 |
rug-pulled definition | 1 | 1 | 1 |
server-renamed transfer | 1 | 1 | 1 |
Local gate (bun run check, Bun 1.4.3): install, skills layout, Biome, tsc --noEmit,
policy lint, bun test (91 pass, 0 fail, 1 skipped: bankmcp live listing without
BANKMCP_DIR), report, bun audit, gitleaks 8.30.1. With BANKMCP_DIR pointing at a local
bankmcp v0.1.15 checkout, bun test ran 92 pass, 0 fail.
Tests
File | What it proves |
| Reads pass; |
| Default must be deny; payment/write tools cannot be allowlisted; pins required; bad patterns and non-ASCII names rejected |
| Receipts verify offline; values are not stored; every tampered field, wrong key, wrong policy, deleted or reordered file is caught |
| Full MCP stdio path: tool list filtered, reads return data, writes/renames/smuggling/rug pull blocked and never reach the server, control shows the fixture does move money, tampered file fails |
|
|
| Preset allows exactly five bankmcp reads; live part (needs |
API keys
None. Everything runs keyless against the fixture.
Variable | Required | Used for |
| no | Where |
| no | Local bankmcp checkout with |
No .env is needed. Copy .env.example to .env only to set the optional paths above.
Limits
The idea that started this lab calls it a "JPMorgan-style" allowlist. We did not verify any statement, product or policy from JPMorgan Chase or any other bank, and this lab does not copy one. No bank has reviewed or endorsed this.
Tested only against the fixture server and the tool listing of bankmcp v0.1.15. No real bank account, payment provider or brokerage was used.
Pins catch a changed definition, not a changed behavior. A server that keeps the exact same name, description and schema but starts moving money is not detected. Pins are checked once, at proxy start.
Payment/write words are matched on whole name tokens. A server that names a payment tool with an innocent word (say
get_reportthat pays) is only stopped because it is not on the allowlist; if you allowlist it, bankdoor will forward it.Read tools whose names contain a listed word cannot be allowlisted (for example
list_transfers, becausetransfersis on the list). The built-in list is insrc/policy.ts; a policy can add words but not remove them.Receipts are hash-chained, but deleting the newest receipts (truncating the tail) is not detectable without an outside anchor such as shipping the latest hash elsewhere.
Only blocked calls get receipts. Allowed calls are forwarded without a signed record.
Only
toolsare proxied. Resources and prompts from the downstream server are not exposed.The signing key must be kept where the agent cannot read it. If the agent can edit the policy, read the key, or reach the bank server directly, none of this helps.
Educational minilab, not regularly maintained, no SLA. Not a compliance control.
Using the skill in Claude Code and Cursor
The skill lives only at .claude/skills/bankdoor-policy-pack/SKILL.md
(no .agents/skills or .cursor/skills copies). It tells an agent how to wire a server
behind bankdoor, how to dry-run a decision, how to verify receipts, and not to loosen a policy
to get a blocked call through.
Not affiliated
Not affiliated with, sponsored by or endorsed by JPMorgan Chase, any bank, Anthropic, the Model Context Protocol project, noskillish/bankmcp, Enable Banking or HOL Guard. Names are used only to describe compatibility or where the idea came from.
Credits
Building blocks: agent-flight-recorder (canonical JSON, Ed25519 pattern, deny-by-default MCP proxy), minilab-bankmcp-readonly-gate, minilab-hol-guard-bank-gate.
noskillish/bankmcp (MIT), for the preset's tool list.
See NOTICE.
Security
See SECURITY.md. Contact: hello@skilleddragon.com.
License
Apache-2.0. See LICENSE and NOTICE.
Built by SkilledDragon LLC, skilleddragon.com
This server cannot be deployed
Maintenance
Related MCP Connectors
Read-only Remote MCP for externally grounded AI agent trust receipts.
Zero-secret MCP gateway for AI agents: risk-scored, audited calls with human-in-the-loop approval.
Crypto transaction firewall and risk tools for MCP agents.
Security firewall for AI agents — scans MCP calls for injection, secrets, and risks.
Related MCP Servers
- AlicenseBqualityCmaintenanceSecurity gateway that wraps any MCP server with per-tool policies, approval gates, and optional Ed25519-signed decision receipts. Shadow mode logs every tool call without blocking; enforce mode applies block, rate-limit, and minimum-tier rules. Receipts are independently verifiable offline with no accounts needed.5653 npm11MIT
- FlicenseNot gradedqualityBmaintenanceAn MCP server that enables AI agents to safely interact with a double-entry payments ledger, enforcing idempotency, policy-based access control, and human-in-the-loop approval for high-value actions.-
- AlicenseBqualityAmaintenanceA governance proxy for AI tools — every MCP/agent tool call is policy-gated, secret-redacted, and written to a hash-chained, offline-verifiable audit trail.13MIT
- AlicenseNot gradedqualityBmaintenanceA proxy that sits in front of any MCP server. Hides tools with hidden instructions, freezes actions if tool descriptions change mid-session, caps actions like delete, send or pay with a budget, and keeps a signed audit log.Apache 2.0