Skip to content

Latest commit

 

History

History
721 lines (611 loc) · 33.5 KB

File metadata and controls

721 lines (611 loc) · 33.5 KB

Architecture

Project: Actual MCP Server
Version: 0.16.2
Last Updated: 2026-06-07


Table of Contents


System Architecture

High-Level Diagram

┌─────────────────┐         ┌──────────────────┐         ┌─────────────────┐
│                 │   MCP   │                  │  REST   │                 │
│  MCP Client     │◄────────┤  Actual MCP      │◄────────┤  Actual Budget  │
│  (LibreChat)    │         │  Server          │         │  Server         │
│                 │         │                  │         │                 │
└─────────────────┘         └──────────────────┘         └─────────────────┘
                                     │
                                     │ SQLite
                                     ▼
                            ┌─────────────────┐
                            │   Local Cache   │
                            │  (Budget Data)  │
                            └─────────────────┘

Component Layers

┌───────────────────────────────────────────────────────────┐
│              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)                     │
└───────────────────────────────────────────────────────────┘

Component Overview

Core Modules

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 Implementations

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

Tool Definitions

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

Data Flow

Request Flow

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

Tool Call Example

// 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"
    }]
  }
}

Module Structure

Project Layout

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

Execution Lifecycle

Startup Sequence

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

Shutdown Sequence

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)

Test Modes

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 responses

Configuration

Environment Variables

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

Required Variables

# Actual Budget connection
ACTUAL_SERVER_URL=http://localhost:5006
ACTUAL_PASSWORD=your_password
ACTUAL_BUDGET_SYNC_ID=your_sync_id

Server Configuration

# 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

Security

# 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

Logging

# 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

Testing

# Test flags
SKIP_BUDGET_DOWNLOAD=false               # Skip budget sync on startup
DEBUG=true                                # Enable debug logging

Configuration Schema

Validated 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(),
});

Transport Protocols

HTTP Transport (Recommended)

Type: streamable-http from @modelcontextprotocol/sdk

Endpoints:

  • POST /http - MCP requests
  • GET /health - Health check
  • GET /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.


Error Handling

Error Flow

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: ..."
  }
}

Retry Logic

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)

Concurrency Control

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

Performance & Reliability

Optimization Strategies

  1. Per-session connection pool: ActualConnectionPool holds one connection per MCP session (up to MAX_CONCURRENT_SESSIONS, default 15), with idle timeouts. While a session is active the withActualApi wrapper runs operations against the existing connection instead of re-running api.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.
  2. Local Caching: Budget data cached to SQLite (MCP_BRIDGE_DATA_DIR)
  3. Lazy Loading: Dynamic imports for faster cold starts
  4. Retry Logic: Automatic recovery from transient failures (the pool entry is dropped only on infrastructure errors, not on user/domain errors)

Monitoring

  • Health Endpoint: /health returns {"status":"ok","initialized":true}
  • Metrics Endpoint: /metrics exposes Prometheus metrics
  • Structured Logging: Winston with daily rotation

Reliability Features

  • 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

Session Liveness Ownership (#167)

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 connections map, per-session lastActivity, the idle sweep timer, and the idle threshold. httpServer owns only the transports map (one StreamableHTTP transport object per session) plus sessionInitPromises (transport-init coordination).
  • Per-request activity: on every request httpServer calls connectionPool.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. httpServer deliberately does NOT additionally gate on connectionPool.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. httpServer registers one that closes the transport and deletes its transports / sessionInitPromises entries, 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 call connectionPool.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.

Technology Stack & Dependencies

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.


Next Steps

For more details: