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

> Discover how DesktopCommander MCP's feature flag system dynamically alters behavior without redeployment using remote JSON payloads and a synchronous API.

- Repository: [Eduard Ruzga/DesktopCommanderMCP](https://github.com/wonderwhy-er/DesktopCommanderMCP)
- Tags: internals
- Published: 2026-08-04

---

**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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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

```typescript
// 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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/index.ts) lines 65‑67. The main entry point calls `initialize()` non‑blocking so flag data begins loading immediately while the UI renders.

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

```typescript
// 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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/usageTracker.ts) demonstrates this pattern at lines 424‑425:

```typescript
// 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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/ab-test.ts) (lines 41‑115) resolves variant assignment:

```typescript
// 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:

```typescript
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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/welcome-onboarding.ts) shows two patterns:

### Conditional Rendering (Line 69)

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

```

### Blocking for Fresh Data (Lines 62‑63)

```typescript
// 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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/mcp-ui-ab-test.ts) utility file provides higher‑level helpers for common UI experiment patterns.

---

## Complete Implementation Example

```typescript
// 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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/feature-flags.ts) provides single source of truth for all conditional behavior
- **Non-blocking initialization** at startup ([`src/index.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/index.ts) lines 65‑67) eliminates boot latency
- **Local caching** to [`feature-flags.json`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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.