Skip to main content
Glama
type0labs-dev

actual-mcp

README.md
# actual-mcp

[![CI](https://github.com/Type-zero-labs/actual-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Type-zero-labs/actual-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-informational.svg)](./LICENSE)

**An [MCP](https://modelcontextprotocol.io) server for [Actual Budget](https://actualbudget.org) — 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.

---

## Requirements

- **Node.js 20+**
- A running, self-hosted **Actual sync server** (the [`actual-server`](https://actualbudget.org/docs/install/) you 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.

## Quick start

### 1. Find your Sync ID

In Actual: **Settings → Show advanced settings → Sync ID**. It's a UUID like `a1b2c3d4-....`

### 2. Add the server to your MCP client

<details open>
<summary><b>Claude Desktop</b></summary>

Edit `claude_desktop_config.json` (Claude → Settings → Developer → Edit Config):

```json
{
  "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.
</details>

<details>
<summary><b>Cursor</b> / other MCP clients</summary>

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.
</details>

### 3. Ask it things

```
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.
```

## Configuration

| 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).

## The safety model

| 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:

```jsonc
// → delete_category { "id": "cat-abc-123" }
// ← (isError)
⚠️ "delete_category" is a destructive action and needs confirmation.
It will run with:
{ "id": "cat-abc-123" }
To proceed, call "delete_category" again with "confirm": true.
Confirmation token: 75aa4f772b62
```

Only an explicit re-invocation clears it:

```jsonc
// → delete_category { "id": "cat-abc-123", "confirm": true }
// ← { "ok": true }
```

The human reading the transcript sees precisely what "yes" authorized.

## Tools (28)

| 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` |

## Troubleshooting

- **"actual-mcp is not configured"** — `ACTUAL_SERVER_URL` or `ACTUAL_SYNC_ID` is missing from the client's `env` block.
- **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.

## Verify your setup

Before wiring it into an agent, confirm the server can reach your Actual instance. This prints **only counts** — never names or amounts:

```bash
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
```

## Develop

```bash
npm install
npm run build
npm run inspect          # @modelcontextprotocol/inspector against the built server
npm run smoke            # connection check (counts only)
```

## License

MIT © [Mauricio Juba](https://mauriciojuba.com) · [TYPE:ZERO](https://typezerolabs.com)

Not affiliated with Actual Budget. Built on the official [`@actual-app/api`](https://www.npmjs.com/package/@actual-app/api).

TDQS

B3.3/5.0

Scored across 28 tools

Disambiguation5/5

Each tool targets a distinct entity and action (e.g., account, category, payee, schedule, transaction, budget month). There is no overlap or ambiguity; an agent can clearly differentiate between tools.

Naming Consistency5/5

All tool names follow a strict verb_noun pattern: create_, delete_, get_, list_, set_, update_. The naming is uniform and predictable throughout the entire set.

Tool Count4/5

With 28 tools, the surface is comprehensive for a personal finance manager. While slightly above the typical 3-15 range, the number is justified by the domain's natural complexity (accounts, categories, payees, schedules, transactions, budgets).

Completeness4/5

The tool set provides full CRUD for core entities (accounts, categories, payees, schedules, transactions) plus budget operations (set amount, carryover, get month). Minor gaps exist (e.g., no import or reporting tools), but the essential lifecycle is well-covered.

Maintenance

ActivityStale
ResponsivenessNo issues