trek-boldly-go/actual-budget-mcp-server
If you are the rightful owner of actual-budget-mcp-server and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to dayong@mcphub.com.
A minimal Model Context Protocol (MCP) HTTP server for the Actual Budget API, designed to expose Actual tools over HTTP with server-sent events for streaming responses.
Simple Streamable HTTP MCP Server
A minimal Model Context Protocol (MCP) HTTP server for the Actual Budget API. It exposes the Actual tools over HTTP with server-sent events for streaming responses.
What it does
- Runs an MCP server at
/mcpwith streaming (SSE) support. - Provides tool endpoints for Accounts, Transactions, Categories, Payees, Rules, reporting, and AI-style summaries.
Prerequisites
- Node 18+ (matches the SDK requirement)
- npm
- Actual credentials:
ACTUAL_SERVER_URL,ACTUAL_PASSWORD,ACTUAL_SYNC_ID(required to start the server)
Install
npm install
Run (dev)
export ACTUAL_SERVER_URL=https://your-actual
export ACTUAL_PASSWORD=...
export ACTUAL_SYNC_ID=... # Find your sync_id in your Actual Server advanced settings
npm run dev
The server listens on http://localhost:3000/mcp by default.
Build & run compiled output
npm run build
npm start
Docker
Build and run locally:
npm run docker:run
# or manually
npm run docker:build
docker run --rm -p 3000:3000 \
-e MCP_PORT=3000 \
-e ACTUAL_SERVER_URL=https://your-actual \
-e ACTUAL_PASSWORD=... \
-e ACTUAL_SYNC_ID=... \
ghcr.io/trek-boldly-go/actual-budget-mcp-server:latest
Docker Compose (MCP + Keycloak OAuth)
The repo includes a compose stack that runs the MCP server and Keycloak, with a realm preloaded from an env-templated JSON. First/last name and email are populated so the default user can authenticate without extra profile setup.
docker compose up -d
Services:
mcp(ports3000:3000): runs withMCP_AUTH_MODE=oauthand introspects tokens against Keycloak.keycloak(ports8080:8080): importsdocker/keycloak/realm-export/actual-mcp-realm.template.jsonrendered bykeycloak-realm-builder. If you changeKEYCLOAK_REALM, also override the MCP issuer envs (compose defaults do not nest).keycloak-realm-builder: renders the realm template with env vars before Keycloak starts.
Images and platforms:
mcpservice keeps the published image name and will build locally if missing (so arm64 works). Override the target platform withMCP_DOCKER_PLATFORM(defaultlinux/amd64); if you prefer native, setMCP_DOCKER_PLATFORM=linux/arm64.
Environment knobs (with defaults):
MCP_PORT(default3000),MCP_AUTH_MODE(defaultoauth),MCP_PUBLIC_URL(defaulthttp://localhost:3000/mcp)MCP_OAUTH_ISSUER_URL(defaulthttp://keycloak:8080/realms/${KEYCLOAK_REALM})KEYCLOAK_REALM(defaultactual-mcp)KEYCLOAK_PUBLIC_CLIENT_ID(defaultactual-mcp-public)KEYCLOAK_INTROSPECTION_CLIENT_ID/KEYCLOAK_INTROSPECTION_CLIENT_SECRET(defaultactual-mcp-introspection/actual-mcp-introspection-secret)KEYCLOAK_DEMO_USER/KEYCLOAK_DEMO_PASSWORD(defaultdemo-user/demo-pass)KEYCLOAK_DEMO_FIRST_NAME/KEYCLOAK_DEMO_LAST_NAME(defaultDemo/User)KEYCLOAK_ADMIN/KEYCLOAK_ADMIN_PASSWORD(defaultadmin/admin)- Actual credentials (required):
ACTUAL_SERVER_URL,ACTUAL_PASSWORD,ACTUAL_SYNC_ID
Getting a token for testing:
curl -X POST http://localhost:8080/realms/actual-mcp/protocol/openid-connect/token \
-d grant_type=password \
-d client_id=actual-mcp-public \
-d username=demo-user \
-d password=demo-pass
Then call the MCP server with Authorization: Bearer <access_token>. If you prefer UI login, you can also run a standard authorization code flow via your OAuth client.
Redirect URIs for actual-mcp-public (preloaded):
http://localhost:3000/*http://localhost:4173/*Add your MCP client’s exact callback if it differs.
Environment variables
MCP_PORT(default3000)MCP_PUBLIC_URL(defaulthttp://localhost:<MCP_PORT>/mcp) used for OAuth resource metadataMCP_AUTH_MODE(bearerdefault) choosenone,bearer, oroauthMCP_BEARER_TOKENrequired whenMCP_AUTH_MODE=bearer- OAuth mode:
MCP_OAUTH_INTERNAL_ISSUER_URL(e.g.,http://keycloak:8080/realms/actual-mcp) used by the server for discovery/introspection (defaults toMCP_OAUTH_ISSUER_URL)MCP_OAUTH_ISSUER_URL(back-compat) falls back to internal issuer whenMCP_OAUTH_INTERNAL_ISSUER_URLis unset. If you changeKEYCLOAK_REALM, update these explicitly; compose does not nest defaults.MCP_OAUTH_CLIENT_ID/MCP_OAUTH_CLIENT_SECRET(used for token introspection)MCP_OAUTH_INTROSPECTION_URL(optional override; defaults to issuer metadata)MCP_OAUTH_AUDIENCE(optional audience/resource value to enforce)MCP_OAUTH_PUBLIC_ISSUER_URL(optional) public-facing issuer to advertise in metadata; server still uses the internal issuer for discovery/introspectionMCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL(set totruefor HTTP issuers in dev only)
- Actual configuration:
ACTUAL_SERVER_URL,ACTUAL_PASSWORD,ACTUAL_SYNC_ID(required for real Actual usage) ACTUAL_DATA_DIR(default/app/.actual-datain Docker)ACTUAL_ENCRYPTION_PASS(optional)- Amounts: all tools and rules use the currency’s smallest unit (e.g., cents for USD).
Authentication
The server now supports three modes via MCP_AUTH_MODE (default bearer):
bearer(default): RequireAuthorization: Bearer <MCP_BEARER_TOKEN>on every/mcprequest. SetMCP_BEARER_TOKENto a long random value.oauth: Use OAuth 2.1/2.0 Bearer tokens from an external Authorization Server (e.g., Keycloak). The server introspects tokens withMCP_OAUTH_CLIENT_ID/MCP_OAUTH_CLIENT_SECRET, exposes OAuth protected resource metadata at/.well-known/oauth-protected-resource/mcp, and enforcesMCP_OAUTH_AUDIENCEif provided.none: No authentication (only for trusted networks).
Quick bearer setup:
- Set
MCP_AUTH_MODE=bearerandMCP_BEARER_TOKEN="<long-random-token>". - Restart the server.
- Configure your MCP client to send
Authorization: Bearer <long-random-token>on/mcp.
Quick OAuth (Keycloak) flow:
- Set
MCP_AUTH_MODE=oauth,MCP_OAUTH_ISSUER_URL,MCP_OAUTH_CLIENT_ID,MCP_OAUTH_CLIENT_SECRET, and optionallyMCP_OAUTH_AUDIENCE. - Ensure your Authorization Server supports the RFC 7662 introspection endpoint; the server will auto-discover it from the issuer metadata.
- Clients can discover the resource metadata at
/.well-known/oauth-protected-resource/mcpand follow theauthorization_serversentry to your IdP.
Development notes
- Lint:
npm run lint(standard-with-typescript, semicolons enforced) - Build:
npm run build - CI publishes Docker images to GHCR on
mainpushes (.github/workflows/ci.yml).
Where to wire in the real Actual API
src/actual/client.tsconnects directly to your Actual server. All reads and writes are live; provide required env vars before starting.