How DesktopCommander MCP's Feature Flag System Enables Dynamic Behavior Changes

DesktopCommander MCP implements a centralized feature‑flag manager that loads JSON payloads from remote endpoints, caches them locally, and exposes a synchronous API for runtime feature control without requiring redeployment.

This feature flag system powers remote kill‑switches, A/B testing, and gradual rollouts across the codebase. The implementation in wonderwhy-er/DesktopCommanderMCP keeps core application logic clean while allowing product teams to modify behavior instantly via configuration changes.


Feature Flag Manager Architecture

The system centers on FeatureFlagManager defined in src/utils/feature-flags.ts. This singleton handles five responsibilities: remote fetching, local caching, in‑memory storage, synchronous reads, and async initialization.

Core API Surface

The manager exposes these methods as shown in the source:

  • get(key, defaultValue?) – Synchronous flag lookup
  • getAll() – Returns entire flag payload
  • wasLoadedFromCache() – Indicates if current data came from disk
  • waitForFreshFlags() – Promise that resolves when network fetch completes
  • initialize() – Async bootstrap that starts fetch without blocking
// src/utils/feature-flags.ts (simplified structure)
export class FeatureFlagManager {
  private cachePath: string;
  private flags: Record<string, any> = {};
  private loadedFromCache = false;

  constructor() {
    // Cache location: <config-dir>/feature-flags.json
    this.cachePath = path.join(getConfigDir(), 'feature-flags.json');
  }

  async initialize(): Promise<void> {
    // Attempt cache read first, then fetch remote
  }

  get(key: string, defaultValue?: any): any {
    return this.flags[key] ?? defaultValue;
  }

  async waitForFreshFlags(): Promise<void> {
    // Resolves when network request finishes
  }
}

export const featureFlagManager = new FeatureFlagManager();

Initialization Flow at Startup

Application bootstrap occurs in src/index.ts lines 65‑67. The main entry point calls initialize() non‑blocking so flag data begins loading immediately while the UI renders.

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

// Non-blocking start of flag loading
featureFlagManager.initialize().catch(err => {
  server.sendLoggingMessage({ level: 'error', data: err.message });
});

This pattern ensures zero startup latency from flag fetching. If remote data arrives later, the in‑memory store updates automatically. Components that need guaranteed fresh data use waitForFreshFlags() instead.


Local Caching Strategy

The manager persists flags to disk at <config-dir>/feature-flags.json (constructor line 27). This enables offline operation and instant restarts:

  1. First, attempt to read cached file
  2. If present and valid, load into memory (wasLoadedFromCache() returns true)
  3. Regardless, trigger background network fetch to refresh data
  4. On success, overwrite cache with new payload
// Checking cache provenance
if (featureFlagManager.wasLoadedFromCache()) {
  console.log('Running with cached flags; update pending');
}

This graceful degradation ensures the application functions even when remote endpoints are unreachable.


Remote Kill‑Switches in Practice

Kill‑switches allow instant feature disablement without code changes. The feedback prompt in src/utils/usageTracker.ts demonstrates this pattern at lines 424‑425:

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

// Only prompt if explicitly enabled via remote config
if (featureFlagManager.get('feedback_prompt_enabled', false)) {
  showFeedbackDialog();
}

Product teams can set "feedback_prompt_enabled": false in the remote JSON to immediately suppress prompts across all users. No redeployment, no user updates required.


A/B Testing Integration

Experiments live within the same flag payload under an experiments key. The hasFeature helper in src/utils/ab-test.ts (lines 41‑115) resolves variant assignment:

// src/utils/ab-test.ts
export async function hasFeature(experimentName: string): Promise<boolean> {
  const experiments = featureFlagManager.get('experiments', {});
  const experiment = experiments[experimentName];
  
  if (!experiment) return false;
  
  // Deterministic variant assignment based on stable user ID
  const variant = assignVariant(experiment.variants);
  return variant === 'treatment';
}

Usage in application code:

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

if (await hasFeature('new_search_algorithm')) {
  // Execute experimental search path
  results = await experimentalSearch(query);
} else {
  // Standard implementation
  results = await legacySearch(query);
}

The helper handles weighted variants and consistent bucketing—the same user always sees the same variant across sessions.


Dynamic UI Adjustments

UI components query flags directly to conditionally render content. The welcome screen in src/utils/welcome-onboarding.ts shows two patterns:

Conditional Rendering (Line 69)

// src/utils/welcome-onboarding.ts
if (!featureFlagManager.get('welcome_page_enabled', true)) {
  return; // Skip welcome flow entirely
}

Blocking for Fresh Data (Lines 62‑63)

// Wait for remote config before showing personalized onboarding
await featureFlagManager.waitForFreshFlags();

const onboardingVariant = featureFlagManager.get('onboarding_flow', 'standard');
renderOnboarding(onboardingVariant);

The mcp-ui-ab-test.ts utility file provides higher‑level helpers for common UI experiment patterns.


Complete Implementation Example

// Full workflow: startup, cache check, conditional feature, A/B test
import { featureFlagManager } from './utils/feature-flags.js';
import { hasFeature } from './utils/ab-test.js';

async function initializeApp() {
  // Start loading flags immediately
  const initPromise = featureFlagManager.initialize();
  
  // Render basic UI using cache if available
  renderSkeletonUI();
  
  // For critical features, wait for fresh data
  await featureFlagManager.waitForFreshFlags();
  
  // Kill-switch check
  if (featureFlagManager.get('new_dashboard_enabled', false)) {
    // A/B test within the new feature
    const useV2 = await hasFeature('dashboard_v2_layout');
    renderDashboard({ version: useV2 ? 2 : 1 });
  } else {
    renderLegacyDashboard();
  }
}

Summary

  • Centralized manager in src/utils/feature-flags.ts provides single source of truth for all conditional behavior
  • Non-blocking initialization at startup (src/index.ts lines 65‑67) eliminates boot latency
  • Local caching to feature-flags.json enables offline operation and instant restarts
  • Synchronous reads via get() allow pervasive flag checks without async contagion
  • Remote kill-switches empower instant feature disablement without redeployment
  • A/B testing via src/utils/ab-test.ts supports weighted experiments with consistent user bucketing
  • UI integration demonstrates practical patterns for conditional rendering and blocking awaits

Frequently Asked Questions

How does the feature flag system handle network failures?

The manager falls back to cached data automatically. If wasLoadedFromCache() returns true, the application runs with stale flags while a background retry proceeds. This ensures continuous operation even during outages.

Can flags change during a session without restarting?

Yes. The initialize() method updates the in‑memory store when network fetches complete. Subsequent get() calls return new values. However, code that already checked a flag won't retroactively change behavior unless re‑evaluated.

What's the difference between get() and waitForFreshFlags()?

get() returns immediately with current data (cached or fresh). waitForFreshFlags() returns a Promise that resolves only after the network request finishes, useful when you need guaranteed‑fresh configuration before proceeding.

How are users consistently assigned to A/B test variants?

The hasFeature helper in src/utils/ab-test.ts uses a deterministic hash of a stable user identifier combined with experiment name. This ensures the same user always receives the same variant across devices and sessions.

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 →