trackmcp
Back to directory
agigante80

actual-mcp-server

View on GitHub

Talk to your self-hosted Actual Budget from any MCP client: a remote HTTP server for LibreChat/LobeChat or a local stdio process for Claude Desktop, with 71 tools for transactions, budgets, rules, and bank sync. No Docker needed for local use.

49 stars JavaScriptOthers Updated Sep 4, 2026
actual-budgetai-assistantapi-serverbudget-managementbudget-trackingconversation-aidockerfinance-automationfinancial-managementlibrechatmcpmodel-context-protocolnodejspersonal-financetypescript

Documentation

Actual MCP Server

npm version
npm downloads
License: MIT
Node.js Version
TypeScript
MCP Protocol
Docker Pulls
Docker Image Size
Unraid Community Apps
GitHub Actions CI
GitHub stars

Talk to your budget. Run it anywhere. Trust it in production.

Actual MCP Server is a Model Context Protocol server that connects any MCP-compatible AI assistant (such as LibreChat, LobeChat, Claude Desktop, and more) directly to your self-hosted Actual Budget instance. Ask natural language questions, create transactions, analyse spending, and manage your entire budget without ever opening the Actual Budget UI.

code
┌─────────────┐   MCP/HTTP    ┌──────────────────┐   Actual API   ┌──────────────┐
│  LibreChat  │ ◄───────────► │  Actual MCP      │ ◄───────────► │   Actual     │
│  LobeChat   │               │  Server          │               │   Budget     │
│  (remote)   │               │  (77 tools)      │               │   Server     │
└─────────────┘               └──────────────────┘               └──────────────┘

┌─────────────┐   MCP/stdio   ┌──────────────────┐   Actual API   ┌──────────────┐
│  Claude     │ ◄───────────► │  Actual MCP      │ ◄───────────► │   Actual     │
│  Desktop    │               │  Server          │               │   Budget     │
│  (local)    │               │  (77 tools)      │               │   Server     │
└─────────────┘               └──────────────────┘               └──────────────┘

Why this project?

Most Actual Budget MCP implementations are simple stdio bridges designed for single-user, local use with Claude Desktop. This project goes further:

  • 77 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.
  • 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.
  • 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.
  • 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).
  • Multi-user ready with OIDC. Secure every session with JWKS-validated JWTs and per-user budget ACLs. No shared tokens required.
  • 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).

> Verified working with LibreChat, LobeChat, and Claude Desktop. All 77 tools tested end-to-end. Any MCP-compatible client should work.


Table of Contents


Quick Start

Prerequisites

  • Actual Budget server running (local or remote)
  • Your Budget Sync ID: Actual → Settings → Show Advanced Settings → Sync ID
  • Node.js 22+ (npm method) or Docker
bash
docker run -d \
  --name actual-mcp-server-backend \
  -p 3600:3600 \
  # Use the same URL you type in your browser to open Actual Budget:
  #   http://localhost:5006          (if Actual Budget runs on the same machine)
  #   http://192.168.1.50:5006       (if it runs on another machine on your network)
  #   https://actual.yourdomain.com  (if you use a domain name)
  #   http://actual:5006             (if both containers share a Docker network; use container name)
  -e ACTUAL_SERVER_URL=http://localhost:5006 \
  -e ACTUAL_PASSWORD=your_password \
  -e ACTUAL_BUDGET_SYNC_ID=your_sync_id \
  -e MCP_SSE_AUTHORIZATION=your_secret_token \
  -v actual-mcp-data:/app/data \        # required, see note below
  -v actual-mcp-logs:/app/logs \
  ghcr.io/agigante80/actual-mcp-server:latest

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

>

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

Verify it's running:

bash
# Quick health check
curl http://localhost:3600/health
# Expected: {"status":"ok","transport":"http","version":"..."}

# Full MCP handshake (also verifies your token)
curl -s -X POST http://localhost:3600/http \
  -H "Authorization: Bearer your_secret_token" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"cli-test","version":"1.0"}}}' \
  | python3 -m json.tool
# Success: JSON response with "protocolVersion" and "serverInfo"
# Wrong token: {"error": "Unauthorized"}
# Server not running: curl: (7) Failed to connect

Also available on Docker Hub: `agigante80/actual-mcp-server:latest`

Option B: Docker Compose

bash
git clone https://github.com/agigante80/actual-mcp-server.git
cd actual-mcp-server
cp .env.example .env        # fill in ACTUAL_SERVER_URL, ACTUAL_PASSWORD, ACTUAL_BUDGET_SYNC_ID

docker compose --profile production up -d   # production: MCP server listens on :3600
# or
docker compose --profile dev up -d          # dev mode with hot-reload

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

Option C: npm (HTTP server)

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

bash
# Quick start via npx (no clone needed):
ACTUAL_SERVER_URL=http://localhost:5006 \
ACTUAL_PASSWORD=your_password \
ACTUAL_BUDGET_SYNC_ID=your-sync-id \
MCP_SSE_AUTHORIZATION=your_token \
npx actual-mcp-server --http

# Or clone for development / custom config:
git clone https://github.com/agigante80/actual-mcp-server.git
cd actual-mcp-server
npm install
cp .env.example .env        # fill in required values
npm run build
npm run dev -- --http

Server starts at `http://localhost:3600/http` by default (the listen port is `MCP_BRIDGE_PORT`, default `3600`).

Option D: stdio (Claude Desktop native, no Docker or HTTP server needed)

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.

Add to `claude_desktop_config.json` (see docs/guides/MCP_CLIENTS_SETUP.md for config file location and all client options):

json
{
  "mcpServers": {
    "actual-budget": {
      "command": "npx",
      "args": ["-y", "actual-mcp-server", "--stdio"],
      "env": {
        "ACTUAL_SERVER_URL": "http://localhost:5006",
        "ACTUAL_PASSWORD": "your_actual_password",
        "ACTUAL_BUDGET_SYNC_ID": "your-sync-id-here",
        "MCP_BRIDGE_DATA_DIR": "/absolute/path/to/data-dir"
      }
    }
  }
}

> No token needed. stdio runs as a local process owned by your user. The transport itself is the security boundary. All 77 tools are available.

>

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

Option E: Unraid (Community Applications)

Unraid Community Applications

Actual MCP Server is published in the Unraid Community Applications store: **ca.unraid.net/apps/actual-mcp-server**. This runs the HTTP transport, the right choice for LibreChat, LobeChat, and other remote MCP clients.

Install it from the Apps tab (Community Applications):

1. Open the Apps tab and search for `actual-mcp-server`, then click Install.

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

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

4. Leave PUID=99 and PGID=100 (`nobody:users`) so the container can write the appdata Data and Logs directories, then start it.

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.

The Unraid template lives in `unraid/actual-mcp-server.xml`. For the publishing workflow see docs/UNRAID_CA_PUBLISHING.md.

Connect an AI client

LibreChat / LobeChat: add to `librechat.yaml` (or LobeChat MCP plugin settings):

yaml
mcpServers:
  actual-mcp:
    type: "streamable-http"
    url: "http://actual-mcp-server-backend:3600/http"
    headers:
      Authorization: "Bearer YOUR_TOKEN_HERE"
    serverInstructions: true
    timeout: 600000

See docs/guides/AI_CLIENT_SETUP.md for full LibreChat, LobeChat, network, and HTTPS/TLS proxy setup.

Claude Desktop via HTTP (when the server is already running as a Docker container):

json
{
  "mcpServers": {
    "actual-budget": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "http://localhost:3600/http",
        "--header",
        "Authorization: Bearer YOUR_TOKEN_HERE"
      ]
    }
  }
}

Claude Desktop via stdio (native, no HTTP server needed; see Option D above):

json
{
  "mcpServers": {
    "actual-budget": {
      "command": "node",
      "args": ["/absolute/path/to/actual-mcp-server/dist/src/index.js", "--stdio"],
      "env": {
        "ACTUAL_SERVER_URL": "http://localhost:5006",
        "ACTUAL_PASSWORD": "your_password",
        "ACTUAL_BUDGET_SYNC_ID": "your-sync-id",
        "MCP_BRIDGE_DATA_DIR": "/absolute/path/to/actual-mcp-server/actual-data"
      }
    }
  }
}

See docs/guides/MCP_CLIENTS_SETUP.md for all options (including Cursor, VS Code, Gemini CLI), Linux/NVM path fixes, and troubleshooting.


