Actual Budget Remote

Máy chủ MCP Actual Budget tích hợp OAuth để Gemini truy cập từ xa

Tài liệu

actual-mcp logo

CI Docker GitHub Release Node Version Model Context Protocol Listed on mcpservers.org

A self-contained Model Context Protocol (MCP) server that connects a self-hosted Actual Budget instance to AI assistants, including Claude Desktop, Cursor, Google Gemini, and custom agents. The server communicates over Streamable HTTP (POST /mcp) and includes an embedded OAuth 2.1 server that supports PKCE and RFC 7591 dynamic client registration.

Designed for single-user VPS deployments running actual-server and cloudflared (Cloudflare Tunnel), actual-mcp packages @actual-app/api (loot-core), a local SQLite budget cache, and the embedded OAuth server in one Docker container.


Architecture

actual-mcp Architecture


MCP tools reference

actual-mcp provides all 20 MCP tools: 10 read tools, 5 consolidated deep Resource Facades (with in-process human-readable name resolution and standardized result envelopes), and 5 budget and bank synchronization write tools. Every tool returns structured JSON in MCP text content blocks and includes safety annotations (readOnlyHint, destructiveHint).

Read tools (readOnlyHint: true, destructiveHint: false)

View read tools (10 tools)
ToolDescriptionParameters
list_accountsList all accounts in the target budget with ID, name, offbudget flag, closed flag, and current balance as a decimal amount.forceSync? (boolean)
get_transactionsQuery transactions with enriched accountName, categoryName, and payeeName alongside IDs and decimal amounts.accountId? (string), startDate? (YYYY-MM-DD), endDate? (YYYY-MM-DD), categoryId? (string), payeeId? (string), searchText? (string, case-insensitive match on payee or notes), limit? (1..500, default 100), forceSync? (boolean)
get_spending_summaryAggregate transactions over a date range into a spending summary grouped by 'category', 'payee', or 'month' with split transactions inlined. Excludes internal transfers and off-budget accounts by default.startDate (YYYY-MM-DD, required), endDate (YYYY-MM-DD, required), groupBy? ('category' | 'payee' | 'month', default 'category'), includeTransfers? (boolean, default false), includeOffBudget? (boolean, default false), forceSync? (boolean)
run_aql_queryExecute a validated, read-only AQL query against @actual-app/api and convert monetary fields to decimal amounts.table ('transactions' | 'accounts' | 'categories' | 'category_groups' | 'payees' | 'schedules' | 'rules'), filter? (object), select? (string | Array<string | object>), groupBy? (string | string[]), orderBy? (string | object | Array), limit? (1..1000, default 100), offset? (number), splits? ('inline' | 'grouped' | 'all', default 'inline'), forceSync? (boolean)
list_categoriesList all category groups and their child categories (id, name, group_id, is_income, hidden).forceSync? (boolean)
list_payeesList all payees (id, name, transfer_acct).forceSync? (boolean)
list_budget_monthsList available YYYY-MM budget months in the target budget.forceSync? (boolean)
get_budget_monthInspect a YYYY-MM budget month summary (toBudget, totalIncome, totalBudgeted, totalSpent, totalBalance, and per-category amounts in decimal units).month (YYYY-MM, required), forceSync? (boolean)
list_schedulesList all recurring schedules in the target budget with ID, name, rule ID, next date, completed flag, transaction posting flag, payee ID, account ID, decimal amount, amount operator, and recurrence configuration.forceSync? (boolean)
list_rulesList all automation rules in the target budget with conditions, actions, stage, and operator, with optional payee filtering. Amounts in conditions and actions are converted to decimal amounts.payeeId? (string), forceSync? (boolean)

Deep Resource Facades (readOnlyHint: false, destructiveHint: true)

Consolidated entity lifecycle facades (5 tools)
ToolDestructiveDescriptionParameters
manage_transactionstrueConsolidated Resource Facade for transaction lifecycle (action: 'create' | 'update' | 'delete'). Supports batch creation (1–50 items with decimal amounts, in-process account/category/payee name resolution, and runRules default true), single or bulk updates across 1–50 transaction IDs with automatic payee creation, and single or bulk deletions.action ('create' | 'update' | 'delete', required), account? / accountId? (string), transactions? (Array<{ date, amount, payee?, payeeName?, payeeId?, category?, categoryId?, notes?, cleared?, importedId? }>, 1–50 items), runRules? (boolean, default true), id? (string), ids? (string[], 1–50 items), date? (YYYY-MM-DD), amount? (number), payee? / payeeId? / payeeName? (string | null), category? / categoryId? (string | null), notes? (string | null), cleared? (boolean)
manage_categoriestrueConsolidated Resource Facade for category and category group lifecycle (action: 'create' | 'update' | 'delete' | 'move', target: 'category' | 'group'). Resolves names or UUIDs in-process, automatically creates missing category groups on category create/move, and enforces transferTo when deleting non-empty categories or groups.action ('create' | 'update' | 'delete' | 'move', required), target? ('category' | 'group', default 'category'), id? (string), name? (string), newName? (string), group? / groupId? (string), isIncome? (boolean), hidden? (boolean), transferTo? / transferCategoryId? (string)
manage_payeestrueConsolidated Resource Facade for payee lifecycle (action: 'create' | 'update' | 'delete' | 'merge'). Accepts human-readable payee names or UUIDs and merges duplicate payees in a single call.action ('create' | 'update' | 'delete' | 'merge', required), id? / name? / payee? (string), newName? (string), target? / targetId? / targetPayeeId? (string), merge? / mergeIds? / sourcePayeeIds? (string[], min 1)
manage_accountstrueConsolidated Resource Facade for account lifecycle (action: 'create' | 'update' | 'close' | 'reopen'). Accepts human-readable names or UUIDs for accounts and transfer categories, and enforces safe balance transfer on closure.action ('create' | 'update' | 'close' | 'reopen', required), id? / name? / account? (string), newName? (string), type? ('checking' | 'savings' | 'credit' | 'investment' | 'mortgage' | 'debt' | 'other'), offbudget? (boolean), initialBalance? (number), transferTo? / transferAccountId? (string), transferCategory? / transferCategoryId? (string), forced? (boolean)
manage_automationstrueConsolidated Resource Facade for recurring schedules and automation rules (target: 'schedule' | 'rule', action: 'create' | 'update' | 'delete' | 'preview_dates'). Resolves account, payee, category, and schedule names in-process and converts decimal amounts automatically.target ('schedule' | 'rule', required), action ('create' | 'update' | 'delete' | 'preview_dates', required), id? / scheduleId? / name? (string), account? / accountId? (string), payee? / payeeId? (string | null), amount? (number | { num1, num2 }), amountOp? ('is' | 'isapprox' | 'isbetween'), date? (string | object), postsTransaction? / posts_transaction? (boolean), resetNextDate? (boolean), start? / end? (YYYY-MM-DD), count? (1..100), forceSync? (boolean), stage? ('pre' | 'post' | 'default' | null), conditionsOp? ('and' | 'or'), conditions? (Array), actions? (Array)

Budget & bank sync write tools (readOnlyHint: false, destructiveHint: false)

Budget envelope & bank synchronization tools (5 tools)
ToolDestructiveDescriptionParameters
set_budget_amountfalseUpdate a category budgeted decimal amount or overspending carryover flag in a YYYY-MM budget month, returning { previous, updated } states.month (YYYY-MM, required), categoryId (string, required), budgeted? (number), carryover? (boolean)
run_bank_syncfalseTrigger bank synchronization for a single account or for all linked accounts when accountId is omitted. Returns { updatedAccountIds, newTransactionsCount, matchedTransactionsCount, errors }.accountId? (string, min 1)
set_budget_carryoverfalseSet the overspending carryover flag for a category envelope in a budget month (YYYY-MM). When true, overspent category balance rolls into the next month.month (YYYY-MM, required), categoryId (string, min 1, required), carryover (boolean, required)
hold_budget_for_next_monthfalseHold a decimal amount from the current month available budget for the next month.month (YYYY-MM, required), amount (number, finite decimal amount, required)
reset_budget_holdfalseReset the budget hold for a month and return held funds to the available budget.month (YYYY-MM, required)

