Project: Actual MCP Server
Version: 0.16.2
Last Updated: 2026-06-07
- System Architecture
- Component Overview
- Data Flow
- Module Structure
- Execution Lifecycle
- Configuration
- Transport Protocols
- Error Handling
- Performance & Reliability
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ │ MCP │ │ REST │ │
│ MCP Client │◄────────┤ Actual MCP │◄────────┤ Actual Budget │
│ (LibreChat) │ │ Server │ │ Server │
│ │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
│ SQLite
▼
┌─────────────────┐
│ Local Cache │
│ (Budget Data) │
└─────────────────┘
┌───────────────────────────────────────────────────────────┐
│ Client Layer (LibreChat, etc.) │
└───────────────────────────────────────────────────────────┘
│
HTTP / WebSocket
▼
┌───────────────────────────────────────────────────────────┐
│ Transport Layer │
│ ┌──────────┐ │
│ │ HTTP │ │
│ │ Server │ │
│ └──────────┘ │
└───────────────────────────────────────────────────────────┘
│
MCP Protocol
▼
┌───────────────────────────────────────────────────────────┐
│ MCP Protocol Layer │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ActualMCPConnection │ │
│ │ (Request routing, response handling) │ │
│ └────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
│
Tool Calls
▼
┌───────────────────────────────────────────────────────────┐
│ Business Logic Layer │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ActualToolsManager │ │
│ │ (Tool registry, validation, dispatch) │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Tool1│ │Tool2│ │Tool3│ │ ... │ │ 70 │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
└───────────────────────────────────────────────────────────┘
│
Adapter Functions
▼
┌───────────────────────────────────────────────────────────┐
│ Data Access Layer │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Actual Adapter │ │
│ │ (Retry logic, concurrency control, error mapping)│ │
│ └────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
│
@actual-app/api
▼
┌───────────────────────────────────────────────────────────┐
│ External API Layer │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Actual Budget API Client │ │
│ │ (Official @actual-app/api package) │ │
│ └────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
│
REST API / SQLite
▼
┌───────────────────────────────────────────────────────────┐
│ Actual Budget Server │
│ (External Service) │
└───────────────────────────────────────────────────────────┘
| Module | File | Responsibility | Key Functions |
|---|---|---|---|
| Main Entry | src/index.ts |
Orchestration, CLI parsing, server startup | main() |
| Connection Manager | src/actualConnection.ts |
Actual Budget API lifecycle | connectToActual(), shutdownActual() |
| Tool Manager | src/actualToolsManager.ts |
Tool registry and dispatch | registerTools(), callTool() |
| MCP Connection | src/lib/ActualMCPConnection.ts |
MCP protocol implementation | handleToolCall(), handleRequest() |
| Adapter Layer | src/lib/actual-adapter.ts (+ leaf modules in src/lib/actual-adapter/) |
withActualApi wrapper: retry, concurrency, pooled vs legacy execution (#134, split in #166) |
All Actual API functions |
| Connection Pool | src/lib/ActualConnectionPool.ts |
Per-session connections (up to MAX_CONCURRENT_SESSIONS, default 15), idle timeouts |
getConnection(), touch(), shutdownConnection() |
| Request Context | src/lib/requestContext.ts |
AsyncLocalStorage carrying the active session id (and principal) across async boundaries |
requestContext.run() |
| Singleton State | src/lib/apiState.ts |
Shared flag for the @actual-app/api singleton "live" state, probed by the adapter to decide pool reuse |
isApiInitialized() |
| Configuration | src/config.ts |
Environment validation | config, configSchema |
| Logger | src/logger.ts |
Structured logging | logger singleton |
| Observability | src/observability.ts |
Metrics collection | incrementToolCall(), getMetricsText() |
| Transport | File | Status | Authentication | LibreChat Support |
|---|---|---|---|---|
| HTTP | src/server/httpServer.ts |
✅ Production | Bearer token OR OIDC/JWT | ✅ Fully supported |
| WebSocket | (removed) | ❌ Removed | N/A | ❌ Not supported |
| SSE | (removed) | ❌ Removed | N/A | N/A |
74 tools organized by category:
src/tools/
├── server_info.ts # Server version / connection info
├── server_get_version.ts # Actual Budget server version
├── get_id_by_name.ts # Exact name → UUID lookup (accounts/categories/payees/schedules)
├── entities_search.ts # Pattern/fuzzy name search for accounts/categories/payees (#204)
├── session_list.ts # Session management (2 tools)
├── session_close.ts
├── accounts_create.ts # Accounts (7 tools)
├── accounts_list.ts
├── accounts_update.ts
├── accounts_delete.ts
├── accounts_close.ts
├── accounts_reopen.ts
├── accounts_get_balance.ts
├── transactions_create.ts # Transactions (13 tools)
├── transactions_get.ts
├── transactions_update.ts
├── transactions_delete.ts
├── transactions_import.ts
├── transactions_filter.ts
├── transactions_search_by_amount.ts
├── transactions_search_by_category.ts
├── transactions_search_by_month.ts
├── transactions_search_by_payee.ts
├── transactions_summary_by_category.ts
├── transactions_summary_by_payee.ts
├── transfers_create.ts # Transfers (1 tool)
├── budgets_list_available.ts # Budgets (13 tools)
├── budgets_switch.ts
├── budgets_getMonth.ts
├── budgets_getMonths.ts
├── budgets_get_all.ts
├── budgets_setAmount.ts
├── budgets_transfer.ts
├── budgets_setCarryover.ts
├── budgets_holdForNextMonth.ts
├── budgets_resetHold.ts
├── budgets_export.ts # #332: writes a zip to ACTUAL_EXPORT_DIR
├── budgets_import.ts # #334: LOADS the imported budget (active budget changes)
├── budget_updates_batch.ts
├── preferences_get.ts # Preferences (1 tool, #333)
├── categories_get.ts # Categories (4 tools)
├── categories_create.ts
├── categories_update.ts
├── categories_delete.ts
├── category_groups_get.ts # Category Groups (4 tools)
├── category_groups_create.ts
├── category_groups_update.ts
├── category_groups_delete.ts
├── payees_get.ts # Payees (6 tools)
├── payees_common_list.ts
├── payees_create.ts
├── payees_update.ts
├── payees_delete.ts
├── payees_merge.ts
├── payee_rules_get.ts # Payee Rules (1 tool)
├── rules_get.ts # Rules (4 tools)
├── rules_create.ts
├── rules_update.ts
├── rules_delete.ts
├── tags_list.ts # Tags (4 tools)
├── tags_create.ts
├── tags_update.ts
├── tags_delete.ts
├── notes_get.ts # Notes (2 tools)
├── notes_update.ts
├── query_run.ts # Advanced ActualQL queries
├── bank_sync.ts # Bank synchronization
└── index.ts # Tool exports
1. Client sends MCP request
│
├──> HTTP POST /http
│
2. Transport layer receives request
│
└──> Parses JSON-RPC 2.0 format
│
3. ActualMCPConnection routes request
│
├──> tools/list → Returns available tools
├──> tools/call → Dispatches to ActualToolsManager
└──> Other MCP methods
│
4. ActualToolsManager validates and calls tool
│
├──> Validates tool name exists
├──> Validates input schema (Zod)
└──> Calls tool implementation function
│
5. Tool calls Actual Adapter function
│
└──> Adapter applies retry logic & concurrency control
│
6. @actual-app/api makes REST call
│
└──> Actual Budget Server processes request
│
7. Response flows back up the stack
│
└──> JSON result or error returned to client
// Client request (MCP format)
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "actual_transactions_create",
"arguments": {
"accountId": "uuid-123",
"date": "2025-11-11",
"amount": -5000,
"payee": "Amazon"
}
}
}
// Server processing
ActualMCPConnection.handleToolCall()
└─> ActualToolsManager.callTool("actual_transactions_create", args)
└─> transactionsCreate(args) in src/tools/transactions_create.ts
└─> actualAdapter.addTransaction(args) in src/lib/actual-adapter.ts
└─> api.addTransaction(args) from @actual-app/api
└─> REST POST to Actual Budget Server
// Server response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{
"type": "text",
"text": "Transaction ID: uuid-456"
}]
}
}actual-mcp-server/
├── src/ # TypeScript source code
│ ├── index.ts # Main entry point
│ ├── config.ts # Environment validation (Zod)
│ ├── logger.ts # Winston logger singleton
│ ├── observability.ts # Prometheus metrics
│ ├── actualConnection.ts # Actual API connection manager
│ ├── actualToolsManager.ts # Tool registry singleton
│ ├── utils.ts # Utility functions
│ ├── tests_adapter_runner.ts # Adapter test executor
│ │
│ ├── lib/ # Core libraries
│ │ ├── actual-adapter.ts # withActualApi wrapper (retry, concurrency, pool cooperation)
│ │ ├── actual-adapter/ # Leaf modules: normalize, query, auth-retry, concurrency (#166)
│ │ ├── actual-schema.ts # Actual DB schema for SQL validation
│ │ ├── ActualConnectionPool.ts # Per-session connection pool (default 15, idle timeouts)
│ │ ├── ActualMCPConnection.ts # MCP protocol handler (EventEmitter-based)
│ │ ├── apiState.ts # Shared @actual-app/api singleton live-state flag
│ │ ├── requestContext.ts # AsyncLocalStorage carrying sessionId / principal
│ │ ├── budget-registry.ts # Parses BUDGET_N_* env vars into budget configs
│ │ ├── budget-preference-store.ts # Per-principal preferred-budget store (#189)
│ │ ├── constants.ts # Configuration constants and limits
│ │ ├── errors.ts # notFoundMsg / constraintErrorMsg helpers
│ │ ├── loggerFactory.ts # Module-scoped logger factory
│ │ ├── query-validator.ts # ActualQL query validation
│ │ ├── retry.ts # Exponential backoff retry logic
│ │ ├── toolFactory.ts # createTool() factory helper
│ │ └── schemas/ # Per-domain Zod schemas (CommonSchemas)
│ │
│ ├── server/ # Transport implementations
│ │ ├── httpServer.ts # HTTP transport (Express, StreamableHTTP, auth)
│ │ └── stdioServer.ts # stdio transport (Claude Desktop/Code)
│ │
│ ├── auth/ # Authentication
│ │ ├── setup.ts # OIDC/JWKS factory (AUTH_PROVIDER=oidc)
│ │ └── budget-acl.ts # Per-user budget ACL (email/sub/group)
│ │
│ ├── tools/ # MCP tool definitions (74 tools + index.ts)
│ │ ├── server_info.ts # Server info (1 tool)
│ │ ├── session_*.ts # Session management (2 tools)
│ │ ├── accounts_*.ts # Accounts (7 tools)
│ │ ├── transactions_*.ts # Transactions (13 tools, incl. search/summary)
│ │ ├── budgets_*.ts # Budgets (11 tools)
│ │ ├── budget_updates_batch.ts # Batch budget operations
│ │ ├── categories_*.ts # Categories (4 tools)
│ │ ├── category_groups_*.ts # Category groups (4 tools)
│ │ ├── payees_*.ts # Payees (6 tools)
│ │ ├── payee_rules_get.ts # Payee rules (1 tool)
│ │ ├── rules_*.ts # Rules (4 tools)
│ │ ├── tags_*.ts # Tags (4 tools)
│ │ ├── notes_*.ts # Notes (2 tools)
│ │ ├── query_run.ts # Advanced ActualQL queries
│ │ ├── bank_sync.ts # Bank synchronization
│ │ └── index.ts # Tool exports
│ │
│ ├── types/ # TypeScript type definitions
│ │ └── tool.d.ts # MCP tool types
│ │
│ ├── prompts/ # MCP prompt templates
│ │ └── showLargeTransactions.ts
│ │
│ └── resources/ # MCP resources
│ └── accountsSummary.ts
│
├── tests/ # Tests
│ ├── e2e/ # End-to-end tests (Playwright)
│ │ ├── mcp-client.playwright.spec.ts # Protocol compliance tests
│ │ ├── docker.e2e.spec.ts # Docker smoke tests
│ │ ├── docker-all-tools.e2e.spec.ts # All-tools Docker E2E (~80 named tests, all 74 tools)
│ │ ├── run-docker-e2e.sh # Docker test orchestrator
│ │ └── (#366: the suites/ tree was removed. It never executed, and every doc that
│ │ named it now points at docker-all-tools.e2e.spec.ts instead)
│ ├── unit/ # Unit tests (offline, stub adapter)
│ │ ├── transactions_create.test.js
│ │ ├── generated_tools.smoke.test.js
│ │ └── schema_validation.test.js
│ ├── shared/ # Shared test utilities
│ │ ├── e2e-helpers.ts # TS helpers for E2E: waitForMCPHealth, retryRequest, callTool, extractResult
│ │ └── mcp-protocol.js # JS mirror of extractResult (used by manual integration suite)
│ └── manual/ # Live integration tests (real Actual Budget)
│
├── scripts/ # Build and utility scripts (see scripts/README.md)
│ ├── verify-tools.js # Tool coverage verification
│ ├── bootstrap-and-init.sh # Docker: bootstrap Actual server + import test budget
│ ├── import-test-budget.sh # Upload test-data/*.zip to Actual server
│ ├── register-tsconfig-paths.js # Path alias resolver for dist/ runtime
│ ├── list-actual-api-methods.mjs # API method coverage checker
│ └── version-bump.js / version-check.js / version-dev.js # Versioning
│
├── docs/ # Documentation (this folder)
├── generated/ # Generated TypeScript types
├── actual-data/ # Budget data cache (gitignored)
├── logs/ # Application logs (gitignored)
│
├── Dockerfile # Production container
├── docker-compose.yaml # Docker Compose (dev/production profiles)
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── .env.example # Environment variable template
1. CLI Argument Parsing
└─┾ src/index.ts parses --help, --debug, --http
└─> --help exits early (before loading environment)
2. Environment Loading
└─> dotenv loads .env file
└─> src/config.ts validates with Zod schema
└─> Exits with error if validation fails
3. Dynamic Imports
└─> Lazy load all dependencies (winston, @actual-app/api, etc.)
└─> Improves cold start performance
4. Actual Budget Connection
└─> src/actualConnection.ts::connectToActual()
└─> api.init({ dataDir, serverURL, password })
└─> api.downloadBudget(syncId, { password })
└─> Budget data cached to MCP_BRIDGE_DATA_DIR
5. Tool Registry Initialization
└─> src/actualToolsManager.ts loads all tools
└─> Validates tool schemas
└─> Registers 74 tools with MCP capabilities
6. MCP Connection Setup
└─> Create ActualMCPConnection instance
└─> Build capabilities object (tools, resources, prompts)
7. Transport Server Startup
└─┾ Start HTTP server
└─> Bind to MCP_BRIDGE_PORT
└─> Register health endpoints
8. Ready State
└─> Log current server version and transport mode
└─> Accept MCP requests
1. SIGINT / SIGTERM received
│
2. Graceful shutdown initiated
├─┾ Close transport server (HTTP)
├─> Stop accepting new requests
├─> Wait for pending requests (timeout: 10s)
│
3. Actual Budget disconnection
└─> src/actualConnection.ts::shutdownActual()
└─> api.shutdown() - closes DB connections
│
4. Logger flush
└─> Winston flushes remaining log entries
│
5. Process exit
└─> Exit code 0 (clean shutdown)
The server supports special test modes:
# Test Actual Budget connection only
npm run dev -- --test-actual-connection
└─> Connects, downloads budget, disconnects, exits
# Test MCP client interaction
npm run dev -- --http --test-mcp-client
└─> Starts server, sends test requests, verifies responsesAll configuration via environment variables. The canonical inventory of every
variable (type, default, source, read site) is docs/CONFIGURATION.md;
.env.example carries the same set with inline comments. A variable is canonical if it
is a Zod schema key in src/config.ts OR an entry in RAW_ENV_ALLOWLIST in
src/lib/config-registry.ts. The drift guard scripts/config-drift.mjs (run in CI via
tests/unit/config_drift.test.js) fails the build if the schema/allowlist,
.env.example, and the README env table disagree.
# Actual Budget connection
ACTUAL_SERVER_URL=http://localhost:5006
ACTUAL_PASSWORD=your_password
ACTUAL_BUDGET_SYNC_ID=your_sync_id# Server settings
MCP_BRIDGE_PORT=3600 # Server port
MCP_BRIDGE_DATA_DIR=./actual-data # Budget cache directory
MCP_BRIDGE_BIND_HOST=0.0.0.0 # Bind address
# Transport mode (Docker only)
MCP_TRANSPORT_MODE=--http # Only --http is supported# Authentication mode (default: none = static Bearer)
AUTH_PROVIDER=none # 'none' or 'oidc'
# Static Bearer token (AUTH_PROVIDER=none)
MCP_SSE_AUTHORIZATION=your_bearer_token # Bearer token (optional)
# OIDC / JWT authentication (AUTH_PROVIDER=oidc)
OIDC_ISSUER=https://sso.yourdomain.com # OIDC issuer URL
OIDC_RESOURCE=your-client-id # Expected 'aud' claim
OIDC_ACCEPTED_AUDIENCES= # Extra accepted 'aud' values (#245, optional)
OIDC_JWKS_TRUSTED_HOSTS= # Cross-host JWKS allowlist, e.g. Google (#254, optional)
OIDC_ALLOW_INSECURE_ISSUER=false # #244 opt-out for a trusted-LAN http issuer
OIDC_SCOPES= # Required scopes (empty = none)
AUTH_BUDGET_ACL=user@example.com:sync-id # Per-user budget ACL (optional)
# HTTPS
MCP_ENABLE_HTTPS=true # Enable TLS
MCP_HTTPS_CERT=/app/certs/cert.pem # Certificate path
MCP_HTTPS_KEY=/app/certs/key.pem # Private key path# Log configuration
MCP_BRIDGE_STORE_LOGS=false # Write to disk
MCP_BRIDGE_LOG_DIR=./logs # Log directory
MCP_BRIDGE_LOG_LEVEL=info # error, warn, info, debug# Test flags
SKIP_BUDGET_DOWNLOAD=false # Skip budget sync on startup
DEBUG=true # Enable debug loggingValidated by a Zod schema in src/config.ts. The snippet below is illustrative, not exhaustive: .env.example and src/config.ts are authoritative. Notably, multi-budget mode adds BUDGET_N_NAME, BUDGET_N_SYNC_ID, BUDGET_N_SERVER_URL, BUDGET_N_PASSWORD, and BUDGET_N_ENCRYPTION_PASSWORD (N = 1, 2, 3...), and concurrency is tunable via ACTUAL_API_CONCURRENCY.
export const configSchema = z.object({
ACTUAL_SERVER_URL: z.string().url(),
ACTUAL_PASSWORD: z.string().min(1),
ACTUAL_BUDGET_SYNC_ID: z.string().min(1),
MCP_BRIDGE_DATA_DIR: z.string().default('./actual-data'),
MCP_BRIDGE_PORT: z.string().default('3600'),
MCP_TRANSPORT_MODE: z.enum(['--http']).default('--http'),
MCP_SSE_AUTHORIZATION: z.string().optional(),
AUTH_PROVIDER: z.enum(['none', 'oidc']).default('none'),
OIDC_ISSUER: z.string().optional(),
OIDC_RESOURCE: z.string().optional(),
OIDC_SCOPES: z.string().optional(),
AUTH_BUDGET_ACL: z.string().optional(),
MCP_ENABLE_HTTPS: z.string().optional().transform(val => val === 'true'),
MCP_HTTPS_CERT: z.string().optional(),
MCP_HTTPS_KEY: z.string().optional(),
});Type: streamable-http from @modelcontextprotocol/sdk
Endpoints:
POST /http- MCP requestsGET /health- Health checkGET /metrics- Prometheus metrics
Authentication: Bearer token OR OIDC/JWT in Authorization header
LibreChat Status: ✅ Fully supported and verified
Configuration:
# Start HTTP server
npm run dev -- --http
# Docker (default)
docker run -e MCP_TRANSPORT_MODE=--http ...LibreChat Config:
mcpServers:
actual-mcp:
type: "streamable-http"
url: "https://your-server:3600/http"
headers:
Authorization: "Bearer your_token"
serverInstructions: true
⚠️ Removed: WebSocket transport (wsServer.ts) has been removed. Use HTTP transport instead.
⚠️ Removed: SSE transport (sseServer.ts) has been removed. Use HTTP transport instead.
Tool Error
└─> Caught by tool implementation
└─> Adapter layer retry logic (3 attempts)
└─> If all retries fail → Error response
└─> ActualMCPConnection formats error
└─> Transport sends JSON-RPC error
Error Response Format:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32000,
"message": "Tool execution failed: ..."
}
}
Implemented in src/lib/actual-adapter.ts:
// Exponential backoff retry (src/lib/constants.ts)
DEFAULT_RETRY_ATTEMPTS = 3
DEFAULT_RETRY_BACKOFF_MS = 200 // 200ms base delay
MAX_RETRY_DELAY_MS = 10000 // cap at 10s
// backoff sequence: 200ms → 400ms → 800ms (capped at 10s)// Prevent overwhelming Actual Budget server (src/lib/constants.ts)
DEFAULT_CONCURRENCY_LIMIT = 5 // max simultaneous API calls
// Overflow requests are queued and drained FIFO
// Configurable via ACTUAL_API_CONCURRENCY env var- Per-session connection pool:
ActualConnectionPoolholds one connection per MCP session (up toMAX_CONCURRENT_SESSIONS, default 15), with idle timeouts. While a session is active thewithActualApiwrapper runs operations against the existing connection instead of re-runningapi.init()/api.shutdown()per call (pooled mode, #134), which removed the per-op upstream-login burst. Non-MCP callers fall back to the legacy init-to-shutdown cycle. - Local Caching: Budget data cached to SQLite (MCP_BRIDGE_DATA_DIR)
- Lazy Loading: Dynamic imports for faster cold starts
- Retry Logic: Automatic recovery from transient failures (the pool entry is dropped only on infrastructure errors, not on user/domain errors)
- Health Endpoint:
/healthreturns{"status":"ok","initialized":true} - Metrics Endpoint:
/metricsexposes Prometheus metrics - Structured Logging: Winston with daily rotation
- Graceful Shutdown: SIGTERM/SIGINT handlers
- Error Boundaries: All tool calls wrapped in try/catch
- Input Validation: Zod schemas for all tool inputs
- Type Safety: Full TypeScript with strict mode
ActualConnectionPool is the single source of truth for session liveness and idle timing. There is one idle timer and one SESSION_IDLE_TIMEOUT_MINUTES value for the whole process, consumed only by the pool.
- Ownership split: the pool owns the
connectionsmap, per-sessionlastActivity, the idle sweep timer, and the idle threshold.httpServerowns only thetransportsmap (one StreamableHTTP transport object per session) plussessionInitPromises(transport-init coordination). - Per-request activity: on every request
httpServercallsconnectionPool.touch(sessionId)so the pool's idle clock reflects real usage. Liveness for serving is decided purely by transport presence: the eviction listener (below) removes the transport the moment a session is genuinely evicted, so a missing transport means expired.httpServerdeliberately does NOT additionally gate onconnectionPool.isLive(), because a pool entry can be legitimately absent while the MCP session is still usable (after a transient infra error the adapter drops the pool entry without evicting the transport, and the next call re-establishes it through the legacy fallback).isLive()remains a pool query method for diagnostics. - Eager teardown via eviction callback: when the pool removes a session (idle sweep or explicit
session_close/ server shutdown) it fires a registered eviction listener.httpServerregisters one that closes the transport and deletes itstransports/sessionInitPromisesentries, so both tables change in the same window. The mechanism is callback-based eager teardown, not lazy query-on-demand: a lazily-cleaned table would leak transport objects for sessions a client abandons without reconnecting. - Paths that do NOT evict:
switchBudget's slow path and infra-error drops callconnectionPool.shutdownConnection(sessionId)without{ evict: true }, so the MCP session's transport survives a budget switch or a transient network error while the pool entry is recycled. - Behavior change: the effective idle window is now exactly the single configured value, not
min(httpServer, pool). Before this consolidation the httpServer table defaulted to 2 minutes and the pool to 5, so the two clocks could disagree and a session could be alive in one table and dead in the other.
The dependency list lives in package.json (the source of truth) and is summarised in CLAUDE.md's "Tech Stack" line, so it is not duplicated here. A hand-maintained copy drifts: run npm ls --depth=0 for the installed tree and npm audit for the current security posture.
Highlights: TypeScript (NodeNext/ESM) on Node 22+, @actual-app/api v26, @modelcontextprotocol/sdk, Express 5, Zod v4, Winston for logging, and Playwright for E2E. Dependency updates are automated (Dependabot weekly plus the scheduled Dependency Update workflow): patches auto-merge after CI, minor and major bumps get review.
For more details:
- Testing & Reliability - Testing strategy
- Security & Privacy - Security policies
- Planned and future work: tracked as GitHub issues