An MCP server for Actual Budget — with a per-tool safety layer.
Actual has no HTTP or MCP surface of its own; it ships a Node SDK. This wraps that SDK as an MCP server so any agent — Claude Desktop, Cursor, your own — can read and edit your budget in natural language.
The difference from a plain wrapper: every tool carries its own risk. A read is a read. A mutation says so. And a delete can never fire on the agent's say-so — it returns a confirmation token and refuses until you call it again with confirm: true. The model can plan freely; it cannot quietly wipe a category because a prompt drifted.
Giving an agent write access to your finances is a liability unless the tools themselves carry the seatbelt. The safest place to enforce a rule is the tool, not the prompt.
- Node.js 20+
- A running, self-hosted Actual sync server (the
actual-serveryou sync your budget to — not the desktop app). You need its URL, its password (if set), and the Sync ID of the budget you want to expose.
In Actual: Settings → Show advanced settings → Sync ID. It's a UUID like a1b2c3d4-....
Claude Desktop
Edit claude_desktop_config.json (Claude → Settings → Developer → Edit Config):
{
"mcpServers": {
"actual": {
"command": "npx",
"args": ["-y", "actual-mcp"],
"env": {
"ACTUAL_SERVER_URL": "https://actual.example.com",
"ACTUAL_PASSWORD": "your-server-password",
"ACTUAL_SYNC_ID": "your-sync-id"
}
}
}
}Restart Claude Desktop. You'll see the tools appear under the 🔌 icon.
Cursor / other MCP clients
Any stdio MCP client works — point it at npx -y actual-mcp with the same four env vars. In Cursor: Settings → MCP → Add new and use the same command/args/env shape.
What did I spend on groceries last month?
List my accounts and their balances.
Move R$200 from Dining to Savings for July.
Add a R$54.90 transaction to Nubank, payee "Uber", category Transport, dated today.
| Env var | Required | What |
|---|---|---|
ACTUAL_SERVER_URL |
✅ | Your Actual sync server URL. |
ACTUAL_SYNC_ID |
✅ | Sync ID of the budget to open. |
ACTUAL_PASSWORD |
— | Server password. Omit if the server has none. |
ACTUAL_DATA_DIR |
— | Where the SDK caches the budget. Defaults to /tmp/actual-mcp-data. |
Amounts are integer minor units, matching the Actual SDK — R$200.00 is
20000, and outflows are negative (-5490= R$54.90 spent).
| Level | Tools | Behaviour |
|---|---|---|
| 🟢 read-only | all list_*, get_* |
Never touch data. |
| 🟡 mutating | all create_*, update_*, set_budget_* |
Change data; run immediately. |
| 🔴 guarded | all delete_* |
Change data; refuse until confirm: true. |
Every tool's level is exposed three ways so the seatbelt is visible before a call: in the tool description, in MCP readOnlyHint / destructiveHint annotations, and — for guarded tools — as a hard refusal at call time.
What a guarded call looks like. The agent tries to delete, and gets back a refusal with the exact arguments and a confirmation token:
Only an explicit re-invocation clears it:
// → delete_category { "id": "cat-abc-123", "confirm": true }
// ← { "ok": true }The human reading the transcript sees precisely what "yes" authorized.
| Tool | Level | Args |
|---|---|---|
list_category_groups |
🟢 | — |
create_category_group |
🟡 | name |
update_category_group |
🟡 | id, name |
delete_category_group |
🔴 | id, confirm |
list_categories |
🟢 | — |
create_category |
🟡 | name, group_id |
update_category |
🟡 | id, name?, group_id?, hidden? |
delete_category |
🔴 | id, confirm |
list_transactions |
🟢 | account_id, start_date?, end_date? |
create_transaction |
🟡 | account_id, date, amount, payee_name?, category?, notes? |
update_transaction |
🟡 | id, date?, amount?, payee_name?, category?, notes?, cleared? |
delete_transaction |
🔴 | id, confirm |
list_accounts |
🟢 | — |
get_account_balance |
🟢 | account_id |
create_account |
🟡 | name, offbudget? |
update_account |
🟡 | id, name?, offbudget? |
list_budget_months |
🟢 | — |
get_budget_month |
🟢 | month (YYYY-MM) |
set_budget_amount |
🟡 | month, category_id, amount |
set_budget_carryover |
🟡 | month, category_id, enabled |
list_schedules |
🟢 | — |
create_schedule |
🟡 | account_id, amount, start_date, name?, frequency?, category_id?, payee_name? |
update_schedule |
🟡 | id, + any schedule field |
delete_schedule |
🔴 | id, confirm |
list_payees |
🟢 | — |
create_payee |
🟡 | name |
update_payee |
🟡 | id, name?, category? |
delete_payee |
🔴 | id, confirm |
- "actual-mcp is not configured" —
ACTUAL_SERVER_URLorACTUAL_SYNC_IDis missing from the client'senvblock. - Can't connect / auth fails — check the URL is the sync server (not the desktop app) and the password matches. Test it by logging into that URL in a browser.
- Wrong or empty data — the Sync ID points at a different budget file. Re-copy it from Settings → Show advanced settings → Sync ID.
- Tools don't appear in Claude Desktop — fully quit and reopen; check the config file is valid JSON.
Before wiring it into an agent, confirm the server can reach your Actual instance. This prints only counts — never names or amounts:
ACTUAL_SERVER_URL=https://actual.example.com \
ACTUAL_SYNC_ID=your-sync-id \
ACTUAL_PASSWORD=your-password \
npm run smoke✓ connected to Actual
accounts: 1
categories: 63
category groups: 17
budget months: 21
✓ clean shutdown
npm install
npm run build
npm run inspect # @modelcontextprotocol/inspector against the built server
npm run smoke # connection check (counts only)MIT © Mauricio Juba · TYPE:ZERO
Not affiliated with Actual Budget. Built on the official @actual-app/api.