Upgrading

Docker (Option A)

bash
docker pull ghcr.io/agigante80/actual-mcp-server:latest
docker stop actual-mcp-server-backend
docker rm actual-mcp-server-backend
# Re-run the original docker run command with the same flags and volumes

Also available on Docker Hub: `docker pull agigante80/actual-mcp-server:latest`

Docker Compose (Option B)

bash
docker compose pull
docker compose --profile production up -d

npm / cloned repo (Option C)

bash
git pull
npm install
npm run build
# Then restart the server

npx / stdio (Options C & D)

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:

bash
# Upgrade the global install
npm install -g actual-mcp-server

# Or force the registry version without touching your global install
npx actual-mcp-server@latest --http

For Claude Desktop (stdio), restart Claude after upgrading.


Available Tools

77 tools across all categories. All tools use the `actual__` naming convention.

Accounts (8)

ToolDescription
`actual_accounts_list`List all accounts
`actual_accounts_create`Create new account
`actual_accounts_update`Update account details. An id that does not exist is refused rather than creating a partial record
`actual_accounts_delete`Permanently delete account
`actual_accounts_close`Close account. An account with NO transactions is REMOVED by Actual, not closed
`actual_accounts_reopen`Reopen closed account. An id that is not an account is refused rather than creating one
`actual_accounts_get_balance`Get account balance at a date
`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

Transactions (16)

Standard (6)

ToolDescription
`actual_transactions_get`Get transactions for an account
`actual_transactions_filter`Filter with advanced criteria
`actual_transactions_create`Create new transaction(s), including splits (pass a `subtransactions` array)
`actual_transactions_import`Import and reconcile transactions
`actual_transactions_update`Update a transaction, or edit the children of an existing split via `subtransactions`
`actual_transactions_delete`Delete a transaction

> 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` can edit the children of a transaction that is ALREADY a split; converting a plain transaction into a split via update is not supported (create it as a split instead). 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.

Utility (2)

ToolDescription
`actual_transactions_uncategorized`Summary of uncategorized transactions (totalCount, totalAmount, per-account breakdown); pass `includeTransactions:true` for paginated rows
`actual_transactions_update_batch`Apply many transaction updates in ONE call (`updates: [{ id, fields }]`); returns per-item success and failure counts

Exclusive ActualQL-powered (8), unique to this MCP server

ToolDescription
`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
`actual_transactions_search_by_month`Search by month using `$month` transform
`actual_transactions_search_by_amount`Find by amount range
`actual_transactions_search_by_category`Search by category name
`actual_transactions_search_by_payee`Find by payee/vendor
`actual_transactions_summary_by_category`Spending summary grouped by category
`actual_transactions_summary_by_payee`Top vendors with totals and counts
`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

Transfers (1)

ToolDescription
`actual_transfers_create`Create a paired transfer between two accounts (debit + credit linked by `transfer_id`, identical to UI "Make Transfer")

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

Categories (4)

`actual_categories_get` · `actual_categories_create` · `actual_categories_update` · `actual_categories_delete`

Category Groups (4)

`actual_category_groups_get` · `actual_category_groups_create` · `actual_category_groups_update` · `actual_category_groups_delete`

Payees (7)

`actual_payees_get` · `actual_payees_common_list` · `actual_payees_create` · `actual_payees_update` · `actual_payees_delete` · `actual_payees_merge` · `actual_payee_rules_get`

Tags (4)

`actual_tags_list` · `actual_tags_create` · `actual_tags_update` · `actual_tags_delete`

ToolDescription
`actual_tags_list`List all tags (id, tag word, optional color and description)
`actual_tags_create`Create or upsert a tag by name; returns the tag UUID
`actual_tags_update`Update tag name, color, or description by UUID
`actual_tags_delete`Soft-delete a tag by UUID

Notes (2)

`actual_notes_get` · `actual_notes_update`

ToolDescription
`actual_notes_get`Get the note for any entity (account/category/category-group/payee UUID, or budget-YYYY-MM)
`actual_notes_update`Set or clear the note for any entity; validates entity exists or matches budget-YYYY-MM pattern

Preferences (1)

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

Budgets (12)

ToolDescription
`actual_budgets_list_available`List all configured budget files
`actual_budgets_switch`Switch active budget (multi-budget)
`actual_budgets_get_all`List available budget files
`actual_budgets_getMonths`List budget months
`actual_budgets_getMonth`Get budget for a specific month
`actual_budgets_setAmount`Set category budget amount. The month must be one the budget has (see `actual_budgets_getMonths`)
`actual_budgets_transfer`Transfer amount between categories
`actual_budgets_setCarryover`Enable/disable carryover
`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
`actual_budgets_resetHold`Reset hold status
`actual_budgets_export`Export the active budget as a `.zip` into `ACTUAL_EXPORT_DIR`; returns path, byte size and sha256, never the file contents
`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

Rules (5)

`actual_rules_get` · `actual_rules_create` · `actual_rules_update` · `actual_rules_delete`

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

Schedules (4)

ToolDescription
`actual_schedules_get`List scheduled transactions, with their next occurrence date
`actual_schedules_create`Create a schedule (one-off or recurring); recurrence is a typed config, see `src/lib/schemas/recur.ts`
`actual_schedules_update`Update an existing schedule's amount, payee, account, or recurrence
`actual_schedules_delete`Delete a schedule

Advanced Query & Sync (2)

ToolDescription
`actual_query_run`Execute custom ActualQL query
`actual_bank_sync`Trigger bank sync (GoCardless/SimpleFIN)

Batch Operations (1)

`actual_budget_updates_batch`: batch multiple budget updates in one call

Server Information & Lookup (4)

