How the featureFlagManager in Desktop Commander MCP Initializes and Controls Experimental Features and A/B Testing

The featureFlagManager in Desktop Commander MCP is a singleton class that uses a cache-first initialization strategy with background network fetching to provide non-blocking access to remote feature flags and experiment configurations.

Desktop Commander MCP implements a robust feature flag system through the FeatureFlagManager class located in src/utils/feature-flags.ts. This singleton manages the lifecycle of experimental features—from cold-start cache loading to periodic remote refreshes—while providing synchronous APIs for runtime checks. The architecture ensures that A/B testing logic in src/utils/ab-test.ts can query flag values immediately, even before fresh network data arrives.

Architecture Overview

The flag management system follows a cache-first, refresh-after pattern. At startup, the manager reads persisted flags from feature-flags.json adjacent to the global configuration, then initiates an asynchronous fetch to update values without blocking the main process. A fresh-fetch promise mechanism allows critical paths—such as A/B test assignments—to wait for guaranteed network-fetched data when needed.

Initialization Flow

Constructor and Configuration

The manager instantiates as a module-level singleton at the bottom of src/utils/feature-flags.ts:

export const featureFlagManager = new FeatureFlagManager();

During construction, the manager establishes three key components:

  • Cache Location: Resolves feature-flags.json beside the global config directory
  • Remote URL: Uses process.env.DC_FLAG_URL or falls back to the default production endpoint (L29-L31)
  • Fresh-Fetch Promise: Creates a deferred promise (freshFetchPromise) that resolves upon the first successful network fetch (L33-L36)

The Initialize Method

The initialize() method (L42-L73) orchestrates the startup sequence:

  1. Cache Loading: loadFromCache() reads the local JSON file into this.flags and sets loadedFromCache = true
  2. Background Fetch: Invokes fetchFlags() without awaiting, allowing the process to continue immediately
  3. Periodic Refresh: Starts a setInterval (default every 5 minutes based on cacheMaxAge) to re-fetch flags, using .unref() to prevent the interval from keeping the process alive (L61-L66)

Remote Fetching and Caching Strategy

Cache-First Loading

The manager prioritizes immediate availability over freshness. During initialize(), it first hydrates the internal state from disk, ensuring that get() calls return values even when offline. This cached data remains available while the background network request proceeds.

Background Fetch with AbortController

The fetchFlags() implementation (L65-L98) uses defensive networking patterns:

  • Dual Timeout Protection: Combines AbortController with a secondary hardTimeout to guarantee the request cannot hang indefinitely
  • Atomic Updates: On success, updates this.flags, timestamps, and persists to cache via saveToCache() in a single flow
  • Promise Resolution: Calls resolveFreshFetch() to settle the fresh-fetch promise, unblocking any waiting consumers

Periodic Refresh Mechanism

The initialization routine establishes an interval timer that re-runs fetchFlags() every 5 minutes. This keeps experiment configurations current without requiring manual restarts, while the .unref() method ensures the timer doesn't interfere with graceful process shutdowns.

Accessing Flags and Waiting for Fresh Data

Synchronous Flag Accessors

The manager exposes three primary methods for reading state:

  • get(flagName, defaultValue): Returns this.flags[flagName] or the provided default
  • getAll(): Returns a shallow copy of the entire flags object for debugging
  • wasLoadedFromCache(): Boolean indicating whether current values originated from local storage versus a fresh network fetch

The Fresh-Fetch Promise Pattern

For operations requiring guaranteed fresh data—such as A/B test bucketing—the waitForFreshFlags() method (L20-L34) races the fresh-fetch promise against a 5-second safety timeout:

await featureFlagManager.waitForFreshFlags();
// Now safe to make decisions based on latest remote configuration

This pattern prevents indefinite blocking while ensuring experiments use network-fetched weights.

A/B Testing Integration

Experiment Definition and Variant Assignment

The src/utils/ab-test.ts module consumes flags through the manager to drive experiments:

  • Experiment Configuration: Expects an experiments object in the remote JSON containing weighted variant definitions
  • Deterministic Assignment: getVariant() uses a hash of clientId + experimentName to deterministically select variants, ensuring consistent user experiences across sessions (L48-L96)
  • Feature Detection: hasFeature(featureName) iterates active experiments, fetches the user's assigned variant, and returns true only when the variant matches the requested feature (L4-L22)

Consumer Usage Patterns

Applications typically follow this sequence when checking experimental features:

import { featureFlagManager } from './utils/feature-flags.js';
import { hasFeature, getABTestVariant } from './utils/ab-test.js';

// At startup
await featureFlagManager.initialize();

// For critical experiment gates
await featureFlagManager.waitForFreshFlags();
if (await hasFeature('showOnboardingPage')) {
  // Show experimental onboarding UI
}

// For analytics or assignment tracking
const variant = await getABTestVariant('OnboardingPreTool');
console.log(`User assigned to variant: ${variant}`);

Summary

  • Singleton Pattern: The featureFlagManager exports a single instance from src/utils/feature-flags.ts that coordinates all flag operations
  • Cache-First Initialization: initialize() loads local cache immediately, then fetches remotely without blocking startup
  • Non-Blocking Refresh: A 5-minute interval with AbortController timeouts keeps flags current while allowing clean process exits
  • Fresh-Fetch Coordination: waitForFreshFlags() provides a gate for A/B tests that require network-guaranteed configuration
  • Deterministic A/B Tests: The ab-test.ts utilities use consistent hashing against the manager's experiments flag to assign variants

Frequently Asked Questions

How does featureFlagManager handle offline startups?

The manager reads feature-flags.json from disk during initialize() before attempting any network requests. This cache-first approach ensures that get() calls return the last known values immediately, even without connectivity. The background fetch proceeds asynchronously, updating values only when the network becomes available.

What happens if the remote flag server is slow or unresponsive?

The fetchFlags() method implements dual timeout protection using both AbortController and a secondary hardTimeout. If the fetch hangs, the abort signal triggers after a configured duration, preventing the promise from blocking indefinitely. The manager retains cached values and retries automatically during the next 5-minute refresh interval.

How do A/B tests ensure consistent variant assignment across sessions?

The getVariant() function in src/utils/ab-test.ts uses deterministic hashing based on a stable clientId concatenated with the experiment name. This hash maps to weighted buckets defined in the remote experiments flag. Because the algorithm is deterministic and the weights come from the cached configuration, users receive the same variant assignment every time they launch the application, even offline.

Can consumers force a fresh flag check before critical decisions?

Yes. While standard flag checks use get() for immediate synchronous access, consumers can await featureFlagManager.waitForFreshFlags() before making important experiment assignments. This method races the internal fresh-fetch promise against a 5-second timeout, ensuring that code only proceeds after verifying that network-fetched flags are available, or timing out gracefully if the remote source fails.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →