DesktopCommanderMCP Feature Flag Architecture: How A/B Tests Are Implemented

DesktopCommanderMCP uses a centralized FeatureFlagManager singleton that fetches remote JSON configurations, caches them locally to feature-flags.json, and exposes a v2 schema where weighted variant distributions in the experiments field drive deterministic A/B test assignments.

The DesktopCommanderMCP repository implements a production-grade feature flag system to enable gradual rollouts and controlled experimentation without redeploying the application. At its core, the architecture separates flag management into a singleton manager handling remote synchronization while maintaining offline capability through local JSON caching. Understanding how the system processes both simple boolean toggles and complex experiment assignments requires examining the core components in src/utils/feature-flags.ts and the A/B testing logic in src/utils/ab-test.ts.

Core Architecture Components

FeatureFlagManager Singleton

Located in src/utils/feature-flags.ts, the FeatureFlagManager class serves as the central authority for all feature flag operations. It maintains an internal cache of flag values loaded from both local storage (feature-flags.json in the config directory) and remote endpoints defined by the flagUrl parameter. The manager exposes critical methods including initialize() for non-blocking setup, get(name, default?) for value retrieval, and waitForFreshFlags() to block execution until the first successful network synchronization completes.

Key implementation details include:

  • Local persistence: Remote payloads are written to feature-flags.json to ensure offline availability
  • Cache tracking: The boolean loadedFromCache property distinguishes between stale disk data and fresh network payloads
  • Background refresh: A setInterval (default 5 minutes) periodically calls fetchFlags(), with unref() applied to prevent the interval from blocking clean process exit

A/B Test Helper Module

The src/utils/ab-test.ts module interprets the experiments flag structure to determine user variant assignments. It exports three primary functions: getExperiments() retrieves the experiments map from the feature flag manager, getVariant(experimentName) returns the assigned variant string (defaulting to 'control'), and hasFeature(featureName) checks if the current user qualifies for a specific experimental feature by parsing the feature name to extract the experiment context.

UI-A/B Bridge

For interface components requiring guaranteed fresh data, src/utils/mcp-ui-ab-test.ts re-exports wasLoadedFromCache() and waitForFreshFlags() from the main manager. This allows onboarding flows and critical UI paths to await network confirmation before rendering experiment-specific content, preventing flickering between control and variant experiences.

Initialization and Data Flow

Startup Sequence

The initialization sequence begins in src/index.ts where the application creates the singleton featureFlagManager instance and invokes initialize(). This method operates non-blocking: it immediately loads cached values from disk via loadFromCache() while simultaneously initiating a background network request to the configured flagUrl via fetchFlags().

When the remote fetch succeeds, the internal freshFetchPromise resolves, unblocking any callers awaiting waitForFreshFlags(). This dual-phase approach ensures the application remains responsive during startup while guaranteeing eventual consistency with remote configuration.

Remote Flag Schema (v2)

The system expects a JSON payload conforming to the v2 schema:

{
  "version": "2",
  "flags": {
    "welcome_page_enabled": true,
    "experiments": {
      "new_ui": {
        "weights": { "control": 50, "variantA": 50 },
        "assignment": "variantA"
      }
    }
  }
}

The flags object contains simple boolean or string values, while the experiments object defines A/B tests through weighted distributions and pre-calculated assignments. The client respects the assignment field as the source of truth, while weights documents the intended distribution logic used by the server to calculate that assignment.

A/B Test Implementation Details

Variant Assignment Logic

A/B tests rely entirely on the experiments flag structure. When hasFeature('new_ui_variantA') is called, the helper extracts the experiment name (new_ui) from the feature string, retrieves the user's assigned variant from the flag payload, and returns true only if the assignment matches the requested feature variant.

This design centralizes assignment logic on the server side, allowing the client to remain a simple stateless consumer. The deterministic assignment ensures users see consistent experiences across sessions without requiring client-side randomization or user ID hashing.