ToolDescription
`actual_server_info`Server status, version, build info
`actual_server_get_version`Actual Budget server version
`actual_get_id_by_name`Resolve an exact name → UUID for accounts, categories, payees
`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

Session Management (2)

`actual_session_list` · `actual_session_close`

Not Yet Implemented

  • Scheduled/recurring transactions (`getSchedules`, `createSchedule`, `updateSchedule`, `deleteSchedule`)

Configuration

All configuration is via environment variables. Copy `.env.example` to `.env` to get started.

Complete Environment Variables Reference

VariableDefaultRequiredDescription
Actual Budget Connection
`ACTUAL_SERVER_URL`_(none)_YesURL 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)
`ACTUAL_PASSWORD`_(none)_YesLogin 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
`ACTUAL_BUDGET_SYNC_ID`_(none)_YesBudget Sync ID from Actual (Settings then Sync ID)
`ACTUAL_BUDGET_PASSWORD`_(none)_NoOptional encryption password for encrypted budgets
`ALLOW_INSECURE_UPSTREAM``false`NoAllow an `http://` upstream even when `ACTUAL_BUDGET_PASSWORD` is set (#161). Off by default so a plaintext upstream plus an encryption password is refused
`ACTUAL_OP_TIMEOUT_MS``30000`NoPer-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
`ACTUAL_IMPORT_TIMEOUT_MS``600000`NoSeparate, 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
MCP Server Settings
`MCP_BRIDGE_PORT``3600`NoPort for MCP server to listen on
`MCP_BRIDGE_BIND_HOST``0.0.0.0`NoHost address to bind server to (`0.0.0.0` = all interfaces)
`MCP_BRIDGE_DATA_DIR``./actual-data`NoDirectory 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
`ACTUAL_EXPORT_DIR`(unset)NoDirectory where `actual_budgets_export` writes budget `.zip` files. Unset means `/exports`, which is writable and persisted in the Docker image. Must be writable by the runtime user
`MCP_BRIDGE_PUBLIC_HOST`auto-detectedNoPublic hostname/IP for server (shown in logs)
`MCP_BRIDGE_PUBLIC_SCHEME`auto-detectedNoPublic scheme (`http` or `https`)
`MCP_BRIDGE_USE_TLS``false`NoSet to `true` to advertise `https://` in the server URL (for reverse-proxy setups where TLS is terminated upstream)
Transport Configuration
`MCP_TRANSPORT_MODE``--http`NoTransport mode. Only `--http` is a valid value; stdio is selected via the `--stdio` CLI flag, not this var
`MCP_HTTP_PATH``/http`NoHTTP endpoint routing path
`MCP_BRIDGE_HTTP_PATH`same as `MCP_HTTP_PATH`NoAdvertised HTTP path shown to clients (set when a reverse proxy rewrites the path)
`MCP_HTTP_BODY_LIMIT``512kb`NoMaximum accepted JSON-RPC request body size (e.g. `512kb`, `1mb`)
Session Management
`USE_CONNECTION_POOL``true`NoEnable session-based connection pooling
`MAX_CONCURRENT_SESSIONS``15`NoMaximum concurrent MCP sessions allowed
`SESSION_IDLE_TIMEOUT_MINUTES``5`NoMinutes before idle session cleanup
Security & Authentication
`AUTH_PROVIDER``none`NoAuth mode: `none` (static Bearer) or `oidc` (JWKS-validated JWT)
`MCP_SSE_AUTHORIZATION`_(none)_NoStatic Bearer token (`AUTH_PROVIDER=none`; highly recommended in production)
`MCP_ALLOW_UNAUTHENTICATED``false`NoOpt-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)
`OIDC_ISSUER`_(none)_If OIDCOIDC issuer URL (e.g., `https://sso.example.com`)
`OIDC_ALLOW_INSECURE_ISSUER``false`NoAllow 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
`OIDC_RESOURCE`_(none)_NoExpected `aud` claim in JWT (your client ID)
`OIDC_ACCEPTED_AUDIENCES`_(none)_NoExtra accepted `aud` values beyond `OIDC_RESOURCE`, comma-separated (#245). For IdPs that put the client-id in `aud` (e.g. Authentik). Strict allowlist, never a wildcard
`OIDC_JWKS_TRUSTED_HOSTS`_(none)_NoOpt-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
`OIDC_SCOPES`_(none)_NoComma-separated required scopes; leave empty for Casdoor
`AUTH_BUDGET_ACL`_(none)_NoPer-user budget ACL; see AI Client Setup
`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
`AUTH_BUDGET_ACL_CLAIM``auto`NoWhich 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
`AUTH_BUDGET_ACL_IDENTITY_MAP`_(empty)_NoExplicit `=` 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
`AUTH_BUDGET_ACL_IDENTITY_SOURCE``token`NoWhere 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
`AUTH_BUDGET_ACL_USERINFO_TIMEOUT_MS``5000`NoTimeout 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
`MCP_ENABLE_HTTPS``false`NoEnable native TLS. Requires `MCP_HTTPS_CERT` and `MCP_HTTPS_KEY`
`MCP_HTTPS_CERT`_(none)_NoPath to PEM certificate file (required when `MCP_ENABLE_HTTPS=true`)
`MCP_HTTPS_KEY`_(none)_NoPath to PEM private key file (required when `MCP_ENABLE_HTTPS=true`)
Logging Configuration
`MCP_BRIDGE_STORE_LOGS``false`NoEnable file logging (vs console only)
`MCP_BRIDGE_LOG_DIR``app/logs` (beside the install)NoDirectory for log files (if `STORE_LOGS=true`). `.env.example` and Docker set it explicitly (e.g. `./logs`, `/app/logs`)
`MCP_BRIDGE_LOG_LEVEL``debug` (dev) / `info` (prod)NoLog level: `error`, `warn`, `info`, `debug`
`LOG_FORMAT`autoNoLog output format: `json` or `pretty`. Precedence: explicit `LOG_FORMAT` wins, else `NODE_ENV=production` selects `json`, else `pretty`
`MCP_SERVICE_NAME``actual-mcp-server`NoService name stamped on every structured (json) log record
Log Rotation (when `MCP_BRIDGE_STORE_LOGS=true`)
`MCP_BRIDGE_MAX_FILES``14d`NoKeep rotated logs for N days (e.g., `14d`, `30d`)
`MCP_BRIDGE_MAX_LOG_SIZE``20m`NoRotate when file reaches size (e.g., `20m`, `100m`)
`MCP_BRIDGE_ROTATE_DATEPATTERN``YYYY-MM-DD`NoDate pattern for rotated log filenames
Development & Debugging
`DEBUG`_(none)_NoEnable debug mode (verbose logging) when set to any truthy value
`LOG_LEVEL`_(none)_NoDebug-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
`MCP_BRIDGE_DEBUG_TRANSPORT``false`NoEnable transport-level debug logging
Advanced/Internal
`ACTUAL_API_CONCURRENCY``5`NoMax concurrent Actual API operations
`NODE_ENV`_(none)_ / `production`NoNode environment. No app default; the Docker image sets `production`, which selects json logs and hides stack traces in error responses
`VERSION`auto-detectedNoServer version (auto-set by build/Docker)
`TZ``UTC`NoTimezone for timestamps (e.g., `America/New_York`)

Multi-Budget Switching

Configure multiple Actual Budget files so the AI can switch between them at runtime using `actual_budgets_list_available` and `actual_budgets_switch`.

`BUDGET_N_SERVER_URL` and `BUDGET_N_PASSWORD` fall back to `ACTUAL_SERVER_URL` / `ACTUAL_PASSWORD` when omitted.

VariableRequiredFallback
`BUDGET_DEFAULT_NAME`No`"Default"`
`BUDGET_N_NAME`Yes (enables group)_(none)_
`BUDGET_N_SYNC_ID`Yes_(none)_
`BUDGET_N_SERVER_URL`No`ACTUAL_SERVER_URL`
`BUDGET_N_PASSWORD`No`ACTUAL_PASSWORD`
`BUDGET_N_ENCRYPTION_PASSWORD`No_(none)_
bash
# Default budget
ACTUAL_SERVER_URL=http://actual:5006
ACTUAL_PASSWORD=my-password
ACTUAL_BUDGET_SYNC_ID=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
BUDGET_DEFAULT_NAME=Personal

# Budget 1 (same server, same password)
BUDGET_1_NAME=Family
BUDGET_1_SYNC_ID=bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb

# Budget 2 (different server)
BUDGET_2_NAME=Business
BUDGET_2_SERVER_URL=https://actual-office.example.com
BUDGET_2_PASSWORD=office-password
BUDGET_2_SYNC_ID=cccccccc-cccc-cccc-cccc-cccccccccccc

Transport & Authentication

The server supports two transport modes:

ModeFlagUse caseAuth
HTTP`--http`LibreChat, LobeChat, Docker, multi-user deploymentsBearer token or OIDC
stdio`--stdio`Claude Desktop, Cursor, local single-user useNone (OS process isolation)

The two modes are mutually exclusive. Pass exactly one flag when starting the server.

stdio transport

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.

Key properties of stdio mode:

  • No network port. The transport is a pipe, not a socket.
  • No auth token. Process ownership is the security boundary.
  • All logs go to stderr so they never corrupt the JSON-RPC framing on stdout
  • The process exits when stdin closes (Claude Desktop shutting down)
  • All 77 tools are available, identical to HTTP mode

Start manually to verify:

bash
cd /path/to/actual-mcp-server
ACTUAL_SERVER_URL=http://localhost:5006 \
ACTUAL_PASSWORD=your_password \
ACTUAL_BUDGET_SYNC_ID=your-sync-id \
node dist/src/index.js --stdio

Send a test request (keep stdin open with `sleep`):

bash
{ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'; sleep 5; } \
| ACTUAL_SERVER_URL=http://localhost:5006 ACTUAL_PASSWORD=your_password ACTUAL_BUDGET_SYNC_ID=your-sync-id \
  node dist/src/index.js --stdio 2>/dev/null

Claude Desktop config (`claude_desktop_config.json`):

json
{
  "mcpServers": {
    "actual-budget": {
      "command": "node",
      "args": ["/absolute/path/to/actual-mcp-server/dist/src/index.js", "--stdio"],
      "env": {
        "ACTUAL_SERVER_URL": "http://localhost:5006",
        "ACTUAL_PASSWORD": "your_actual_password",
        "ACTUAL_BUDGET_SYNC_ID": "your-sync-id-here",
        "MCP_BRIDGE_DATA_DIR": "/absolute/path/to/actual-mcp-server/actual-data"
      }
    }
  }
}

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

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

HTTP transport

HTTP transport uses the `/http` endpoint (StreamableHTTP) with optional Bearer token or OIDC authentication.

Static Bearer token (single-user)

bash
# Generate a token
openssl rand -hex 32

# Add to .env
MCP_SSE_AUTHORIZATION=your_token_here

Clients send: `Authorization: Bearer your_token_here`

OIDC (multi-user)

bash
AUTH_PROVIDER=oidc
OIDC_ISSUER=https://sso.yourdomain.com
OIDC_RESOURCE=your-client-id    # must match 'aud' JWT claim
OIDC_SCOPES=                    # leave empty for Casdoor

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:

  • `GET /.well-known/oauth-protected-resource` (RFC 9728): identifies this server as a protected resource and points at your `OIDC_ISSUER` as the authorization server.
  • `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`.

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:

bash
AUTH_BUDGET_ACL_SOURCE=actual   # default: static (the AUTH_BUDGET_ACL map)
AUTH_BUDGET_ACL_CLAIM=auto      # default: mirrors Actual's own claim precedence

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.

Three things to know before enabling it:

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

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.

See AI Client Setup, OIDC for `AUTH_BUDGET_ACL` format and Casdoor notes.


Testing

CommandWhat It TestsRequires Live Server
`npm run build`TypeScript compilationNo
`npm run test:unit-js`77-tool smoke, schema validation, auth ACLNo
`npm run test:adapter`Adapter, retry logic, concurrencyNo
`npm run test:e2e`MCP protocol compliance (Playwright)No
`npm run test:e2e:docker:full`Full stack integrationYes (Docker)
`npm run test:integration`Live server sanity checksYes
`npm run test:integration:full`Full live integration suiteYes

Integration test levels (`tests/manual/`): `sanity` → `smoke` → `normal` → `extended` → `full` → `cleanup`

See `tests/manual/README.md` and `tests/e2e/README.md` for details.


Documentation

DocumentContents
docs/guides/MCP_CLIENTS_SETUP.mdStart here to connect Claude Desktop, Cursor, VS Code (Copilot), Gemini CLI, or Claude Code
docs/guides/AI_CLIENT_SETUP.mdLibreChat & LobeChat setup, Docker networking, HTTPS/TLS proxy, OIDC
docs/guides/DEPLOYMENT.mdDocker, Docker Compose profiles, production config, Kubernetes
docs/ARCHITECTURE.mdComponent layers, data flow, transport protocols
docs/SECURITY_AND_PRIVACY.mdAuth models, threat model, hardening
docs/TESTING_AND_RELIABILITY.mdTest strategy, coverage, reliability patterns
docs/NEW_TOOL_CHECKLIST.mdStep-by-step guide for adding a new MCP tool
CONTRIBUTING.mdDevelopment setup, code standards, PR process
.env.exampleFully annotated environment variable reference

Contributing

Contributions are welcome! See CONTRIBUTING.md for development setup, code standards, and the PR process.

Quick flow:

1. Fork → `git checkout -b feature/my-feature`

2. Make changes + add tests

3. `npm run build && npm run test:unit-js` must pass

4. Open a Pull Request


Architecture

  • Runtime: Node.js 22 (Alpine Linux in Docker)
  • Language: TypeScript (ESM, NodeNext module resolution)
  • MCP SDK: `@modelcontextprotocol/sdk`
  • Actual API: `@actual-app/api`
  • Validation: Zod (runtime types + JSON Schema for tool inputs)
  • Transports: Express + StreamableHTTP (`--http`) · `StdioServerTransport` (`--stdio`)
  • Logging: Winston with daily rotation (all output routed to stderr in stdio mode)

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 for full design documentation.


License

MIT. See LICENSE for details.


Acknowledgments


Disclaimer

This project started as a personal learning exercise to explore the Model Context Protocol technology. It is an independent open-source project, not affiliated with, endorsed by, or supported by Actual Budget or any other organisation.

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.


Support


Version: 0.19.3 | Tool Count: 77 (verified LibreChat-compatible)

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 and you feel like saying thanks, you can do that at github.com/sponsors/agigante80. Entirely optional, and nothing about the project changes either way.

Frequently asked questions

What is actual-mcp-server?

actual-mcp-server is Talk to your self-hosted Actual Budget from any MCP client: a remote HTTP server for LibreChat/LobeChat or a local stdio process for Claude Desktop, with 71 tools for transactions, budgets, rules, and bank sync. No Docker needed for local use.

How do I install actual-mcp-server?

Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.

Is actual-mcp-server open source?

Yes — it is hosted on GitHub at https://github.com/agigante80/actual-mcp-server and has 49 stars.

Related MCP tools

Run your own MCP server? See who uses it and what to fix.

Measure it with TrackMCP