How DesktopCommanderMCP Enables Dynamic Feature Toggles at Runtime

The FeatureFlagManager provides a cache-first, asynchronous mechanism that updates feature flags in-place every 5 minutes, allowing the application to toggle functionality instantly without restarts.

DesktopCommanderMCP implements a robust FeatureFlagManager (located in src/utils/feature-flags.ts) to control feature availability dynamically. This system enables runtime configuration changes without requiring application redeployment or process restarts. The manager follows a singleton pattern, ensuring consistent flag state across all modules while maintaining graceful degradation when network connectivity is unavailable.

Architecture of the Feature Flag Manager

The FeatureFlagManager class implements a seven-stage lifecycle that balances immediate availability with fresh data:

Cache-First Initialization

On startup, the manager prioritizes speed over freshness. The loadFromCache() method (lines 40-55) synchronously reads from a local feature-flags.json file, populating this.flags—a plain JavaScript object that serves as the single source of truth.

// Inside FeatureFlagManager
private async loadFromCache(): Promise<void> {
  // Reads feature-flags.json immediately on startup
  // Populates this.flags without blocking the event loop
}

This cache-first approach ensures the application boots instantly, even if the remote flag service is unreachable.

Background Refresh and Polling

After the cache loads, initialize() (lines 42-61) triggers an asynchronous fetchFlags() call to retrieve the latest configuration from DC_FLAG_URL (or a default production endpoint). A setInterval timer then repeats this fetch every 5 minutes (controlled by cacheMaxAge), ensuring flags remain fresh throughout the process lifetime.

// From src/utils/feature-flags.ts
public async initialize(): Promise<void> {
  await this.loadFromCache();
  this.fetchFlags(); // Non-blocking initial fetch
  
  // Periodic polling every 5 minutes
  this.intervalId = setInterval(() => {
    this.fetchFlags();
  }, this.cacheMaxAge);
}

Safe Timeout Handling

Network requests include defensive programming to prevent hanging connections. The fetchFlags() method (lines 71-89) wraps the fetch in an AbortController and races it against a hard timeout using Promise.race, guaranteeing resolution even on platforms where TCP aborts fail.

private async fetchFlags(): Promise<void> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);
  
  try {
    const response = await fetch(this.url, { signal: controller.signal });
    // Update this.flags in-place
  } catch (error) {
    // Graceful degradation handled here
  } finally {
    clearTimeout(timeout);
  }
}

Core API for Dynamic Runtime Toggles

The manager exposes a minimal public API that enables synchronous reads of asynchronously updated data:

  • get(name, default): Returns the current flag value from this.flags, or the default if undefined.
  • waitForFreshFlags(): Returns a Promise that resolves once the current network fetch completes—critical for first-run scenarios where no cache exists.
  • wasLoadedFromCache(): Boolean indicating whether the current flags came from disk or network.
  • getAll(): Returns the entire flag map for debugging purposes.

Because this.flags is updated in-place when fetchFlags() succeeds, all subsequent get() calls automatically receive the new values without requiring module reloads or state propagation.

Implementation in Source Files

The singleton instance is exported at the bottom of src/utils/feature-flags.ts:

export const featureFlagManager = new FeatureFlagManager();

Onboarding Flow Integration

The src/utils/welcome-onboarding.ts file demonstrates waiting for fresh flags before making UI decisions:

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

export async function handleWelcomePageOnboarding() {
  const pending = await configManager.getValue('pendingWelcomeOnboarding');
  if (!pending) return;

  // Block only for first-time users without cache
  if (!featureFlagManager.wasLoadedFromCache()) {
    await featureFlagManager.waitForFreshFlags();
  }

  const shouldShow = await hasFeature('showOnboardingPage');
  if (shouldShow) {
    // Render onboarding UI
  }
}

A/B Testing and Telemetry

In src/utils/ab-test.ts (line 41), the experiments flag drives test assignments:

const experiments = featureFlagManager.get('experiments', {});

Similarly, src/utils/usageTracker.ts (lines 224-226) conditionally sends surveys based on the user_surveys flag:

if (featureFlagManager.get('user_surveys', false)) {
  // Trigger survey telemetry
}

Server Bootstrap Logging

The main entry point (src/index.ts, line 111) logs the entire flag map during startup for operational visibility:

console.log('Feature flags:', featureFlagManager.getAll());

Graceful Degradation and Error Handling

When fetchFlags() encounters network errors (lines 110-112), the catch block silently preserves the existing cached flags. This ensures that temporary connectivity issues never break functionality—the application simply continues operating with the last known good configuration.

The destroy() method (lines 40-45) clears the polling interval during shutdown, preventing memory leaks and stray handles in long-running processes.

Summary

  • Cache-first loading delivers immediate availability via feature-flags.json on startup, stored in src/utils/feature-flags.ts.
  • Background polling updates flags every 5 minutes without blocking the main thread, using initialize() and fetchFlags().
  • Zero-downtime toggles are achieved by updating this.flags in-place, making changes instantly visible to all get() callers.
  • Defensive timeouts via AbortController and Promise.race prevent network hangs in fetchFlags().
  • Graceful degradation ensures the application remains functional even when the remote flag service is unreachable.
  • Singleton pattern guarantees consistent flag state across welcome-onboarding.ts, ab-test.ts, and usageTracker.ts.

Frequently Asked Questions

How does the FeatureFlagManager handle the first application launch when no cache exists?

On first launch, wasLoadedFromCache() returns false because feature-flags.json does not exist. Code paths that require flags immediately—such as the onboarding flow in src/utils/welcome-onboarding.ts—can await waitForFreshFlags(), which blocks only until the initial network fetch completes. This ensures fresh configuration is available before rendering critical UI, while the rest of the application continues initialization unblocked.

What happens if the remote flag server becomes unreachable?

The manager implements graceful degradation in the fetchFlags() catch block (lines 110-112). If the network request fails—whether due to timeout, DNS failure, or HTTP errors—the manager retains the existing cached flags in this.flags. The application continues operating normally without throwing errors, ensuring stability during network outages.

Can features be toggled without restarting the DesktopCommanderMCP server?

Yes. The FeatureFlagManager updates the this.flags object in-place when fetchFlags() receives a successful response. Because all consumers read from this same object via get() calls, changes to remote flags become effective immediately upon the next polling interval (every 5 minutes) without requiring process restarts or module reloads.

How do I force a manual refresh of feature flags outside the polling cycle?

While the manager automatically polls every 5 minutes, you can trigger an immediate refresh by calling await featureFlagManager.refresh(). This method returns true on successful fetch and update, allowing test suites or administrative commands to bypass the normal polling interval when immediate propagation is required.

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 →