Synchronization Guarantees

UI components that must avoid flickering or incorrect experiment rendering call await featureFlagManager.waitForFreshFlags() before checking experiment status. This ensures the decision reflects the latest remote configuration rather than potentially stale cached data from previous sessions. The wasLoadedFromCache() method allows components to display loading spinners when fresh data is pending.

Practical Implementation Examples

Reading Simple Feature Flags

To check a standard boolean flag without waiting for network synchronization:

import { featureFlagManager } from './utils/feature-flags.js';

const welcomeEnabled = featureFlagManager.get('welcome_page_enabled', true);
if (welcomeEnabled) {
  // Render welcome page
}

Source: [src/utils/welcome-onboarding.ts](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/welcome-onboarding.ts)

Executing A/B Test Branches

For experiment-dependent rendering, use the hasFeature helper which automatically awaits fresh flags:

import { hasFeature } from './utils/ab-test.js';

async function renderDashboard() {
  if (await hasFeature('new_ui_variantA')) {
    renderVariantA();
  } else {
    renderControl();
  }
}

Source: [src/utils/ab-test.ts](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/ab-test.ts)

Blocking for Fresh Configuration

Critical paths like onboarding should await fresh flags before proceeding:

import { featureFlagManager } from './utils/feature-flags.js';

async function initOnboarding() {
  await featureFlagManager.waitForFreshFlags();
  const onboardingEnabled = featureFlagManager.get('onboarding_injection', false);
  if (onboardingEnabled) {
    startOnboardingFlow();
  }
}

Source: [src/utils/mcp-ui-ab-test.ts](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/mcp-ui-ab-test.ts)

Summary

  • DesktopCommanderMCP implements a singleton FeatureFlagManager in src/utils/feature-flags.ts that synchronizes remote JSON configurations with local caching to feature-flags.json.
  • The v2 schema separates simple flags from A/B test definitions under the experiments field, where each experiment specifies weighted variants and deterministic user assignments.
  • A/B tests use the hasFeature() helper in src/utils/ab-test.ts to match feature names against pre-calculated variant assignments, ensuring consistent behavior per user.
  • The initialization flow loads cached values immediately while fetching fresh data in the background, with waitForFreshFlags() providing a synchronization point for critical UI components.
  • Background refresh occurs every 5 minutes via an unref()-ed interval, allowing clean process termination while maintaining flag freshness.

Frequently Asked Questions

How does DesktopCommanderMCP handle offline scenarios when feature flags cannot be fetched?

The FeatureFlagManager persists the last successful remote payload to feature-flags.json in the configuration directory. During initialization, it loads these cached values immediately so the application functions offline. The wasLoadedFromCache() method indicates whether current values came from disk rather than the network, allowing UI to display appropriate loading states or fallback behaviors while awaiting fresh data.

What determines which variant a user sees in an A/B test?

Variant assignment is server-side deterministic. The remote JSON payload includes an assignment field within each experiment object (e.g., "assignment": "variantA"), which the client reads but does not calculate. While the weights field documents the intended distribution logic used by the server, the client simply respects the pre-computed assignment, ensuring consistent experiences across sessions and devices for the same user.

Can feature flags be updated without restarting the application?

Yes. The system implements hot reloading through a background refresh mechanism. The FeatureFlagManager sets a 5-minute interval that re-fetches the remote JSON and updates the in-memory cache. New flag values take effect immediately for subsequent get() calls without requiring application restart. For immediate updates, call await featureFlagManager.refresh() to force a synchronous network fetch.

How do UI components prevent showing the wrong A/B test variant during initial load?

Components call await featureFlagManager.waitForFreshFlags() before rendering experiment-dependent content. This blocks execution until the first successful network fetch completes, ensuring the component uses the latest experiment assignments rather than potentially stale cached values. The wasLoadedFromCache() check allows components to show loading states when flags haven't been refreshed from the network yet.

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 →