# ============================================================================ # ACTUAL MCP SERVER - ENVIRONMENT CONFIGURATION # ============================================================================ # Copy this file to .env and fill in required values. # Security: never commit .env to version control. # ============================================================================ # ---------------------------------------------------------------------------- # ACTUAL BUDGET CONNECTION (REQUIRED) # ---------------------------------------------------------------------------- # URL of your Actual Budget server # Examples: # Local: http://localhost:5006 # Docker: http://actual-server:5006 # Remote: https://actual.yourdomain.com ACTUAL_SERVER_URL=http://localhost:5006 # Password you use to access Actual Budget ACTUAL_PASSWORD=your_password_here # Budget Sync ID. Find it in Actual Budget: # Settings then Show Advanced Settings then Sync ID # This is the *default* budget (named via BUDGET_DEFAULT_NAME below). ACTUAL_BUDGET_SYNC_ID=your-budget-sync-id-here # Optional: encryption password for E2E-encrypted budgets # Only required if your default budget uses end-to-end encryption # ACTUAL_BUDGET_PASSWORD= # Optional: per-operation timeout in milliseconds for every upstream Actual API # call (init, budget download, sync, and each tool operation). #270: a stalled # upstream call cannot hold the global API mutex forever; it rejects after this # many ms so subsequent tool calls recover instead of hanging. Default 30000 # (30s). Set to 0 to disable the bound (not recommended). # ACTUAL_OP_TIMEOUT_MS=30000 # # Security (#161): when ACTUAL_BUDGET_PASSWORD (or a BUDGET_N_ENCRYPTION_PASSWORD) is set, # the upstream must be https:// so the encryption password is not sent in cleartext. # Set ALLOW_INSECURE_UPSTREAM=true only for a trusted isolated network (e.g. Docker). # ALLOW_INSECURE_UPSTREAM=false # Optional: friendly name for the default budget (shown in actual_budgets_list_available) # Default: "Default" # BUDGET_DEFAULT_NAME=My Main Budget # ---------------------------------------------------------------------------- # ADDITIONAL BUDGETS (Optional, for multi-budget / multi-server switching) # ---------------------------------------------------------------------------- # Define extra budgets the AI can switch to at runtime via actual_budgets_switch. # Use actual_budgets_list_available to see all configured budgets. # # Variables for budget N (N = 1, 2, 3, ...): # BUDGET_N_NAME Human-readable name (required to enable group) # BUDGET_N_SYNC_ID Budget Sync ID (required) # BUDGET_N_SERVER_URL Falls back to ACTUAL_SERVER_URL when omitted # BUDGET_N_PASSWORD Falls back to ACTUAL_PASSWORD when omitted # BUDGET_N_ENCRYPTION_PASSWORD Optional, only for E2E-encrypted budgets # # Fallback rule: only specify what differs from the default ACTUAL_* values. # # Number budgets consecutively from 1, with no gaps: enumeration stops at the first # missing BUDGET_N_NAME, so BUDGET_1 + BUDGET_3 (no BUDGET_2) silently ignores BUDGET_3. # A [CONFIG] ... IGNORED warning is logged at startup if a gap is detected (#289). # # Optional: give the default budget a friendly name (default: "Default") # BUDGET_DEFAULT_NAME=My Main Budget # # Example 1: same server, same password (SERVER_URL + PASSWORD omitted): # BUDGET_1_NAME=Shared Family Account # BUDGET_1_SYNC_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # # Example 2: same server, different password (SERVER_URL omitted): # BUDGET_2_NAME=Business # BUDGET_2_PASSWORD=business-password # BUDGET_2_SYNC_ID=yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy # # Example 3: entirely separate Actual server (all three fields required): # BUDGET_3_NAME=Remote Office # BUDGET_3_SERVER_URL=https://actual-office.example.com:5006 # BUDGET_3_PASSWORD=office-password # BUDGET_3_SYNC_ID=zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz # BUDGET_3_ENCRYPTION_PASSWORD=e2e-password-if-encrypted # ---------------------------------------------------------------------------- # MCP BRIDGE SERVER SETTINGS # ---------------------------------------------------------------------------- # Port the MCP server listens on # Default: 3600 MCP_BRIDGE_PORT=3600 # Host to bind to # 0.0.0.0 = all interfaces (needed in Docker) # 127.0.0.1 = local connections only # Default: 0.0.0.0 MCP_BRIDGE_BIND_HOST=0.0.0.0 # Directory for Actual Budget local data (SQLite files) # Default: ./actual-data MCP_BRIDGE_DATA_DIR=./actual-data # Where the per-user budget ACL comes from (#338). # static (default) = the AUTH_BUDGET_ACL JSON above, hand-maintained # actual = derived from the Actual server's own per-file access list # The 'actual' source requires the Actual server to run in multi-user (OpenID) # mode, AND that server must have been bootstrapped with a PASSWORD before OpenID # was added: @actual-app/api authenticates with a password, and an OpenID-only # server rejects that, so this MCP server cannot connect to one at all. # Default: static # AUTH_BUDGET_ACL_SOURCE=static # Which token claim identifies the principal when AUTH_BUDGET_ACL_SOURCE=actual. # Default 'auto' mirrors Actual's OWN precedence and matches the result against # Actual's user_name: preferred_username -> login -> email -> id -> sub # Actual stores that string as user_name and mints an unrelated UUID as its # userId, so matching a token's sub against userId can never work (#343). # Set a single claim name to pin one instead, which is worth doing if your IdP # lets users edit preferred_username or does not verify email. Whatever you pin # must be the claim your IdP used to populate Actual's user_name. # Default: auto # AUTH_BUDGET_ACL_CLAIM=auto # Explicit principal bindings, used when AUTH_BUDGET_ACL_SOURCE=actual (#345). # Format: =[,=...] # Consulted BEFORE the claim precedence above, and AUTHORITATIVE: if a sub is # bound and its userName matches no budget file, the principal is DENIED rather # than falling back to a claim, so an operator typo is loud instead of silent. # Keyed on sub because sub is always present and verified, and because it does # not change when a user is renamed at the IdP (Actual freezes user_name at # first login, so a rename otherwise breaks the join permanently). # A blank userName is rejected at startup: it would match this server's own # service-account row, which owns every file. # Find the userNames to bind to in the startup log line 'dynamic ACL preflight'. # Default: (empty, no bindings) # AUTH_BUDGET_ACL_IDENTITY_MAP=a1b2c3d4e5f6opaque=jdoe # Where the ACL reads identity claims from when AUTH_BUDGET_ACL_SOURCE=actual (#346). # token (default) the verified access-token payload. No network call. # userinfo the IdP's UserInfo endpoint, which is the SAME document Actual # derives user_name from, so the join stops depending on whether your # IdP copies preferred_username into access tokens. # userinfo is OPT-IN because it makes your IdP a hard dependency of authorization # (its downtime becomes a denial) and because it needs the access token to carry # the openid scope, which this server does not require. The UserInfo endpoint must # be same-origin with OIDC_ISSUER, and the sub it returns must equal the token's # sub (OIDC Core 5.3.2) or the request is denied. # Default: token # AUTH_BUDGET_ACL_IDENTITY_SOURCE=token # Timeout for that UserInfo request, in ms. Clamped to [250, 60000]. There is no # disable value on purpose: an unbounded call on the authorization path is how a # slow IdP becomes an outage. # Default: 5000 # AUTH_BUDGET_ACL_USERINFO_TIMEOUT_MS=5000 # Directory where actual_budgets_export writes budget .zip files (#332). # Leave unset to derive it as /exports, which is the # writable, persisted location in the Docker image. Set it only if you want # exports somewhere else; the path must be writable by the runtime user. # Default: (unset) -> /exports # ACTUAL_EXPORT_DIR=./actual-data/exports # Transport mode: the only supported value is --http # Default: --http # MCP_TRANSPORT_MODE=--http # HTTP endpoint path # Default: /http # MCP_HTTP_PATH=/http # Advertised HTTP path (shown in logs / returned to clients, may differ from # the routing path when behind a reverse proxy with path rewriting) # Default: same as MCP_HTTP_PATH # MCP_BRIDGE_HTTP_PATH=/http # ---------------------------------------------------------------------------- # REVERSE PROXY / TLS SETTINGS (Optional) # ---------------------------------------------------------------------------- # Set these when running behind a reverse proxy (nginx, Traefik, Caddy, etc.) # so that the server announces the correct public URL to clients. # Override the advertised hostname (default: auto-detected local IP) # MCP_BRIDGE_PUBLIC_HOST=mcp.yourdomain.com # Override the advertised scheme (http or https) # MCP_BRIDGE_PUBLIC_SCHEME=https # Force https in the advertised URL (alternative to MCP_BRIDGE_PUBLIC_SCHEME) # Set to 'true' when TLS is terminated upstream # MCP_BRIDGE_USE_TLS=false # Native TLS (terminate TLS directly in the MCP server without a reverse proxy) # When MCP_ENABLE_HTTPS=true, BOTH MCP_HTTPS_CERT and MCP_HTTPS_KEY are required; # the server refuses to start otherwise (#169). # Max incoming JSON body size (#168). Express byte string, e.g. 512kb or 2mb. Default 512kb. # MCP_HTTP_BODY_LIMIT=512kb # MCP_ENABLE_HTTPS=false # MCP_HTTPS_CERT=/app/certs/cert.pem # MCP_HTTPS_KEY=/app/certs/key.pem # ---------------------------------------------------------------------------- # SESSION MANAGEMENT # ---------------------------------------------------------------------------- # Maximum concurrent MCP sessions # Default: 15 # MAX_CONCURRENT_SESSIONS=15 # Session idle timeout in minutes. Sessions with no activity are closed # Default: 5 # SESSION_IDLE_TIMEOUT_MINUTES=5 # Maximum concurrent Actual API operations (connection pool depth) # Default: 5 # ACTUAL_API_CONCURRENCY=5 # Enable connection pooling (set to 'false' to disable, rarely needed) # Default: true # USE_CONNECTION_POOL=true # ---------------------------------------------------------------------------- # AUTHENTICATION # ---------------------------------------------------------------------------- # Static Bearer token for simple single-user setups. # Clients must send: Authorization: Bearer # Generate with: openssl rand -hex 32 # Leave unset to disable token auth (not recommended for production). # MCP_SSE_AUTHORIZATION=your-secure-token-here # Required-by-default HTTP auth (#242). On a non-loopback bind (anything other # than 127.0.0.1/::1/localhost, so the 0.0.0.0 default counts) with no token and # no AUTH_PROVIDER=oidc, the server REFUSES to start. Set this to the exact string # 'true' to deliberately run unauthenticated (for example behind your own # authenticating reverse proxy). Loopback binds and stdio mode are unaffected. # MCP_ALLOW_UNAUTHENTICATED=false # Authentication provider. Options: 'none' (default), 'oidc' # When set to 'oidc', JWT Bearer tokens are validated via JWKS instead of the # static MCP_SSE_AUTHORIZATION token. # AUTH_PROVIDER=none # OIDC issuer URL (required when AUTH_PROVIDER=oidc) # Examples: # Keycloak: https://auth.example.com/realms/myrealm # Azure AD: https://login.microsoftonline.com//v2.0 # Auth0: https://.auth0.com/ # OIDC_ISSUER=https://auth.example.com/realms/myrealm # Allow a plaintext (http) OIDC issuer on a trusted network (#244). Off by default: # an http issuer is refused at startup because a network attacker could swap the # JWKS and forge tokens. Set to 'true' ONLY for local/LAN testing (e.g. a LAN # Casdoor over http). Loopback issuers (localhost/127.0.0.1) never need this. # OIDC_ALLOW_INSECURE_ISSUER=false # This server's resource identifier (audience claim in JWT validation) # Must match the 'aud' claim your IdP puts in issued tokens. # OIDC_RESOURCE=https://actual-mcp.example.com # Extra accepted JWT audiences beyond OIDC_RESOURCE, comma-separated (#245). # For IdPs that put the client-id (not the resource URI) in the 'aud' claim, e.g. # Authentik. Strict allowlist: the accepted set is OIDC_RESOURCE plus these, never # a wildcard. Leave empty unless your IdP needs it. # OIDC_ACCEPTED_AUDIENCES=my-client-id # Opt-in allowlist of cross-origin JWKS hosts, comma-separated host or host:port # entries (#254). Needed only when the IdP legitimately serves its JWKS from a # different host than the issuer, e.g. Google (issuer accounts.google.com, keys # on www.googleapis.com). Exact match, lowercased, no wildcards; https and the # no-credentials checks still apply. Leave empty for same-origin-only (default). # OIDC_JWKS_TRUSTED_HOSTS=www.googleapis.com # Comma-separated required scopes (optional; tokens must have ALL listed) # OIDC_SCOPES= # Per-user budget ACL (optional; restricts which budgets each user may access) # JSON object: principal → list of allowed budget sync-IDs, or ["*"] for all. # Principal key formats: # "alice@example.com" matches 'email' claim # "some-sub-uuid" matches 'sub' claim # "group:admin" matches 'groups' or 'roles' array element # Example: # AUTH_BUDGET_ACL={"alice@example.com":["budget-sync-id-1"],"group:admin":["*"]} # AUTH_BUDGET_ACL= # ---------------------------------------------------------------------------- # LOGGING # ---------------------------------------------------------------------------- # Log output format: json | pretty # Precedence: explicit LOG_FORMAT wins; else NODE_ENV=production selects json; else pretty. # Use json in production so logs can be aggregated/queried by a log backend; pretty # (human-readable, colorized only on a TTY) is the dev default. # Default: pretty (json when NODE_ENV=production) # LOG_FORMAT=json # Log level: error | warn | info | debug # Default: debug in dev, info when NODE_ENV=production (override with this var) MCP_BRIDGE_LOG_LEVEL=debug # Debug-detection toggle, distinct from MCP_BRIDGE_LOG_LEVEL (the winston level). # Setting LOG_LEVEL=debug also turns on extra transport debug output (see utils.ts). # LOG_LEVEL=debug # Service name stamped on every structured (json) log record # Default: actual-mcp-server # MCP_SERVICE_NAME=actual-mcp-server # Write logs to files (in addition to stdout) # Default: false MCP_BRIDGE_STORE_LOGS=false # Directory for log files (only used when MCP_BRIDGE_STORE_LOGS=true) # Default: ./logs MCP_BRIDGE_LOG_DIR=./logs # Log rotation: date pattern for filenames (winston-daily-rotate-file format) # Default: YYYY-MM-DD (e.g. app-2025-11-09.log) # MCP_BRIDGE_ROTATE_DATEPATTERN=YYYY-MM-DD # Maximum size per log file before rotation (e.g. 20m, 100m, 1g) # Default: 20m # MCP_BRIDGE_MAX_LOG_SIZE=20m # How long to retain rotated log files (e.g. 14d, 30d) # Default: 14d # MCP_BRIDGE_MAX_FILES=14d # Enable verbose transport-level debug output (set by --debug flag automatically) # Default: false # MCP_BRIDGE_DEBUG_TRANSPORT=false # ---------------------------------------------------------------------------- # TIMEZONE # ---------------------------------------------------------------------------- # IANA timezone for log timestamps and date operations # Examples: UTC, America/New_York, Europe/London, Asia/Tokyo # Default: UTC # TZ=UTC # ---------------------------------------------------------------------------- # DEVELOPMENT # ---------------------------------------------------------------------------- # Enable debug mode (also set by --debug CLI flag) # DEBUG=false # Node environment # NODE_ENV=production