MCP server
Badlen ships an MCP server exposing eight tools over the /api/v1 surface. It is how a
whole statement gets into the product at once.
Your statements ──▶ Claude Code / Desktop ──▶ MCP tools ──▶ Badlen /api/v1 reads and extracts import_transactions dedup, write (your subscription)Your Claude client reads the files, on your machine, on your subscription. The server is a stdio bridge and holds no model, so Badlen never calls the Anthropic API, no model key is ever stored on the server, and nothing from a statement reaches Badlen except operations that have already been extracted.
Set it up
Section titled “Set it up”-
Have a running Badlen instance, and know the address you reach it on. An instance installed from Docker Compose answers at
http://localhost:8080: that number isWEB_PORTin your.env, and changing it there changes this address. Behind a reverse proxy, use the address the proxy serves. The examples below carry the default, which is also what the server falls back to whenBADLEN_URLis unset. -
Have Node and pnpm on the machine the client runs on. The server is a Node script your Claude client starts directly, outside Docker, so a machine that only followed Install is short of both:
node22.12 or newer andpnpm12, the versions the repository pins in its rootpackage.json. -
Run
pnpm installat the repository root, which pulls the MCP SDK. -
Issue an API key carrying exactly five permissions:
accounts:read,dashboard:read,positions:read,accounts:writeandtransactions:write. -
Register the server with your client. The path to
server.mjsmust be absolute: the client does not start it from inside the repository.Terminal window claude mcp add badlen \-e BADLEN_URL=http://localhost:8080 \-e BADLEN_API_KEY=sk_your_key \-- node /absolute/path/to/packages/mcp/src/server.mjsclaude_desktop_config.json {"mcpServers": {"badlen": {"command": "node","args": ["/absolute/path/to/packages/mcp/src/server.mjs"],"env": {"BADLEN_URL": "http://localhost:8080","BADLEN_API_KEY": "sk_your_key"}}}}
The eight tools
Section titled “The eight tools”| Tool | What it does |
|---|---|
list_accounts |
The accounts, with id, name, type and value, so Claude knows where to import |
get_net_worth |
Net worth and allocation by asset class |
get_positions |
Holdings by security, with latent and realised gains |
create_account |
Create an account to import into |
set_balance |
Set a manual account’s balance on a given day |
add_holding |
Add a fund or ETF line by ISIN, price resolved automatically |
reconcile_transactions |
Dry run: compare a batch against the account without writing |
import_transactions |
Write the batch, deduplicated |
None of them deletes anything.
Importing a statement
Section titled “Importing a statement”Point your client at the files and ask it, in plain words, to import them into Badlen. It
normalises the operations, calls reconcile_transactions to see the duplicates and raise
its questions with you, then calls import_transactions once you have answered.
Re-running the same statement is safe: duplicates are skipped. What that second call does
with the batch is on the import page.
When a call fails
Section titled “When a call fails”| What you see | What it means |
|---|---|
401 on every tool |
The key is absent, unknown, revoked or expired |
403 on one tool |
The key was issued without the permission that tool needs |
400 on an amount |
Amounts travel as decimal strings; a JSON number is refused outright |
400 on a date |
A date is ten characters, YYYY-MM-DD, not a timestamp |
Every route the tools reach is written out in the REST API reference, generated from the controller rather than typed by hand.