actual-mcp-server 0.24.5 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,571 +1,111 @@
1
1
  <p align="center">
2
- <img src="unraid/actual-mcp-icon.png" alt="Actual MCP Server icon" width="128" height="128">
2
+ <img src="https://raw.githubusercontent.com/agigante80/actual-mcp-server/main/unraid/actual-mcp-icon.png" alt="Actual MCP Server icon" width="128" height="128">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Actual MCP Server</h1>
6
6
 
7
+ [![GitHub Actions CI](https://github.com/agigante80/actual-mcp-server/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/agigante80/actual-mcp-server/actions)
7
8
  [![npm version](https://img.shields.io/npm/v/actual-mcp-server)](https://www.npmjs.com/package/actual-mcp-server)
8
- [![npm downloads](https://img.shields.io/npm/dm/actual-mcp-server)](https://www.npmjs.com/package/actual-mcp-server)
9
+ [![Docker Pulls](https://img.shields.io/docker/pulls/agigante80/actual-mcp-server)](https://hub.docker.com/r/agigante80/actual-mcp-server)
9
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
11
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen)](https://nodejs.org/)
11
12
  [![TypeScript](https://img.shields.io/badge/TypeScript-7.0-blue)](https://www.typescriptlang.org/)
12
- [![MCP Protocol](https://img.shields.io/badge/MCP-1.32-orange)](https://modelcontextprotocol.io/)
13
- [![Docker Pulls](https://img.shields.io/docker/pulls/agigante80/actual-mcp-server)](https://hub.docker.com/r/agigante80/actual-mcp-server)
14
- [![Docker Image Size](https://img.shields.io/docker/image-size/agigante80/actual-mcp-server/latest)](https://hub.docker.com/r/agigante80/actual-mcp-server)
15
- [![Unraid Community Apps](https://img.shields.io/badge/Unraid-Community%20Apps-f15a2c?logo=unraid&logoColor=white)](https://ca.unraid.net/apps/actual-mcp-server-0bghkvs0c7c8bg)
16
- [![GitHub Actions CI](https://github.com/agigante80/actual-mcp-server/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/agigante80/actual-mcp-server/actions)
17
- [![GitHub stars](https://img.shields.io/github/stars/agigante80/actual-mcp-server?style=social)](https://github.com/agigante80/actual-mcp-server)
18
13
 
19
- **Talk to your budget. Run it anywhere. Trust it in production.**
14
+ A [Model Context Protocol](https://modelcontextprotocol.io/) server that lets an AI assistant (Claude, LibreChat, LobeChat, Cursor and others) read and manage your self-hosted [Actual Budget](https://actualbudget.org/).
20
15
 
21
- Actual MCP Server is a [Model Context Protocol](https://modelcontextprotocol.io/) server that connects any MCP-compatible AI assistant (such as [LibreChat](https://www.librechat.ai/), [LobeChat](https://lobehub.com/home), [Claude Desktop](https://claude.ai/download), and more) directly to your self-hosted [Actual Budget](https://actualbudget.org/) instance. Ask natural language questions, create transactions, analyse spending, and manage your entire budget without ever opening the Actual Budget UI.
16
+ **What you can ask it:** "How much did I spend on groceries last month, by payee?", "Add a $47.50 grocery transaction at Whole Foods to my checking account.", "Move $200 from Dining Out to Emergency Fund for this month."
22
17
 
23
- ```
24
- ┌─────────────┐ MCP/HTTP ┌──────────────────┐ Actual API ┌──────────────┐
25
- │ LibreChat │ ◄───────────► │ Actual MCP │ ◄───────────► │ Actual │
26
- │ LobeChat │ │ Server │ │ Budget │
27
- │ (remote) │ │ (84 tools) │ │ Server │
28
- └─────────────┘ └──────────────────┘ └──────────────┘
29
-
30
- ┌─────────────┐ MCP/stdio ┌──────────────────┐ Actual API ┌──────────────┐
31
- │ Claude │ ◄───────────► │ Actual MCP │ ◄───────────► │ Actual │
32
- │ Desktop │ │ Server │ │ Budget │
33
- │ (local) │ │ (84 tools) │ │ Server │
34
- └─────────────┘ └──────────────────┘ └──────────────┘
35
- ```
18
+ **84 tools** across accounts, transactions, budgets, rules, schedules and more, over stdio or HTTP, with Bearer token or OIDC auth, per-user budget access, up to 15 concurrent sessions, and automatic retry of transient Actual errors (3 attempts, exponential backoff; deletes and merges are never retried).
36
19
 
37
- ### Why this project?
20
+ ## Pick your path
38
21
 
39
- Most Actual Budget MCP implementations are simple stdio bridges designed for single-user, local use with Claude Desktop. This project goes further:
22
+ - **Claude Desktop, Claude Code, Cursor, VS Code or another local client:** run it over stdio with `npx`, no server to keep running. Follow your client's page in [docs/guides/clients/](docs/guides/clients/README.md), for example [Claude Desktop](docs/guides/clients/claude-desktop.md) or [Claude Code](docs/guides/clients/claude-code.md).
23
+ - **A chat UI such as LibreChat or LobeChat, or several users:** run it over HTTP in Docker with the quick start below, then follow [LibreChat](docs/guides/clients/librechat.md) or [LobeChat](docs/guides/clients/lobechat.md).
24
+ - **A client that installs from the official [MCP Registry](https://registry.modelcontextprotocol.io/):** it is listed as `io.github.agigante80/actual-mcp-server`, with the npm package (stdio) and the Docker image (HTTP); the client asks for the same settings as the pages above.
25
+ - **Unraid:** install it from [Community Applications](https://ca.unraid.net/apps/actual-mcp-server-0bghkvs0c7c8bg); see [Unraid](docs/guides/deployment.md#unraid-and-puidpgid-227).
40
26
 
41
- - **84 tools, the most comprehensive coverage available.** Accounts, transactions, categories, payees, tags, notes, rules, budgets, batch operations, bank sync, and more. Covers the reachable Actual Budget API with no genuine gaps.
42
- - **HTTP and stdio transport.** Runs as a real remote server for LibreChat/LobeChat (`--http`), or as a direct local process for Claude Desktop (`--stdio`). No Docker or HTTP server is needed for local use.
43
- - **6 exclusive ActualQL-powered tools.** Search and summarise transactions by month, amount, category, or payee using Actual Budget's native query engine. Aggregated results, no raw data dumped into the AI context window.
44
- - **Multi-budget switching at runtime.** Configure multiple budget files and let the AI switch between them mid-conversation with `actual_budgets_switch`. Works on both transports: HTTP keys the active budget to the MCP session, and stdio (Claude Desktop, Claude Code, Cursor) gets a synthetic per-process session so a switch is scoped to that process rather than shared globally (#348).
45
- - **Multi-user ready with OIDC.** Secure every session with JWKS-validated JWTs and per-user budget ACLs. No shared tokens required.
46
- - **Production-grade reliability on both transports.** HTTP connection pooling (up to 15 concurrent sessions), and a long-lived stdio process that logs in ONCE and reuses that connection for every tool call instead of re-authenticating per call, so Claude Desktop and Claude Code stay fast and a burst of calls no longer risks a per-call login storm against the upstream limiter. Automatic retry with exponential backoff, and a full test suite (unit + E2E + integration).
27
+ ## Quick start (Docker, HTTP)
47
28
 
48
- > **Verified working** with [LibreChat](https://www.librechat.ai/), [LobeChat](https://lobehub.com/home), and [Claude Desktop](https://claude.ai/download). All 84 tools tested end-to-end. Any MCP-compatible client should work.
29
+ You need an [Actual Budget](https://actualbudget.org/) server and Docker. **1. Create `.env` with a generated token.** In an empty directory:
49
30
 
50
- ---
51
-
52
- ## Table of Contents
53
-
54
- - [Quick Start](#quick-start)
55
- - [Upgrading](#upgrading)
56
- - [Available Tools](#available-tools)
57
- - [Configuration](#configuration)
58
- - [Multi-Budget Switching](#multi-budget-switching)
59
- - [Transport & Authentication](#transport--authentication)
60
- - [Testing](#testing)
61
- - [Documentation](#documentation)
62
- - [Contributing](#contributing)
63
- - [Architecture](#architecture)
64
- - [License](#license)
65
- - [Acknowledgments](#acknowledgments)
66
- - [Disclaimer](#disclaimer)
67
- - [Support](#support)
68
- - [Sponsor](#sponsor)
69
-
70
- ---
71
-
72
- ## Quick Start
31
+ <!-- runnable: env-token -->
32
+ ```bash
33
+ [ -f .env ] || install -m 600 /dev/null .env
34
+ [ -z "$(tail -c 1 .env)" ] || printf '\n' >> .env
35
+ for key in ACTUAL_SERVER_URL=http://YOUR-ACTUAL-HOST:5006 ACTUAL_PASSWORD= ACTUAL_BUDGET_SYNC_ID=; do
36
+ grep -q "^${key%%=*}=" .env || printf '%s\n' "$key" >> .env
37
+ done
38
+ chmod 600 .env
39
+ if [ "$(grep -c '^MCP_SSE_AUTHORIZATION=' .env)" != 1 ] || ! grep -Eq '^MCP_SSE_AUTHORIZATION=[^[:space:]]{32,}$' .env; then
40
+ ( umask 077; install -m 600 /dev/null .env.new; { grep -v '^MCP_SSE_AUTHORIZATION=' .env; printf 'MCP_SSE_AUTHORIZATION=%s\n' "$(openssl rand -hex 32)"; } > .env.new ) && mv .env.new .env
41
+ fi
42
+ grep '^MCP_SSE_AUTHORIZATION=' .env
43
+ ```
73
44
 
74
- ### Prerequisites
45
+ The last line prints the Bearer token your client sends. It never overwrites a value you set, except a token that is missing, duplicated or shorter than 32 characters, so it is safe to run again.
75
46
 
76
- - [Actual Budget](https://actualbudget.org/) server running (local or remote)
77
- - Your **Budget Sync ID**: Actual → Settings → Show Advanced Settings → Sync ID
78
- - **Node.js 22+** (npm method) or **Docker**
47
+ **2. Fill in `.env`:** `ACTUAL_PASSWORD`, `ACTUAL_BUDGET_SYNC_ID` (Actual: Settings, Show advanced settings, Sync ID) and `ACTUAL_SERVER_URL`. The URL is read inside the container, where `localhost` is the container itself: use your Actual server's LAN address or domain, or `http://host.docker.internal:5006` when Actual runs on the Docker host.
79
48
 
80
- ### Option A: Docker (recommended)
49
+ **3. Start the server** from the same directory:
81
50
 
51
+ <!-- runnable: docker-run -->
82
52
  ```bash
83
53
  docker run -d \
84
54
  --name actual-mcp-server-backend \
85
55
  -p 3600:3600 \
86
- # Use the same URL you type in your browser to open Actual Budget:
87
- # http://localhost:5006 (if Actual Budget runs on the same machine)
88
- # http://192.168.1.50:5006 (if it runs on another machine on your network)
89
- # https://actual.yourdomain.com (if you use a domain name)
90
- # http://actual:5006 (if both containers share a Docker network; use container name)
91
- -e ACTUAL_SERVER_URL=http://localhost:5006 \
92
- -e ACTUAL_PASSWORD=your_password \
93
- -e ACTUAL_BUDGET_SYNC_ID=your_sync_id \
94
- -e MCP_SSE_AUTHORIZATION=your_secret_token \
95
- -v actual-mcp-data:/app/data \ # required, see note below
56
+ --add-host=host.docker.internal:host-gateway \
57
+ --env-file .env \
58
+ -v actual-mcp-data:/app/data \
96
59
  -v actual-mcp-logs:/app/logs \
97
60
  ghcr.io/agigante80/actual-mcp-server:latest
98
61
  ```
99
62
 
100
- > **Why the `/app/data` volume is required:** Actual Budget does not expose a REST API. The official `@actual-app/api` library (used internally by this server) works by downloading a local copy of your budget data, running all queries on that local copy, then syncing changes back. The `/app/data` volume gives the container a persistent, writable place to store that local copy (it is the directory the image creates and owns as the runtime user). Without it the container has nowhere to write and will fail on startup. See the [Actual API docs](https://actualbudget.org/docs/api/) for details.
101
- >
102
- > **actual-mcp does not need to run on the same machine as Actual Budget.** You can have Actual Budget on one server and actual-mcp on another - as long as `ACTUAL_SERVER_URL` points to your Actual Budget instance, everything works.
103
-
104
- **Verify it's running:**
105
-
106
- ```bash
107
- # Quick health check
108
- curl http://localhost:3600/health
109
- # Expected: {"status":"ok","transport":"http","version":"..."}
110
-
111
- # Full MCP handshake (also verifies your token)
112
- curl -s -X POST http://localhost:3600/http \
113
- -H "Authorization: Bearer your_secret_token" \
114
- -H "Content-Type: application/json" \
115
- -H "Accept: application/json, text/event-stream" \
116
- -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"cli-test","version":"1.0"}}}' \
117
- | python3 -m json.tool
118
- # Success: JSON response with "protocolVersion" and "serverInfo"
119
- # Wrong token: {"error": "Unauthorized"}
120
- # Server not running: curl: (7) Failed to connect
121
- ```
122
-
123
- Also available on Docker Hub: `agigante80/actual-mcp-server:latest`
124
-
125
- ### Option B: Docker Compose
126
-
127
- ```bash
128
- git clone https://github.com/agigante80/actual-mcp-server.git
129
- cd actual-mcp-server
130
- cp .env.example .env # fill in ACTUAL_SERVER_URL, ACTUAL_PASSWORD, ACTUAL_BUDGET_SYNC_ID
131
-
132
- docker compose --profile production up -d # production: MCP server listens on :3600
133
- # or
134
- docker compose --profile dev up -d # dev mode with hot-reload
135
- ```
136
-
137
- > The compose file defines only the `dev` and `production` profiles. The MCP server listens on `:3600` directly: there is no bundled reverse proxy and no bundled Actual Budget server, so point `ACTUAL_SERVER_URL` at your own Actual instance. For TLS, enable native HTTPS with `MCP_ENABLE_HTTPS=true` (plus `MCP_HTTPS_CERT` and `MCP_HTTPS_KEY`), or front the server with your own reverse proxy.
138
-
139
- ### Option C: npm (HTTP server)
140
-
141
- > **Requires Node.js 22+.** npm and npx do not enforce this, so check with `node --version` first. On an older Node the server refuses to start and tells you so. Note that `npx` runs whichever `node` is first on your `PATH`, which is not always the one you installed most recently.
142
-
143
- ```bash
144
- # Quick start via npx (no clone needed):
145
- ACTUAL_SERVER_URL=http://localhost:5006 \
146
- ACTUAL_PASSWORD=your_password \
147
- ACTUAL_BUDGET_SYNC_ID=your-sync-id \
148
- MCP_SSE_AUTHORIZATION=your_token \
149
- npx actual-mcp-server --http
150
-
151
- # Or clone for development / custom config:
152
- git clone https://github.com/agigante80/actual-mcp-server.git
153
- cd actual-mcp-server
154
- npm install
155
- cp .env.example .env # fill in required values
156
- npm run build
157
- npm run dev -- --http
158
- ```
159
-
160
- Server starts at `http://localhost:3600/http` by default (the listen port is `MCP_BRIDGE_PORT`, default `3600`).
161
-
162
- ### Option D: stdio (Claude Desktop native, no Docker or HTTP server needed)
163
-
164
- The stdio transport runs the MCP server as a child process. Claude Desktop spawns it directly and communicates over stdin/stdout. No network port, no auth token, no Docker required. No cloning needed: `npx` downloads and caches the package automatically.
165
-
166
- Add to `claude_desktop_config.json` (see [docs/guides/MCP_CLIENTS_SETUP.md](docs/guides/MCP_CLIENTS_SETUP.md) for config file location and all client options):
167
-
168
- ```json
169
- {
170
- "mcpServers": {
171
- "actual-budget": {
172
- "command": "npx",
173
- "args": ["-y", "actual-mcp-server", "--stdio"],
174
- "env": {
175
- "ACTUAL_SERVER_URL": "http://localhost:5006",
176
- "ACTUAL_PASSWORD": "your_actual_password",
177
- "ACTUAL_BUDGET_SYNC_ID": "your-sync-id-here",
178
- "MCP_BRIDGE_DATA_DIR": "/absolute/path/to/data-dir"
179
- }
180
- }
181
- }
182
- }
183
- ```
184
-
185
- > **No token needed.** stdio runs as a local process owned by your user. The transport itself is the security boundary. All 84 tools are available.
186
- >
187
- > **`MCP_BRIDGE_DATA_DIR` should be an absolute path.** Without one, the data directory resolves relative to wherever the client spawns the process, which can be unpredictable. The directory is created automatically on first run.
188
-
189
- ### Option E: Unraid (Community Applications)
190
-
191
- [![Unraid Community Applications](https://img.shields.io/badge/Unraid-Install%20from%20CA-f15a2c?logo=unraid&logoColor=white)](https://ca.unraid.net/apps/actual-mcp-server-0bghkvs0c7c8bg)
192
-
193
- Actual MCP Server is published in the Unraid **Community Applications** store: **[ca.unraid.net/apps/actual-mcp-server](https://ca.unraid.net/apps/actual-mcp-server-0bghkvs0c7c8bg)**. This runs the HTTP transport, the right choice for LibreChat, LobeChat, and other remote MCP clients.
194
-
195
- Install it from the **Apps** tab (Community Applications):
196
-
197
- 1. Open the **Apps** tab and search for **`actual-mcp-server`**, then click **Install**.
198
- 2. Fill in **Actual server URL**, **Actual server password**, and **Actual server Sync ID** (the Sync ID is in Actual Budget: open the budget, **Settings, Show advanced settings, Sync ID**).
199
- 3. **Set a strong MCP auth token.** Generate one with `openssl rand -hex 32`. A blank token disables all HTTP authentication and exposes your financial data unauthenticated on the LAN, so this is required (see [Transport & Authentication](#transport--authentication)).
200
- 4. Leave **PUID=99** and **PGID=100** (`nobody:users`) so the container can write the appdata Data and Logs directories, then start it.
201
- 5. Reach the health endpoint via the container's **WebUI** link (port `3600`); point your MCP client at `http://[server-ip]:3600/http` with the Bearer token.
202
-
203
- The Unraid template lives in [`unraid/actual-mcp-server.xml`](unraid/actual-mcp-server.xml). For the publishing workflow see [docs/UNRAID_CA_PUBLISHING.md](docs/UNRAID_CA_PUBLISHING.md).
204
-
205
- ### Connect an AI client
206
-
207
- **LibreChat / LobeChat**: add to `librechat.yaml` (or LobeChat MCP plugin settings):
208
-
209
- ```yaml
210
- mcpServers:
211
- actual-mcp:
212
- type: "streamable-http"
213
- url: "http://actual-mcp-server-backend:3600/http"
214
- headers:
215
- Authorization: "Bearer YOUR_TOKEN_HERE"
216
- serverInstructions: true
217
- timeout: 600000
218
- ```
219
-
220
- See [docs/guides/AI_CLIENT_SETUP.md](docs/guides/AI_CLIENT_SETUP.md) for full LibreChat, LobeChat, network, and HTTPS/TLS proxy setup.
221
-
222
- **Claude Desktop via HTTP** (when the server is already running as a Docker container):
223
-
224
- ```json
225
- {
226
- "mcpServers": {
227
- "actual-budget": {
228
- "command": "npx",
229
- "args": [
230
- "-y",
231
- "mcp-remote",
232
- "http://localhost:3600/http",
233
- "--header",
234
- "Authorization: Bearer YOUR_TOKEN_HERE"
235
- ]
236
- }
237
- }
238
- }
239
- ```
240
-
241
- **Claude Desktop via stdio** (native, no HTTP server needed; see Option D above):
242
-
243
- ```json
244
- {
245
- "mcpServers": {
246
- "actual-budget": {
247
- "command": "node",
248
- "args": ["/absolute/path/to/actual-mcp-server/dist/src/index.js", "--stdio"],
249
- "env": {
250
- "ACTUAL_SERVER_URL": "http://localhost:5006",
251
- "ACTUAL_PASSWORD": "your_password",
252
- "ACTUAL_BUDGET_SYNC_ID": "your-sync-id",
253
- "MCP_BRIDGE_DATA_DIR": "/absolute/path/to/actual-mcp-server/actual-data"
254
- }
255
- }
256
- }
257
- }
258
- ```
259
-
260
- See [docs/guides/MCP_CLIENTS_SETUP.md](docs/guides/MCP_CLIENTS_SETUP.md) for all options (including Cursor, VS Code, Gemini CLI), Linux/NVM path fixes, and troubleshooting.
261
-
262
- ---
263
-
264
- ## Upgrading
265
-
266
- ### Docker (Option A)
267
-
268
- ```bash
269
- docker pull ghcr.io/agigante80/actual-mcp-server:latest
270
- docker stop actual-mcp-server-backend
271
- docker rm actual-mcp-server-backend
272
- # Re-run the original docker run command with the same flags and volumes
273
- ```
274
-
275
- Also available on Docker Hub: `docker pull agigante80/actual-mcp-server:latest`
63
+ Check it with `curl http://localhost:3600/health`. The image is also on Docker Hub as `agigante80/actual-mcp-server`. Production flags, TLS, reverse proxies and the reason for the `/app/data` volume are in the [deployment guide](docs/guides/deployment.md).
276
64
 
277
- ### Docker Compose (Option B)
65
+ **Docker Compose** (Compose 2.24.0 or later), from a clone of this repository:
278
66
 
67
+ <!-- runnable: compose-production -->
279
68
  ```bash
280
- docker compose pull
69
+ [ -f .env ] || install -m 600 .env.example .env
70
+ chmod 600 .env
71
+ if [ "$(grep -c '^MCP_SSE_AUTHORIZATION=' .env)" != 1 ] || ! grep -Eq '^MCP_SSE_AUTHORIZATION=[^[:space:]]{32,}$' .env; then
72
+ ( umask 077; install -m 600 /dev/null .env.new; { grep -v '^MCP_SSE_AUTHORIZATION=' .env; printf 'MCP_SSE_AUTHORIZATION=%s\n' "$(openssl rand -hex 32)"; } > .env.new ) && mv .env.new .env
73
+ fi
74
+ grep '^MCP_SSE_AUTHORIZATION=' .env
281
75
  docker compose --profile production up -d
282
76
  ```
283
77
 
284
- ### npm / cloned repo (Option C)
78
+ On a fresh `.env`, fill in the three Actual settings, then run the last line again.
285
79
 
286
- ```bash
287
- git pull
288
- npm install
289
- npm run build
290
- # Then restart the server
291
- ```
292
-
293
- ### npx / stdio (Options C & D)
294
-
295
- If you run `npx actual-mcp-server` without a globally installed version, npx fetches the latest from the registry automatically. But if you previously installed it globally (`npm install -g actual-mcp-server`), the global install takes precedence, so you must upgrade it explicitly:
80
+ **npx** (Node.js 22 or later, no container), in a directory holding a filled-in `.env` from step 1, where `http://localhost:5006` is right for Actual on the same machine:
296
81
 
82
+ <!-- runnable: npx-http -->
297
83
  ```bash
298
- # Upgrade the global install
299
- npm install -g actual-mcp-server
300
-
301
- # Or force the registry version without touching your global install
302
- npx actual-mcp-server@latest --http
84
+ npx actual-mcp-server --http
303
85
  ```
304
86
 
305
- For Claude Desktop (stdio), restart Claude after upgrading.
306
-
307
- ---
308
-
309
- ## Available Tools
310
-
311
- **84 tools** across all categories. All tools use the `actual_<category>_<action>` naming convention.
312
-
313
- ### Accounts (12)
314
-
315
- | Tool | Description |
316
- |------|-------------|
317
- | `actual_accounts_list` | List all accounts |
318
- | `actual_accounts_create` | Create new account |
319
- | `actual_accounts_update` | Update account details. An id that does not exist is refused rather than creating a partial record |
320
- | `actual_accounts_delete` | Permanently delete account |
321
- | `actual_accounts_close` | Close account. An account with NO transactions is REMOVED by Actual, not closed |
322
- | `actual_accounts_reopen` | Reopen closed account. An id that is not an account is refused rather than creating one |
323
- | `actual_accounts_get_balance` | Get account balance at a date |
324
- | `actual_account_flow_summary` | Explain the exact balance change across a set of accounts over a date range: external income, expense outflow, credits, uncategorized inflows, and transfers into, out of, or within the selection, with an exact reconciliation |
325
- | `actual_account_groups_list` | List the custom account groups used to organise accounts in the sidebar (Actual 26.9.0+) |
326
- | `actual_account_groups_create` | Create an account group. Assigning accounts to it is a separate `actual_accounts_update` call |
327
- | `actual_account_groups_update` | Rename an account group or change its sidebar position. A group that does not exist is refused, writing nothing |
328
- | `actual_account_groups_delete` | Delete an account group. Its accounts are NOT deleted, they become ungrouped |
329
-
330
- ### Transactions (17)
331
-
332
- **Standard (7)**
333
-
334
- | Tool | Description |
335
- |------|-------------|
336
- | `actual_transactions_get` | Get transactions for an account |
337
- | `actual_transactions_filter` | Filter with advanced criteria |
338
- | `actual_transactions_create` | Create new transaction(s), including splits (pass a `subtransactions` array) |
339
- | `actual_transactions_import` | Import and reconcile transactions |
340
- | `actual_transactions_update` | Update a transaction, split a plain one in place, or edit the children of an existing split via `subtransactions` |
341
- | `actual_transactions_delete` | Delete a transaction |
342
- | `actual_transactions_merge` | Merge two same-account, same-amount transactions (e.g. a bank-imported duplicate and a manual entry). Upstream keeps one (the imported one first) and DELETES the other, filling only the kept row's empty payee, category, notes and schedule and its false cleared and reconciled flags; irreversible |
343
-
344
- > **Split transactions:** pass a `subtransactions` array (each child needs an `amount` in integer cents; `category` and `notes` are optional). The child amounts must sum to the parent `amount` (the server does not enforce this, so the tool does). A split parent carries no category of its own: put categories on the children. `actual_transactions_update` with `subtransactions` edits the children of a transaction that is already a split, or splits a plain transaction IN PLACE: it keeps its id, `imported_id` and cleared state, so a bank-synced transaction can be split without breaking import matching. Its own category is cleared, and a child that names no category inherits it. A split child cannot itself be split. Note: `actual_transactions_import` forwards a `subtransactions` array to the API but does NOT apply the sum check, so use `actual_transactions_create` for guaranteed-balanced splits.
345
-
346
- **Utility (2)**
347
-
348
- | Tool | Description |
349
- |------|-------------|
350
- | `actual_transactions_uncategorized` | Summary of uncategorized transactions (totalCount, totalAmount, per-account breakdown); pass `includeTransactions:true` for paginated rows |
351
- | `actual_transactions_update_batch` | Apply many transaction updates in ONE call (`updates: [{ id, fields }]`); returns per-item success and failure counts |
352
-
353
- **Exclusive ActualQL-powered (8)**, unique to this MCP server
354
-
355
- | Tool | Description |
356
- |------|-------------|
357
- | `actual_transactions_aggregate` | Deterministic integer-cent totals grouped by month, category, category group, payee, or account; transfers excluded via `transfer_id` and split children counted once |
358
- | `actual_transactions_search_by_month` | Search by month using `$month` transform |
359
- | `actual_transactions_search_by_amount` | Find by amount range |
360
- | `actual_transactions_search_by_category` | Search by category name |
361
- | `actual_transactions_search_by_payee` | Find by payee/vendor |
362
- | `actual_transactions_summary_by_category` | Spending summary grouped by category |
363
- | `actual_transactions_summary_by_payee` | Top vendors with totals and counts |
364
- | `actual_recurring_expenses_summary` | Detect recurring charges (subscriptions, bills) from posted history: cadence (with date-drift and month-end tolerance), latest amount, price changes, occurrences, annualized cost, and active or inactive state |
365
-
366
- ### Transfers (1)
367
-
368
- | Tool | Description |
369
- |------|-------------|
370
- | `actual_transfers_create` | Create a paired transfer between two accounts (debit + credit linked by `transfer_id`, identical to UI "Make Transfer") |
371
-
372
- > **Note:** Use `actual_transfers_create` for any account-to-account movement, not `actual_transactions_create`. The dedicated tool creates both sides (debit and credit) atomically so the books stay balanced. Limitations: both accounts must exist and be open, and `from_account` must differ from `to_account`.
373
-
374
- ### Categories (4)
375
-
376
- `actual_categories_get` · `actual_categories_create` · `actual_categories_update` · `actual_categories_delete`
377
-
378
- ### Category Groups (4)
379
-
380
- `actual_category_groups_get` · `actual_category_groups_create` · `actual_category_groups_update` · `actual_category_groups_delete`
381
-
382
- ### Payees (7)
383
-
384
- `actual_payees_get` · `actual_payees_common_list` · `actual_payees_create` · `actual_payees_update` · `actual_payees_delete` · `actual_payees_merge` · `actual_payee_rules_get`
385
-
386
- ### Tags (4)
387
-
388
- `actual_tags_list` · `actual_tags_create` · `actual_tags_update` · `actual_tags_delete`
389
-
390
- | Tool | Description |
391
- |------|-------------|
392
- | `actual_tags_list` | List all tags (id, tag word, optional color and description) |
393
- | `actual_tags_create` | Create or upsert a tag by name; returns the tag UUID |
394
- | `actual_tags_update` | Update tag name, color, or description by UUID |
395
- | `actual_tags_delete` | Soft-delete a tag by UUID |
396
-
397
- ### Notes (2)
398
-
399
- `actual_notes_get` · `actual_notes_update`
400
-
401
- | Tool | Description |
402
- |------|-------------|
403
- | `actual_notes_get` | Get the note for any entity (account/category/category-group/payee UUID, or budget-YYYY-MM) |
404
- | `actual_notes_update` | Set or clear the note for any entity; validates entity exists or matches budget-YYYY-MM pattern |
405
-
406
- ### Preferences (1)
407
-
408
- | Tool | Description |
409
- |------|-------------|
410
- | `actual_preferences_get` | Read the budget's synced display preferences (number format, date format, currency, first day of week). Display only: all amounts stay integer cents |
411
-
412
- ### Budgets (12)
413
-
414
- | Tool | Description |
415
- |------|-------------|
416
- | `actual_budgets_list_available` | List all configured budget files |
417
- | `actual_budgets_switch` | Switch active budget (multi-budget) |
418
- | `actual_budgets_get_all` | List available budget files |
419
- | `actual_budgets_getMonths` | List budget months |
420
- | `actual_budgets_getMonth` | Get budget for a specific month |
421
- | `actual_budgets_setAmount` | Set category budget amount. The month must be one the budget has (see `actual_budgets_getMonths`) |
422
- | `actual_budgets_transfer` | Transfer amount between categories |
423
- | `actual_budgets_setCarryover` | Enable/disable carryover |
424
- | `actual_budgets_holdForNextMonth` | Hold funds for next month. Actual clamps the hold to what is left to budget, so the response reports the amount actually held |
425
- | `actual_budgets_resetHold` | Reset hold status |
426
- | `actual_budgets_export` | Export the active budget as a `.zip` into `ACTUAL_EXPORT_DIR`; returns path, byte size and sha256, never the file contents |
427
- | `actual_budgets_import` | Restore a budget from an Actual `.zip` or a YNAB4/YNAB5 export. **Destructive:** the budget id comes from the archive, so re-importing an export *replaces* that budget's data rather than making a copy. Also loads the imported budget, changing the session's active budget |
428
-
429
- ### Rules (6)
430
-
431
- `actual_rules_get` · `actual_rules_create` · `actual_rules_update` · `actual_rules_delete`
432
-
433
- | Tool | Description |
434
- |------|-------------|
435
- | `actual_rules_create_or_update` | Idempotent upsert: create a rule, or update the existing one that matches the same conditions, in a single call. Use this instead of get-then-create when re-running a categorisation setup, so repeated runs do not pile up duplicate rules |
436
- | `actual_rules_create_batch` | Create 1 to 50 rules in one call, each exactly the input of `actual_rules_create`. Best-effort, not atomic (see Batch Operations): a rule that fails its own checks, names a category, payee or account that does not exist, or is rejected by Actual is reported with its index while the rest continue |
437
-
438
- ### Schedules (4)
87
+ ## Privacy
439
88
 
440
- | Tool | Description |
441
- |------|-------------|
442
- | `actual_schedules_get` | List scheduled transactions, with their next occurrence date |
443
- | `actual_schedules_create` | Create a schedule (one-off or recurring); recurrence is a typed config, see `src/lib/schemas/recur.ts` |
444
- | `actual_schedules_update` | Update an existing schedule's amount, payee, account, or recurrence |
445
- | `actual_schedules_delete` | Delete a schedule |
89
+ Budget data the tools return goes to whichever model provider your AI client uses. The server itself contacts only the configured Actual server and, when OIDC auth is configured, your identity provider (discovery and JWKS). Logs stay on the host you run it on. Details: [security and privacy](docs/explanation/security-and-privacy.md).
446
90
 
447
- ### Advanced Query & Sync (2)
91
+ ## Compatibility
448
92
 
449
- | Tool | Description |
450
- |------|-------------|
451
- | `actual_query_run` | Execute custom ActualQL query |
452
- | `actual_bank_sync` | Trigger bank sync (GoCardless/SimpleFIN) |
93
+ Node.js 22 or later, Actual Budget server 25.0.0 or later, and the bundled `@actual-app/api` (its version is in `package.json` and reported by the `actual_server_info` tool). Requirements and upgrade steps: [upgrading](docs/guides/upgrading.md).
453
94
 
454
- ### Batch Operations (1)
95
+ ## Tools
455
96
 
456
- Four tools take many items in one call, for clients that allow only a few tool calls per turn. They share ONE contract: each item is applied independently and **not atomically**. There is no rollback, so items applied before a failure stay applied; a bad item is reported by its index and the rest continue; an infrastructure failure (lost connection, timeout) fails the whole call, and then you must read the data back before retrying anything. A bulk path never retries a create on its own.
457
-
458
- - `actual_transactions_update_batch` (counted under Transactions): update up to 50 transactions, `{id, fields}` each. Result: `{ succeeded: [{id}], failed: [{id, error}], total, successCount, failureCount }`. Items marked "not attempted" were never sent. Re-read with `actual_transactions_get` before retrying any item.
459
- - `actual_transactions_import` (counted under Transactions): bulk create with import semantics (deduplication and reconciliation against existing transactions, rules optionally applied). Use it to add many transactions at once.
460
- - `actual_budget_updates_batch`: set budget amounts and/or carryover flags for 1 to 100 month/category pairs, and the way to rebalance categories: read the month with `actual_budgets_getMonth`, then send one batch of absolute amounts. Each operation needs at least one of `amount` (integer cents) or `carryover`. Result: `{ succeeded: [{index, month, categoryId}], failed: [{index, month, categoryId, error}], total, successCount, failureCount }`. An unknown category, an out-of-range month or a carryover on an income category fails only that item. After a timeout, read the months back with `actual_budgets_getMonth` before retrying.
461
- - `actual_rules_create_batch` (counted under Rules): create 1 to 50 rules, each exactly the input of `actual_rules_create`. Result: `{ succeeded: [{index, id}], failed: [{index, error}], total, successCount, failureCount }`. Items marked "not attempted" were never sent. Re-read with `actual_rules_get` before retrying any item.
462
-
463
- ### Server Information & Lookup (5)
464
-
465
- | Tool | Description |
466
- |------|-------------|
467
- | `actual_server_info` | Server status, version, build info |
468
- | `actual_server_get_version` | Actual Budget server version |
469
- | `actual_get_context` | Retrieve open accounts, category groups with nested categories, and payees (name-sorted, capped by `payeeLimit`) in a single call |
470
- | `actual_get_id_by_name` | Resolve an exact name → UUID for accounts, categories, payees |
471
- | `actual_entities_search` | Find accounts/categories/payees by a name pattern (contains/startsWith/endsWith/exact/fuzzy). Fixes "payee not found" from a partial or mistyped name |
472
-
473
- ### Session Management (2)
474
-
475
- `actual_session_list` · `actual_session_close`
476
-
477
- ---
478
-
479
- ## Configuration
480
-
481
- All configuration is via environment variables. Copy `.env.example` to `.env` to get started.
482
-
483
- ### Complete Environment Variables Reference
484
-
485
- | Variable | Default | Required | Description |
486
- |----------|---------|----------|-------------|
487
- | **Actual Budget Connection** ||||
488
- | `ACTUAL_SERVER_URL` | _(none)_ | Yes | URL of your Actual Budget server. Use the same URL you type in your browser: `http://localhost:5006` (local), `http://192.168.1.x:5006` (network), `https://actual.yourdomain.com` (domain), or `http://actual:5006` (container name if on the same Docker network) |
489
- | `ACTUAL_PASSWORD` | _(none)_ | Yes | Login password for your Actual Budget **server**, the one you type on its own login screen. Not the same as `ACTUAL_BUDGET_PASSWORD` (which decrypts an E2E-encrypted budget) and unrelated to `AUTH_PROVIDER=oidc` (which controls how MCP *clients* authenticate to this server, not how this server authenticates to Actual). This server always signs in to Actual with a password, so upstream password login must stay enabled |
490
- | `ACTUAL_BUDGET_SYNC_ID` | _(none)_ | Yes | Budget Sync ID from Actual (Settings then Sync ID) |
491
- | `ACTUAL_BUDGET_PASSWORD` | _(none)_ | No | Optional encryption password for encrypted budgets |
492
- | `ALLOW_INSECURE_UPSTREAM` | `false` | No | Allow an `http://` upstream even when `ACTUAL_BUDGET_PASSWORD` is set (#161). Off by default so a plaintext upstream plus an encryption password is refused |
493
- | `ACTUAL_OP_TIMEOUT_MS` | `30000` | No | Per-operation timeout (ms) for every upstream Actual API call (init, budget download, sync, and each tool operation). A stalled call rejects after this bound so it cannot hold the global API mutex forever and hang subsequent tool calls (#270). Set to `0` to disable |
494
- | `ACTUAL_IMPORT_TIMEOUT_MS` | `600000` | No | Separate, larger timeout (ms) for a budget import, which is legitimately long rather than stalled. An import is a tracked load, so every other session waits on it; bounding it at the general operation timeout turned one tenant's large import into a process-wide stall (#407). Set to `0` to disable |
495
- | **MCP Server Settings** ||||
496
- | `MCP_BRIDGE_PORT` | `3600` | No | Port for MCP server to listen on |
497
- | `MCP_BRIDGE_BIND_HOST` | `0.0.0.0` | No | Host address to bind server to (`0.0.0.0` = all interfaces) |
498
- | `MCP_BRIDGE_DATA_DIR` | `./actual-data` | No | Directory to store Actual Budget local data (SQLite). **Required to be a persistent path.** The `@actual-app/api` library downloads a local copy of your budget here to run queries; use a volume mount in Docker to persist it across restarts |
499
- | `ACTUAL_EXPORT_DIR` | (unset) | No | Directory where `actual_budgets_export` writes budget `.zip` files. Unset means `<MCP_BRIDGE_DATA_DIR>/exports`, which is writable and persisted in the Docker image. Must be writable by the runtime user |
500
- | `MCP_BRIDGE_PUBLIC_HOST` | auto-detected | No | Public hostname/IP for server (shown in logs) |
501
- | `MCP_BRIDGE_PUBLIC_SCHEME` | auto-detected | No | Public scheme (`http` or `https`) |
502
- | `MCP_BRIDGE_USE_TLS` | `false` | No | Set to `true` to advertise `https://` in the server URL (for reverse-proxy setups where TLS is terminated upstream) |
503
- | **Transport Configuration** ||||
504
- | `MCP_TRANSPORT_MODE` | `--http` | No | Transport mode. Only `--http` is a valid value; stdio is selected via the `--stdio` CLI flag, not this var |
505
- | `MCP_HTTP_PATH` | `/http` | No | HTTP endpoint routing path |
506
- | `MCP_BRIDGE_HTTP_PATH` | same as `MCP_HTTP_PATH` | No | Advertised HTTP path shown to clients (set when a reverse proxy rewrites the path) |
507
- | `MCP_HTTP_BODY_LIMIT` | `512kb` | No | Maximum accepted JSON-RPC request body size (e.g. `512kb`, `1mb`, `1048576`). Validated at startup: an invalid, zero or overflowing size refuses to start naming the variable (#466) |
508
- | **Tool Surface** ||||
509
- | `MCP_TOOLSETS` | `all` | No | Comma-separated toolset groups (`context`, `transactions`, `analysis`, `budget`, `rules`, `schedules`, `structure`, `query`, `admin`) and presets (`chat`) to publish; `all` publishes every tool. Unknown names refuse to start. See [Reducing the tool surface](#reducing-the-tool-surface) |
510
- | `MCP_TOOLS` | _(empty)_ | No | Extra tools to add on top of `MCP_TOOLSETS`, by full registered name (e.g. `actual_accounts_list`). Unknown names refuse to start |
511
- | `MCP_READ_ONLY` | `false` | No | `true` hides and refuses the 47 write-capable tools. Only `true` or `false` (case-insensitive) are accepted; any other value refuses to start |
512
- | **Session Management** ||||
513
- | `USE_CONNECTION_POOL` | `true` | No | Enable session-based connection pooling |
514
- | `MAX_CONCURRENT_SESSIONS` | `15` | No | Maximum concurrent MCP sessions allowed |
515
- | `MAX_SESSIONS_PER_PRINCIPAL` | unset | No | Maximum concurrent sessions per OIDC subject or named `MCP_STATIC_TOKENS` entry; unset means no per-principal cap. The legacy token and unauthenticated requests are not capped |
516
- | `SESSION_IDLE_TIMEOUT_MINUTES` | `5` | No | Minutes before idle session cleanup |
517
- | **Security & Authentication** ||||
518
- | `AUTH_PROVIDER` | `none` | No | Auth mode: `none` (static Bearer) or `oidc` (JWKS-validated JWT) |
519
- | `MCP_SSE_AUTHORIZATION` | _(none)_ | No | Static Bearer token (`AUTH_PROVIDER=none`; highly recommended in production) |
520
- | `MCP_STATIC_TOKENS` | _(none)_ | No | Several static Bearer tokens, one per client, each with its own principal and budget allow-list (#572). JSON keyed by principal: `{"alice":{"tokenEnv":"MCP_TOKEN_ALICE","budgets":["Home"]}}`. Budgets are registry names. Can coexist with `MCP_SSE_AUTHORIZATION` (which stays unrestricted); refused with `AUTH_PROVIDER=oidc`. See [Security](docs/SECURITY_AND_PRIVACY.md) |
521
- | `MCP_TOKEN_*` | _(none)_ | If named | The token values `MCP_STATIC_TOKENS` points at, one variable per principal (`MCP_TOKEN_ALICE`). At least 32 characters (`openssl rand -hex 32`), unique, RFC 6750 characters only; a bad one refuses to start |
522
- | `MCP_ALLOW_UNAUTHENTICATED` | `false` | No | Opt-out for required-by-default HTTP auth (#242). On a non-loopback bind with no token and no OIDC the server refuses to start; set to `true` to run open deliberately (e.g. behind your own proxy) |
523
- | `OIDC_ISSUER` | _(none)_ | If OIDC | OIDC issuer URL (e.g., `https://sso.example.com`) |
524
- | `OIDC_ALLOW_INSECURE_ISSUER` | `false` | No | Allow a plaintext (http) OIDC issuer on a trusted network (#244). Off by default (http issuer refused at startup); set `true` only for local/LAN testing |
525
- | `OIDC_RESOURCE` | _(none)_ | If OIDC | This server's canonical URL, the RFC 9728 resource identifier and the expected `aud` (#461). Use the full public MCP endpoint URL, e.g. `https://actual-mcp.example.com/http`, no trailing slash. Must be an absolute http(s) URL (a bare client id refuses to start); put a client-id `aud` in `OIDC_ACCEPTED_AUDIENCES` instead |
526
- | `OIDC_ACCEPTED_AUDIENCES` | _(none)_ | No | Extra accepted `aud` values beyond `OIDC_RESOURCE`, comma-separated (#245). For IdPs that put the client-id in `aud` (e.g. Authentik) or the OAuth client's redirect URI (Cloudflare Access for SaaS). Strict allowlist, never a wildcard |
527
- | `OIDC_JWKS_TRUSTED_HOSTS` | _(none)_ | No | Opt-in cross-origin JWKS hosts, comma-separated `host` or `host:port` (#254). For IdPs whose `jwks_uri` lives on another host, e.g. Google needs `www.googleapis.com`. Exact match, no wildcards; empty default keeps same-origin-only |
528
- | `OIDC_JWKS_URI` | _(none)_ | No | Direct JWKS URL that bypasses OpenID discovery (#462), for issuers with no discovery document (Cloudflare Access: `https://<team>.cloudflareaccess.com/cdn-cgi/access/certs`). Same https, credentials and origin-or-trusted-host rules as a discovered `jwks_uri`, plus no query or fragment. When set, `/.well-known/oauth-authorization-server` is not served and `AUTH_BUDGET_ACL_IDENTITY_SOURCE=userinfo` is refused |
529
- | `OIDC_TOKEN_SOURCE` | `authorization` | No | Where the bearer JWT is read from (#462): `authorization` (default) or `cf-access-jwt-assertion` for an MCP server behind Cloudflare Access with Managed OAuth (the edge forwards the signed Access JWT in that header; `Authorization` is then never consulted, `OIDC_SCOPES` must be empty, `OIDC_SCOPES_SUPPORTED` is allowed, and the origin must be reachable only via Cloudflare). See the Cloudflare recipe in `docs/guides/AI_CLIENT_SETUP.md` |
530
- | `OIDC_SCOPES` | _(none)_ | No | Comma-separated required (enforced) scopes, also advertised. Leave empty when the IdP's JWTs carry no `scope` claim (Cloudflare Access for SaaS, or Casdoor when the client requests no scope) |
531
- | `OIDC_SCOPES_SUPPORTED` | _(none)_ | No | Comma-separated scopes advertised in `scopes_supported` but not enforced. Use when the IdP's JWTs carry no `scope` claim but the client needs a scope such as `offline_access` advertised (e.g. Gemini with Cloudflare Access for SaaS). Required scopes from `OIDC_SCOPES` are always advertised too. Advertising `offline_access` unlocks a refresh token this server cannot revoke: see [Limiting a leaked refresh token](docs/SECURITY_AND_PRIVACY.md#limiting-a-leaked-refresh-token-488) |
532
- | `AUTH_BUDGET_ACL` | _(none)_ | No | Per-user budget ACL; see [AI Client Setup](docs/guides/AI_CLIENT_SETUP.md#oidc-authentication-multi-user). Validated at startup: a value that is not a JSON object of string arrays refuses to start (#551). Unset or `{}` means no restrictions |
533
- | `AUTH_BUDGET_ACL_SOURCE` | `static` | No | `static` uses the `AUTH_BUDGET_ACL` map above. `actual` derives the ACL from the Actual server's own per-file access list, so revoking someone in Actual takes effect here without a config edit. Requires a multi-user (OpenID) Actual server that was password-bootstrapped first |
534
- | `AUTH_BUDGET_ACL_CLAIM` | `auto` | No | Which token claim identifies the principal when the source is `actual`. `auto` mirrors Actual's own precedence (`preferred_username`, `login`, `email`, `id`, `sub`) and matches the result against Actual's `userName`. Set a single claim name to pin one instead |
535
- | `AUTH_BUDGET_ACL_IDENTITY_MAP` | _(empty)_ | No | Explicit `<sub>=<Actual userName>` bindings, comma separated, used when the source is `actual`. Consulted before the claim precedence and authoritative: a bound `sub` whose target matches no budget file is denied rather than falling back to a claim. Use it when your access token does not carry the claim Actual stored, or after an IdP-side rename |
536
- | `AUTH_BUDGET_ACL_IDENTITY_SOURCE` | `token` | No | Where the ACL reads identity claims from when the source is `actual`. `token` uses the verified access token (no network call). `userinfo` calls the IdP's UserInfo endpoint, which is the same document Actual derives `userName` from, and fixes deployments where the access token lacks the claim Actual stored. Opt-in because it makes the IdP a hard dependency of authorization |
537
- | `AUTH_BUDGET_ACL_USERINFO_TIMEOUT_MS` | `5000` | No | Timeout for the UserInfo request, clamped to 250ms to 60s. No disable value: an unbounded call on the auth path turns a slow IdP into an outage |
538
- | `MCP_ENABLE_HTTPS` | `false` | No | Enable native TLS. Requires `MCP_HTTPS_CERT` and `MCP_HTTPS_KEY` |
539
- | `MCP_HTTPS_CERT` | _(none)_ | No | Path to PEM certificate file (required when `MCP_ENABLE_HTTPS=true`) |
540
- | `MCP_HTTPS_KEY` | _(none)_ | No | Path to PEM private key file (required when `MCP_ENABLE_HTTPS=true`) |
541
- | **Logging Configuration** ||||
542
- | `MCP_BRIDGE_STORE_LOGS` | `false` | No | Enable file logging (vs console only) |
543
- | `MCP_BRIDGE_LOG_DIR` | `app/logs` (beside the install) | No | Directory for log files (if `STORE_LOGS=true`). `.env.example` and Docker set it explicitly (e.g. `./logs`, `/app/logs`) |
544
- | `MCP_BRIDGE_LOG_LEVEL` | `debug` (dev) / `info` (prod) | No | Log level: `error`, `warn`, `info`, `debug` |
545
- | `LOG_FORMAT` | auto | No | Log output format: `json` or `pretty`. Precedence: explicit `LOG_FORMAT` wins, else `NODE_ENV=production` selects `json`, else `pretty` |
546
- | `MCP_SERVICE_NAME` | `actual-mcp-server` | No | Service name stamped on every structured (json) log record |
547
- | **Log Rotation** (when `MCP_BRIDGE_STORE_LOGS=true`) ||||
548
- | `MCP_BRIDGE_MAX_FILES` | `14d` | No | Keep rotated logs for N days (e.g., `14d`, `30d`) |
549
- | `MCP_BRIDGE_MAX_LOG_SIZE` | `20m` | No | Rotate when file reaches size (e.g., `20m`, `100m`) |
550
- | `MCP_BRIDGE_ROTATE_DATEPATTERN` | `YYYY-MM-DD` | No | Date pattern for rotated log filenames |
551
- | **Development & Debugging** ||||
552
- | `DEBUG` | _(none)_ | No | Enable debug mode (verbose logging) when set to any truthy value |
553
- | `LOG_LEVEL` | _(none)_ | No | Debug-detection toggle: set to `debug` to enable extra transport debug output. Distinct from `MCP_BRIDGE_LOG_LEVEL` (the winston level); has no default and is not itself a log level |
554
- | `MCP_BRIDGE_DEBUG_TRANSPORT` | `false` | No | Enable transport-level debug logging |
555
- | **Advanced/Internal** ||||
556
- | `ACTUAL_API_CONCURRENCY` | `5` | No | Max concurrent Actual API operations |
557
- | `NODE_ENV` | _(none)_ / `production` | No | Node environment. No app default; the Docker image sets `production`, which selects json logs and hides stack traces in error responses |
558
- | `VERSION` | auto-detected | No | Server version (auto-set by build/Docker) |
559
- | `TZ` | `UTC` | No | Timezone for timestamps (e.g., `America/New_York`) |
97
+ Every tool, with its description and caveats, is in the [tools reference](docs/reference/tools.md).
560
98
 
561
99
  ### Reducing the tool surface
562
100
 
563
- All 84 tools are published by default. Chat clients with a small tool budget (Gemini, LibreChat) do better with fewer, so the surface can be narrowed at startup. A tool that is not published is also refused if a client calls it by name, and the refusal names the setting responsible.
101
+ All 84 tools are published by default. Chat clients with a small tool budget (Gemini, LibreChat) do better with fewer. A tool that is not published is also refused if a client calls it by name.
564
102
 
103
+ <!-- BEGIN GENERATED: tools-summary -->
104
+ <!-- Generated from dist/src/lib/toolsets.js by `npm run docs:gen`. Do not edit. -->
565
105
  | Toolset | Tools | Contents |
566
106
  |---------|-------|----------|
567
107
  | `context` | 14 | Orientation, lookups and server information |
568
- | `transactions` | 13 | Transaction reads, create, update, delete, import and transfers |
108
+ | `transactions` | 14 | Transaction reads, create, update, delete, import and transfers |
569
109
  | `analysis` | 7 | Summaries, aggregates and balances |
570
110
  | `budget` | 10 | Budget months, amounts, carryover, holds, transfers, batch updates and notes |
571
111
  | `rules` | 7 | Rules and payee rules, including `actual_rules_create_batch` |
@@ -574,275 +114,59 @@ All 84 tools are published by default. Chat clients with a small tool budget (Ge
574
114
  | `query` | 1 | `actual_query_run` (SELECT only) |
575
115
  | `admin` | 6 | Budget switching, import, export, bank sync and session management |
576
116
 
577
- Each tool is in exactly one group. The `chat` preset cuts across them and publishes 12 tools: `actual_get_context`, `actual_query_run`, `actual_transactions_create`, `actual_transactions_update`, `actual_transactions_delete`, `actual_transactions_update_batch`, `actual_rules_create`, `actual_rules_create_batch`, `actual_budget_updates_batch`, `actual_budgets_transfer`, `actual_budgets_setAmount` and `actual_budgets_getMonth`. Measured on the `tools/list` payload, all 84 tools are 108,164 bytes and the `chat` preset is 25,949 bytes (about 6.5k tokens saved, a 76% cut).
117
+ The `chat` preset publishes 12 tools: `actual_get_context`, `actual_query_run`, `actual_transactions_update_batch`, `actual_budget_updates_batch`, `actual_rules_create_batch`, `actual_transactions_create`, `actual_transactions_update`, `actual_transactions_delete`, `actual_rules_create`, `actual_budgets_transfer`, `actual_budgets_setAmount` and `actual_budgets_getMonth`.
578
118
 
579
- `MCP_READ_ONLY=true` additionally hides and refuses every write-capable tool (47 of the 83, including `actual_bank_sync`, `actual_budgets_export`, `actual_budgets_switch` and `actual_session_close`), leaving 36. `actual_session_list` stays available under read-only. See [Security and Privacy](docs/SECURITY_AND_PRIVACY.md#tool-surface-mcp_toolsets-and-mcp_read_only).
119
+ `MCP_READ_ONLY=true` hides and refuses 48 write-capable tools and leaves 36 published.
120
+ <!-- END GENERATED: tools-summary -->
580
121
 
581
- Example: a chat assistant.
122
+ Example: a chat assistant with two extra tools.
582
123
 
583
124
  ```bash
584
125
  MCP_TOOLSETS=chat
585
126
  MCP_TOOLS=actual_accounts_list,actual_payees_get
586
127
  ```
587
128
 
588
- Example: a read-only household assistant.
589
-
590
- ```bash
591
- MCP_TOOLSETS=context,analysis,query
592
- MCP_READ_ONLY=true
593
- ```
594
-
595
- A configuration that resolves to zero tools refuses to start.
596
-
597
- ---
598
-
599
- ## Multi-Budget Switching
600
-
601
- Configure multiple Actual Budget files so the AI can switch between them at runtime using `actual_budgets_list_available` and `actual_budgets_switch`.
602
-
603
- `BUDGET_N_SERVER_URL` and `BUDGET_N_PASSWORD` fall back to `ACTUAL_SERVER_URL` / `ACTUAL_PASSWORD` when omitted.
604
-
605
- | Variable | Required | Fallback |
606
- |----------|----------|---------|
607
- | `BUDGET_DEFAULT_NAME` | No | `"Default"` |
608
- | `BUDGET_N_NAME` | Yes (enables group) | _(none)_ |
609
- | `BUDGET_N_SYNC_ID` | Yes | _(none)_ |
610
- | `BUDGET_N_SERVER_URL` | No | `ACTUAL_SERVER_URL` |
611
- | `BUDGET_N_PASSWORD` | No | `ACTUAL_PASSWORD` |
612
- | `BUDGET_N_ENCRYPTION_PASSWORD` | No | _(none)_ |
613
-
614
- ```bash
615
- # Default budget
616
- ACTUAL_SERVER_URL=http://actual:5006
617
- ACTUAL_PASSWORD=my-password
618
- ACTUAL_BUDGET_SYNC_ID=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
619
- BUDGET_DEFAULT_NAME=Personal
620
-
621
- # Budget 1 (same server, same password)
622
- BUDGET_1_NAME=Family
623
- BUDGET_1_SYNC_ID=bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
624
-
625
- # Budget 2 (different server)
626
- BUDGET_2_NAME=Business
627
- BUDGET_2_SERVER_URL=https://actual-office.example.com
628
- BUDGET_2_PASSWORD=office-password
629
- BUDGET_2_SYNC_ID=cccccccc-cccc-cccc-cccc-cccccccccccc
630
- ```
631
-
632
- ---
633
-
634
- ## Transport & Authentication
635
-
636
- The server supports two transport modes:
637
-
638
- | Mode | Flag | Use case | Auth |
639
- |------|------|----------|------|
640
- | HTTP | `--http` | LibreChat, LobeChat, Docker, multi-user deployments | Bearer token or OIDC |
641
- | stdio | `--stdio` | Claude Desktop, Cursor, local single-user use | None (OS process isolation) |
642
-
643
- The two modes are mutually exclusive. Pass exactly one flag when starting the server.
644
-
645
- ### stdio transport
646
-
647
- stdio is the simplest way to connect Claude Desktop directly to Actual Budget. The MCP server runs as a child process; Claude Desktop spawns it, communicates over stdin/stdout using NDJSON (the MCP wire format), and the process exits cleanly when Claude Desktop closes.
648
-
649
- **Key properties of stdio mode:**
650
-
651
- - No network port. The transport is a pipe, not a socket.
652
- - No auth token. Process ownership is the security boundary.
653
- - All logs go to stderr so they never corrupt the JSON-RPC framing on stdout
654
- - The process exits when stdin closes (Claude Desktop shutting down)
655
- - All 84 tools are available, identical to HTTP mode
656
-
657
- **Start manually to verify:**
658
-
659
- ```bash
660
- cd /path/to/actual-mcp-server
661
- ACTUAL_SERVER_URL=http://localhost:5006 \
662
- ACTUAL_PASSWORD=your_password \
663
- ACTUAL_BUDGET_SYNC_ID=your-sync-id \
664
- node dist/src/index.js --stdio
665
- ```
666
-
667
- Send a test request (keep stdin open with `sleep`):
668
-
669
- ```bash
670
- { echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'; sleep 5; } \
671
- | ACTUAL_SERVER_URL=http://localhost:5006 ACTUAL_PASSWORD=your_password ACTUAL_BUDGET_SYNC_ID=your-sync-id \
672
- node dist/src/index.js --stdio 2>/dev/null
673
- ```
674
-
675
- **Claude Desktop config** (`claude_desktop_config.json`):
676
-
677
- ```json
678
- {
679
- "mcpServers": {
680
- "actual-budget": {
681
- "command": "node",
682
- "args": ["/absolute/path/to/actual-mcp-server/dist/src/index.js", "--stdio"],
683
- "env": {
684
- "ACTUAL_SERVER_URL": "http://localhost:5006",
685
- "ACTUAL_PASSWORD": "your_actual_password",
686
- "ACTUAL_BUDGET_SYNC_ID": "your-sync-id-here",
687
- "MCP_BRIDGE_DATA_DIR": "/absolute/path/to/actual-mcp-server/actual-data"
688
- }
689
- }
690
- }
691
- }
692
- ```
693
-
694
- > **Path must be absolute.** Claude Desktop does not inherit shell `PATH`, so `node` must also be absolute if you use NVM or a non-standard install: `/home/youruser/.nvm/versions/node/v22.x.x/bin/node`.
695
-
696
- See [docs/guides/MCP_CLIENTS_SETUP.md](docs/guides/MCP_CLIENTS_SETUP.md) for all connection options (stdio native, mcp-remote via HTTP/HTTPS), other clients (Cursor, VS Code, Gemini CLI, Claude Code), Linux path fixes, and troubleshooting.
697
-
698
- ### HTTP transport
699
-
700
- **HTTP transport** uses the `/http` endpoint (StreamableHTTP) with optional Bearer token or OIDC authentication.
701
-
702
- #### Static Bearer token (single-user)
703
-
704
- ```bash
705
- # Generate a token
706
- openssl rand -hex 32
707
-
708
- # Add to .env
709
- MCP_SSE_AUTHORIZATION=your_token_here
710
- ```
711
-
712
- Clients send: `Authorization: Bearer your_token_here`
713
-
714
- #### OIDC (multi-user)
715
-
716
- ```bash
717
- AUTH_PROVIDER=oidc
718
- OIDC_ISSUER=https://sso.yourdomain.com
719
- OIDC_RESOURCE=https://actual-mcp.yourdomain.com/http # this server's public MCP URL (the expected 'aud')
720
- OIDC_ACCEPTED_AUDIENCES=your-client-id # only if your IdP puts the client id in 'aud' (#245)
721
- OIDC_SCOPES= # leave empty when the tokens carry no scope claim
722
- # OIDC_SCOPES_SUPPORTED=openid,offline_access # extra scopes advertised in discovery without enforcement (#472)
723
- ```
129
+ A configuration that resolves to zero tools refuses to start. See [Security and Privacy](docs/explanation/security-and-privacy.md#tool-surface-mcp_toolsets-and-mcp_read_only).
724
130
 
725
- `OIDC_RESOURCE` is the canonical URL of this MCP server (the MCP authorization spec's "canonical server URI" and the RFC 9728 resource identifier), not an IdP client id: the protected-resource metadata route and the expected token audience are both derived from it, and a value that is not an absolute http(s) URL refuses to start with a message naming the variable (#461). Use the full public endpoint URL (`https://host/http`, or whatever `MCP_BRIDGE_HTTP_PATH` / `MCP_HTTP_PATH` advertises) rather than the bare origin: with the endpoint form the server publishes both the path-specific document (canonical) and a root copy for clients that look only at the root (#473), and it warns at startup when the resource path and advertised path disagree.
726
-
727
- **OAuth discovery endpoints (automatic in OIDC mode).** When `AUTH_PROVIDER=oidc`, the server publishes the two metadata documents an OAuth client needs to bootstrap a login, so `mcp-remote` and Claude.ai's native connector can discover the flow without manual endpoint configuration:
728
-
729
- - `GET /.well-known/oauth-protected-resource[<path>]` (RFC 9728): identifies this server as a protected resource and points at your `OIDC_ISSUER` as the authorization server. The route follows `OIDC_RESOURCE`: with `https://host/http`, the canonical document is served at `/.well-known/oauth-protected-resource/http` (RFC 9728 section 3.1) and a root copy is also served at `/.well-known/oauth-protected-resource` for clients that skip the path-specific URL (e.g. Google Gemini, #473); the bare origin serves only the root path. The official MCP SDK tries the path-specific URL first and falls back to the root.
730
- - `GET /.well-known/oauth-authorization-server` (RFC 8414, #285): the authorization server metadata (its `authorization_endpoint` / `token_endpoint` / `registration_endpoint`), re-served from your IdP's own OpenID discovery document. This is here because several clients look for it on the resource-server origin, and some IdPs (e.g. Authentik) do not expose it where those clients look. It is fetched once at startup and served verbatim, exposes only endpoints your IdP already publishes publicly, and requires no authentication (a client reads it before it has a token). No extra configuration is needed; it is absent when `AUTH_PROVIDER` is not `oidc`.
731
-
732
- **Where the per-user budget ACL comes from.** By default the ACL is the `AUTH_BUDGET_ACL` map you maintain by hand. Since v0.10.x it can instead be derived from the Actual server's own per-file access list, so granting or revoking someone in Actual takes effect here without a config edit and a restart:
733
-
734
- ```bash
735
- AUTH_BUDGET_ACL_SOURCE=actual # default: static (the AUTH_BUDGET_ACL map)
736
- AUTH_BUDGET_ACL_CLAIM=auto # default: mirrors Actual's own claim precedence
737
- ```
738
-
739
- **How the match works.** Actual does not store your IdP's `sub`. When a user logs in, Actual resolves their identity as the first non-empty of `preferred_username`, `login`, `email`, `id`, `sub`, saves that string as the user's `userName`, and generates its own unrelated UUID as `userId`. The ACL therefore matches your token against `userName`, using that same precedence. `AUTH_BUDGET_ACL_CLAIM=auto` (the default) does exactly this; set it to a single claim name to pin one instead.
740
-
741
- Three things to know before enabling it:
742
-
743
- - **It requires a multi-user Actual server**, meaning one configured with `ACTUAL_OPENID_*`. On a password-only server no named users exist and every principal resolves to nothing.
744
- - **Your Actual server must have been bootstrapped with a PASSWORD before OpenID was added.** This is a hard prerequisite of this MCP server generally, not just of this feature: `@actual-app/api` authenticates with a password, and an Actual server set up with OpenID from scratch refuses password login, so this server cannot connect to it at all. A password cannot be added afterwards.
745
- - **The identity is only as trustworthy as the claim behind it.** OIDC guarantees that `sub` is unique and never reassigned; `preferred_username` and `email` carry no such guarantee and are often editable by the user. This exposure is inherited from Actual, which keys its own accounts the same way, not introduced here: anyone who can set their `preferred_username` to yours can impersonate you in Actual's login too. If that matters to you, pin `AUTH_BUDGET_ACL_CLAIM` to a claim your IdP does not let users edit, and make sure that is the claim your IdP uses to populate Actual's `user_name`.
746
-
747
- It is opt-in and fails closed: if the budget list cannot be read, or the principal matches no file, access is denied rather than granted.
748
-
749
- See [AI Client Setup, OIDC](docs/guides/AI_CLIENT_SETUP.md#oidc-authentication-multi-user) for `AUTH_BUDGET_ACL` format, Casdoor notes, and the Cloudflare Access for SaaS + Gemini recipe.
750
-
751
- ---
752
-
753
- ## Testing
754
-
755
- | Command | What It Tests | Requires Live Server |
756
- |---------|---------------|---------------------|
757
- | `npm run build` | TypeScript compilation | No |
758
- | `npm run test:unit-js` | 84-tool smoke, schema validation, auth ACL | No |
759
- | `npm run test:adapter` | Adapter, retry logic, concurrency | No |
760
- | `npm run test:e2e` | MCP protocol compliance (Playwright) | No |
761
- | `npm run test:e2e:docker:full` | Full stack integration | Yes (Docker) |
762
- | `npm run test:integration` | Live server sanity checks | Yes |
763
- | `npm run test:integration:full` | Full live integration suite | Yes |
764
-
765
- **Integration test levels** (`tests/manual/`): `sanity` → `smoke` → `normal` → `extended` → `full` → `cleanup`
766
-
767
- See [`tests/manual/README.md`](tests/manual/README.md) and [`tests/e2e/README.md`](tests/e2e/README.md) for details.
131
+ ## Configuration
768
132
 
769
- ---
133
+ <!-- env-summary:start -->
134
+ | Variable | Required | Description |
135
+ |----------|----------|-------------|
136
+ | `ACTUAL_SERVER_URL` | Yes | URL of your Actual Budget server, as seen from where the MCP server runs. Inside a container `localhost` is the container itself: use the host's address, the container name on a shared Docker network, or `http://host.docker.internal:5006` |
137
+ | `ACTUAL_PASSWORD` | Yes | Login password for your Actual Budget **server** (not the budget encryption password) |
138
+ | `ACTUAL_BUDGET_SYNC_ID` | Yes | Budget Sync ID from Actual (Settings then Sync ID) |
139
+ | `ACTUAL_BUDGET_PASSWORD` | No | Encryption password for an end-to-end encrypted budget |
140
+ | `MCP_BRIDGE_PORT` | No | Port the MCP server listens on |
141
+ | `MCP_SSE_AUTHORIZATION` | No | Static Bearer token clients must send (the raw token, not `Bearer ...`); highly recommended in production |
142
+ | `AUTH_PROVIDER` | No | `none` (static Bearer) or `oidc` (JWKS-validated JWT) |
143
+ | `MCP_TOOLSETS` | No | Toolset groups and presets to publish; see [Reducing the tool surface](#reducing-the-tool-surface) |
144
+ <!-- env-summary:end -->
145
+
146
+ Every variable, with its type and default: [configuration reference](docs/reference/configuration.md). Several budgets: [multiple budgets](docs/guides/multiple-budgets.md). Auth modes, per-client tokens and OIDC: [authentication](docs/guides/authentication.md).
770
147
 
771
148
  ## Documentation
772
149
 
773
- | Document | Contents |
774
- |---|---|
775
- | [docs/guides/MCP_CLIENTS_SETUP.md](docs/guides/MCP_CLIENTS_SETUP.md) | **Start here** to connect Claude Desktop, Cursor, VS Code (Copilot), Gemini CLI, or Claude Code |
776
- | [docs/guides/AI_CLIENT_SETUP.md](docs/guides/AI_CLIENT_SETUP.md) | LibreChat & LobeChat setup, Docker networking, HTTPS/TLS proxy, OIDC |
777
- | [docs/guides/DEPLOYMENT.md](docs/guides/DEPLOYMENT.md) | Docker, Docker Compose profiles, production config, Kubernetes |
778
- | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Component layers, data flow, transport protocols |
779
- | [docs/SECURITY_AND_PRIVACY.md](docs/SECURITY_AND_PRIVACY.md) | Auth models, threat model, hardening |
780
- | [docs/TESTING_AND_RELIABILITY.md](docs/TESTING_AND_RELIABILITY.md) | Test strategy, coverage, reliability patterns |
781
- | [docs/NEW_TOOL_CHECKLIST.md](docs/NEW_TOOL_CHECKLIST.md) | Step-by-step guide for adding a new MCP tool |
782
- | [CONTRIBUTING.md](.github/CONTRIBUTING.md) | Development setup, code standards, PR process |
783
- | [.env.example](.env.example) | Fully annotated environment variable reference |
784
-
785
- ---
150
+ - [Documentation index](docs/README.md): every page.
151
+ - [Connect a client](docs/guides/clients/README.md), [deployment](docs/guides/deployment.md), [authentication](docs/guides/authentication.md), [upgrading](docs/guides/upgrading.md), [troubleshooting](docs/guides/troubleshooting.md).
152
+ - [Architecture](docs/explanation/architecture.md), [testing and reliability](docs/development/testing-and-reliability.md).
786
153
 
787
154
  ## Contributing
788
155
 
789
- Contributions are welcome! See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for development setup, code standards, and the PR process.
156
+ Contributions are welcome. See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for setup, the checks every change must pass and the PR process. Questions and bug reports: [SUPPORT.md](.github/SUPPORT.md).
790
157
 
791
- Quick flow:
792
- 1. Fork → `git checkout -b feature/my-feature`
793
- 2. Make changes + add tests
794
- 3. `npm run build && npm run test:unit-js` must pass
795
- 4. Open a Pull Request
158
+ ## Security
796
159
 
797
- ---
160
+ Report a vulnerability privately as described in [SECURITY.md](.github/SECURITY.md), not in a public issue.
798
161
 
799
- ## Architecture
800
-
801
- - **Runtime**: Node.js 22 (Alpine Linux in Docker)
802
- - **Language**: TypeScript (ESM, NodeNext module resolution)
803
- - **MCP SDK**: `@modelcontextprotocol/sdk`
804
- - **Actual API**: `@actual-app/api`
805
- - **Validation**: Zod (runtime types + JSON Schema for tool inputs)
806
- - **Transports**: Express + StreamableHTTP (`--http`) · `StdioServerTransport` (`--stdio`)
807
- - **Logging**: Winston with daily rotation (all output routed to stderr in stdio mode)
162
+ ## Sponsor
808
163
 
809
- Every Actual API call goes through the `withActualApi()` wrapper in `src/lib/actual-adapter.ts`, which handles init/shutdown lifecycle, retry (3 attempts, exponential backoff), and concurrency limiting. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for full design documentation.
810
-
811
- ---
164
+ I build and maintain this in my own time. It is free, it stays free, and it gets maintained either way. If it saved you some time, you can say thanks at [github.com/sponsors/agigante80](https://github.com/sponsors/agigante80). Entirely optional.
812
165
 
813
166
  ## License
814
167
 
815
- MIT. See [LICENSE](LICENSE) for details.
816
-
817
- ---
818
-
819
- ## Acknowledgments
820
-
821
- - **[Actual Budget](https://actualbudget.org/)**: open-source budgeting software
822
- - **[Model Context Protocol](https://modelcontextprotocol.io/)**: standardised AI-app integration
823
- - **[LibreChat](https://github.com/danny-avila/LibreChat)**: open-source ChatGPT alternative
824
- - **[s-stefanov/actual-mcp](https://github.com/s-stefanov/actual-mcp)**: original adapter pattern
168
+ MIT, see [LICENSE](LICENSE). Built on [Actual Budget](https://actualbudget.org/) and the [Model Context Protocol](https://modelcontextprotocol.io/), with thanks to [s-stefanov/actual-mcp](https://github.com/s-stefanov/actual-mcp) for the original adapter pattern. An independent project, not affiliated with Actual Budget, provided as-is without warranty: if you connect it to real financial data, you do so at your own risk.
825
169
 
826
170
  ---
827
171
 
828
- ## Disclaimer
829
-
830
- This project started as a **personal learning exercise** to explore the [Model Context Protocol](https://modelcontextprotocol.io/) technology. It is an independent open-source project, not affiliated with, endorsed by, or supported by [Actual Budget](https://actualbudget.org/) or any other organisation.
831
-
832
- The software is provided **as-is**, without warranty of any kind. The author accepts no responsibility for how it is used, for any data loss, financial errors, or other consequences arising from its use. If you connect it to real financial data, you do so entirely at your own risk.
833
-
834
- ---
835
-
836
- ## Support
837
-
838
- - **[GitHub Issues](https://github.com/agigante80/actual-mcp-server/issues)**: bug reports, feature requests, questions and ideas
839
-
840
- ---
841
-
842
- **Version:** 0.24.5 | **Tool Count:** 84 (verified LibreChat-compatible)
843
-
844
- ## Sponsor
845
-
846
- I build and maintain this in my own time. It is free, it stays free, and it gets maintained either way.
847
-
848
- If it saved you some time and you feel like saying thanks, you can do that at [github.com/sponsors/agigante80](https://github.com/sponsors/agigante80). Entirely optional, and nothing about the project changes either way.
172
+ **Version:** 0.25.0 | **Tool Count:** 84 (verified LibreChat-compatible)