# How Feature Flags Are Implemented in Desktop Commander MCP

> Learn how Desktop Commander MCP implements feature flags with its FeatureFlagManager. Discover immediate caching, background fetches, and promise-based access for fresh data.

- Repository: [Eduard Ruzga/DesktopCommanderMCP](https://github.com/wonderwhy-er/DesktopCommanderMCP)
- Tags: how-to-guide
- Published: 2026-08-02

---

**Desktop Commander MCP implements feature flags through a singleton `FeatureFlagManager` class that loads cached values immediately, fetches fresh configuration from a remote URL in the background, and exposes promise-based accessors for code that needs guaranteed fresh data.**

The `wonderwhy-er/DesktopCommanderMCP` repository uses this manager to control UI A/B tests, onboarding flows, and gradual feature rollouts without blocking server startup. This article breaks down the implementation in [`src/utils/feature-flags.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/feature-flags.ts) and shows how the rest of the codebase consumes the flag API.

---

## The Singleton Manager Pattern

Desktop Commander MCP exports a single global instance that coordinates all feature flag operations:

```typescript
export const featureFlagManager = new FeatureFlagManager();

```

This singleton lives in [[`src/utils/feature-flags.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/feature-flags.ts)](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/feature-flags.ts) and exposes five public methods: `get()`, `getAll()`, `wasLoadedFromCache()`, `waitForFreshFlags()`, and `refresh()`.

The constructor initializes three critical pieces of state:

- `this.flags` — an in-memory object holding the current flag values
- `this.cachePath` — a local JSON file at [`feature-flags.json`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/feature-flags.json) in the user's config directory
- `this.freshFlagsPromise` — a promise that resolves after the first network fetch completes (success or failure)

---

## Startup Flow: Non-Blocking Initialization

The server bootstraps flags without delaying startup. In [`src/index.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/index.ts), you'll find:

```typescript
await featureFlagManager.initialize();

```

The `initialize()` method runs this sequence:

1. **Synchronous cache read** — `this.loadFromCache()` populates `this.flags` from disk if available
2. **Background network fetch** — `this.fetchFlags()` requests fresh configuration with a 3-second timeout
3. **Periodic refresh** — `setInterval` schedules fetches every 5 minutes (`this.cacheMaxAge`)

```typescript
// From feature-flags.ts
async initialize(): Promise<void> {
  await this.loadFromCache();
  // Don't await — let the server start immediately
  this.fetchFlags();
  this.refreshInterval = setInterval(() => this.fetchFlags(), this.cacheMaxAge);
}

```

The hard timeout uses `AbortController` with `Promise.race` to prevent hanging TCP connections:

```typescript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await Promise.race([
  fetch(url, { signal: controller.signal }),
  new Promise<never>((_, reject) => 
    setTimeout(() => reject(new Error('Timeout')), 3000)
  )
]);

```

---

## Remote Configuration Source

By default, flags fetch from `https://desktopcommander.app/flags/v2/production.json`. Operators can override this via environment variable:

```typescript
const url = process.env.DC_FLAG_URL || 'https://desktopcommander.app/flags/v2/production.json';

```

The response is validated and written to `this.cachePath` using standard `fs` operations:

```typescript
await fs.writeFile(this.cachePath, JSON.stringify(this.flags), 'utf8');

```

---

## The Fresh-Flags Promise Pattern

Code that requires guaranteed fresh values—like A/B test assignment on first install—uses `waitForFreshFlags()`:

```typescript
if (!featureFlagManager.wasLoadedFromCache()) {
  await featureFlagManager.waitForFreshFlags();
}
const variant = featureFlagManager.get('experiment_variant', 'control');

```

This promise resolves in two cases:

- **Success:** After the first network fetch completes and updates `this.flags`
- **Failure:** After the fetch errors, so callers never hang indefinitely

The constructor creates this promise and the `fetchFlags()` method resolves it:

```typescript
// Constructor sets up the promise
this.freshFlagsResolve = null;
this.freshFlagsPromise = new Promise((resolve) => {
  this.freshFlagsResolve = resolve;
});

// After first fetch (success or failure)
if (this.freshFlagsResolve) {
  this.freshFlagsResolve();
  this.freshFlagsResolve = null;
}

```

---

## Real-World Usage Patterns

### Basic Flag Access with Defaults

Most code simply calls `get()` with a fallback:

```typescript
const onboardingEnabled = featureFlagManager.get('welcome_page_enabled', true);

```

### A/B Test Coordination

The welcome page flow in [`src/utils/welcome-onboarding.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/welcome-onboarding.ts) demonstrates the full pattern:

```typescript
const enabled = featureFlagManager.get('welcome_page_enabled', true) !== false;

if (!featureFlagManager.wasLoadedFromCache()) {
  try {
    await featureFlagManager.waitForFreshFlags();
  } catch {
    // Continue with defaults on timeout
  }
}

const showOnboarding = featureFlagManager.get('showOnboardingPage', false);

```

### Admin Refresh and Diagnostics

The config tool in [`src/tools/config.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/tools/config.ts) exposes flag state for debugging:

```typescript
const allFlags = featureFlagManager.getAll();

```

Tests and admin commands can force a refresh:

```typescript
await featureFlagManager.refresh();

```

---

## Cleanup and Lifecycle

The manager includes explicit cleanup via `destroy()` to prevent timer leaks:

```typescript
destroy(): void {
  if (this.refreshInterval) {
    clearInterval(this.refreshInterval);
    this.refreshInterval = undefined;
  }
}

```

This is particularly important for test environments that create and tear down multiple server instances.

---

## Summary

- **Singleton architecture:** One `FeatureFlagManager` instance exported from [`src/utils/feature-flags.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/feature-flags.ts) serves the entire application
- **Non-blocking startup:** `initialize()` returns immediately after cache load; network fetch runs in background
- **Hard timeouts:** 3-second `AbortController` timeout prevents startup hangs on slow networks
- **Fresh-data promise:** `waitForFreshFlags()` lets critical paths await guaranteed current configuration
- **Local caching:** JSON file at [`feature-flags.json`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/feature-flags.json) enables offline operation and fast cold starts
- **Periodic refresh:** 5-minute interval keeps flags current without excessive requests

---

## Frequently Asked Questions

### How does Desktop Commander MCP handle feature flags when offline?

The manager silently continues with cached values if the network request fails. On startup, `loadFromCache()` populates `this.flags` from disk before any fetch attempt. If no cache exists, `get()` calls return their default values until a successful refresh occurs.

### What happens if the feature flag endpoint is slow or down?

The fetch uses a 3-second hard timeout via `AbortController`. If the timeout fires or the request errors, the promise still resolves so `waitForFreshFlags()` callers don't hang. The manager continues with existing cached values or defaults.

### Can I change the feature flag URL without modifying code?

Yes. Set the `DC_FLAG_URL` environment variable before starting the server. The manager checks `process.env.DC_FLAG_URL` before falling back to the production default.

### Where is the flag cache stored on disk?

The cache location is [`feature-flags.json`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/feature-flags.json) in the same directory as the user's config file: `path.join(path.dirname(CONFIG_FILE), 'feature-flags.json')`. This path is computed in the `FeatureFlagManager` constructor.