Quick start

To install on a VPS without cloning the repository, run the interactive setup wizard:

mkdir -p actual-mcp && cd actual-mcp && bash -c "$(curl -fsSL https://raw.githubusercontent.com/PedroLiu1999/actual-mcp/main/scripts/setup-wizard.sh)"

The setup wizard automates configuration:

  1. Downloads docker-compose.yml and creates the persistent data/ directory.
  2. Configures .env with PUBLIC_URL and the loopback PORT (default 3000).
  3. Prompts for OAUTH_ADMIN_PASSWORD to protect the authorization consent screen.
  4. Sets up OAuth redirect URIs for your selected AI clients, including Claude Desktop, Cursor, Google Gemini, and ChatGPT.
  5. Configures the connection to your Actual Budget sync server: ACTUAL_SERVER_URL, server password, and ACTUAL_SYNC_ID.
  6. Pulls the container image from GitHub Container Registry and starts the service.
  7. Verifies health against local and public /healthz endpoints.
  8. Displays tailored connection instructions for each selected AI client.

Environment variables

To configure environment variables manually, copy .env.example to .env. Alternatively, run ./scripts/setup-wizard.sh.

VariableRequiredDefaultDescription
PUBLIC_URLYes-Public HTTPS origin exposed by your Cloudflare Tunnel (for example, https://actual-mcp.example.com, without trailing slash).
PORTNo3000Port that the HTTP server listens on inside the container, mapped to host loopback in docker-compose.yml.
ACTUAL_MCP_VERSIONNolatestImage tag from GitHub Container Registry (ghcr.io/pedroliu1999/actual-mcp).
DATA_DIRYes/dataPath to the persistent directory storing oauth-tokens.json (token store) and budget-cache/ (budget cache).
OAUTH_ADMIN_PASSWORDYes-Admin passphrase required on the HTML consent page (GET /authorize) to approve an OAuth grant.
OAUTH_ALLOWED_REDIRECT_URISNohttp://localhost,http://127.0.0.1Comma-separated list of allowed OAuth redirect URIs. Loopback ports match dynamically per RFC 8252. Default includes well-known Google callback endpoints.
ACTUAL_SERVER_URLYes-URL of your self-hosted Actual Budget sync server (for example, http://host.docker.internal:5006).
ACTUAL_PASSWORDYes-Password used to authenticate with your Actual Budget sync server.
ACTUAL_SYNC_IDYes-Sync ID of the target budget. Find this in Actual Budget under Settings → Show advanced settings → Sync ID.
ACTUAL_ENCRYPTION_PASSWORDNo-End-to-end encryption password for the target budget. Leave empty if unencrypted.
SYNC_TTL_SECONDSNo30Coalesced sync freshness window in seconds for read tools.

VPS deployment guide

This guide assumes your VPS already runs:

  1. An Actual Budget sync server (actual-server, typically on port 5006).
  2. A Cloudflare Tunnel (cloudflared).

Step 1: Clone and configure the environment

To clone the repository and prepare the persistent storage directory, run:

git clone git@github.com:PedroLiu1999/actual-mcp.git
cd actual-mcp
mkdir -p data

To configure .env interactively, run:

./scripts/setup-wizard.sh

To configure .env manually, copy the sample file and fill in the values:

cp .env.example .env

Set OAUTH_ADMIN_PASSWORD to a strong passphrase, then set the remaining variables:

  • Set PUBLIC_URL=https://actual-mcp.yourdomain.com, matching the public hostname routed by cloudflared.
  • Set ACTUAL_SERVER_URL:
    • When actual-server publishes port 5006 on the VPS host (for example, 127.0.0.1:5006:5006), set ACTUAL_SERVER_URL=http://host.docker.internal:5006. docker-compose.yml maps host.docker.internal to the Docker host gateway automatically.
    • When actual-server uses HTTPS with a self-signed certificate, connect over local HTTP through host.docker.internal, or mount the CA certificate and set NODE_EXTRA_CA_CERTS.
  • Set ACTUAL_PASSWORD to your Actual server password.
  • Set ACTUAL_SYNC_ID to your budget sync ID (Settings → Show advanced settings → Sync ID in Actual Budget).

Step 2: Start the container

To run using the official image from GitHub Container Registry:

docker compose pull
docker compose up -d

To build and run locally from source:

docker compose up -d --build

To verify that the service is running and the target budget initialized cleanly:

curl -s http://127.0.0.1:3000/healthz | jq .

Expected response:

{
  "status": "ok",
  "targetBudget": {
    "ready": true,
    "lastSyncedAt": "2026-11-04T18:00:00.000Z"
  }
}

Step 3: Route a public hostname through Cloudflare Tunnel

Because docker-compose.yml binds to 127.0.0.1:${PORT:-3000}:3000 on the VPS host, actual-mcp is never exposed directly on the public IP address of your VPS. Traffic routes only through your Cloudflare Tunnel.

Option A: Cloudflare Zero Trust dashboard

  1. Open the Cloudflare Zero Trust Dashboard → Networks → Tunnels.
  2. Select your tunnel and choose Configure → Public Hostname → Add a public hostname.
  3. Set the hostname fields:
    • Subdomain: actual-mcp (or your chosen subdomain)
    • Domain: yourdomain.com
    • Service Type: HTTP
    • URL: 127.0.0.1:3000 (or host.docker.internal:3000 if cloudflared runs inside Docker with host gateway mapping)
  4. Save the hostname.

Option B: Local cloudflared configuration file

Add an ingress rule to /etc/cloudflared/config.yml before the catch-all http_status:404 rule:

ingress:
  - hostname: actual-mcp.yourdomain.com
    service: http://127.0.0.1:3000
  # ... your existing ingress rules ...
  - service: http_status:404

Create the DNS route and reload cloudflared:

cloudflared tunnel route dns <your-tunnel-name> actual-mcp.yourdomain.com
sudo systemctl restart cloudflared

Cloudflare WAF and Bot Fight Mode: Google Gemini sends outbound MCP requests with User-Agent: Google. Ensure Cloudflare Bot Fight Mode or custom WAF rules on actual-mcp.yourdomain.com do not challenge (CAPTCHA or JavaScript challenges) requests to /mcp, /authorize, /token, /revoke, /register, or /.well-known/*.

To verify external OAuth discovery over HTTPS:

curl -s https://actual-mcp.yourdomain.com/.well-known/oauth-protected-resource | jq .
curl -s https://actual-mcp.yourdomain.com/.well-known/oauth-authorization-server | jq .

Connect AI assistants

Once https://actual-mcp.yourdomain.com is accessible through your Cloudflare Tunnel:

  1. Add the MCP server URL in your AI client (such as Claude Desktop, Cursor, or Google Gemini):
    • MCP server URL: https://actual-mcp.yourdomain.com/mcp
  2. Client registration: Clients that support RFC 7591 Dynamic Client Registration (such as Google Gemini) register automatically. Clients connecting directly as public OAuth 2.1 PKCE clients do not require a pre-shared client ID or client secret.
  3. Authorization:
    • The client discovers OAuth metadata and opens https://actual-mcp.yourdomain.com/authorize in your browser.
    • On the consent screen, enter your OAUTH_ADMIN_PASSWORD and select Approve Access.
  4. The client completes the PKCE token exchange and receives an access token. Your AI client can now invoke Actual Budget tools.

Local development and verification

Development requires Node.js 22.0.0 or later.

# Install dependencies
npm install

# Run the test suite (Vitest)
npm test

# Run TypeScript typechecking
npm run typecheck

# Compile TypeScript to dist/
npm run build

Continuous integration

Automated GitHub Actions workflows run on every pull request and push to main:

  • The CI pipeline (.github/workflows/ci.yml) runs dependency installation (npm ci), TypeScript typecheck (npm run typecheck), the full Vitest test suite (npm test), and compilation (npm run build) on Node.js versions 22.x and 24.x.
  • The container publishing pipeline (.github/workflows/docker-publish.yml) builds and pushes multi-architecture (linux/amd64, linux/arm64) container images to GitHub Container Registry.

Brand and logo assets

The official actual-mcp brand identity ("The Agent Spark") is located in docs/assets/logo/: