How the Feature Flag Manager Enables Gradual Feature Rollouts in DesktopCommanderMCP

The DesktopCommanderMCP feature flag manager uses a cache-first, background-refresh architecture that allows developers to roll out features to a percentage of users instantly by updating a remote JSON file, without requiring client redeployment.

The wonderwhy-er/DesktopCommanderMCP repository implements a self-contained Feature Flag Manager to drive safe, staged rollouts of new functionality. Located in src/utils/feature-flags.ts, the FeatureFlagManager class follows a "cache-first, refresh-in-background" pattern that ensures immediate availability of flags while maintaining synchronization with remote configuration.

Architecture Overview

The manager balances availability and freshness by loading persisted flags from disk on startup, then asynchronously fetching updates from a remote source. This design ensures users see functional defaults immediately—even when offline—while gradually adopting new rollout percentages as the network permits.

The Seven-Step Rollout Mechanism

1. Cache-First Initialization (loadFromCache)

On application startup, the manager attempts to load previously saved flags from a local JSON cache stored at feature-flags.json. If the file is missing or corrupted, the system gracefully degrades to an empty flag set.

In src/utils/feature-flags.ts (lines 39-49), the loadFromCache() method reads the file and populates this.flags, ensuring the application has working values before any network request completes.

2. Background Network Refresh (fetchFlags)

Immediately after cache initialization, the manager fires a non-blocking network request via fetchFlags(). When the remote data arrives, fresh flags replace the cached values and the local JSON file is rewritten with the new state.

This occurs in the initialize() method (lines 42-53), which triggers fetchFlags() and tracks the promise in freshFetchPromise.

3. Periodic Refresh (5-Minute Timer)

To support ongoing rollout adjustments without restarts, the manager establishes a recurring timer that pulls the remote flag file every 5 minutes (configurable via cacheMaxAge).

As implemented in lines 61-66 of src/utils/feature-flags.ts, setInterval(..., this.cacheMaxAge) ensures that percentage-based rollouts can be increased from 10% to 50% of users simply by updating the remote configuration.

4. Synchronous Flag Retrieval (get)

Code checks flag values through the get(name, fallback) method (lines 78-84), which returns the current in-memory value from this.flags. This enables immediate, synchronous decisions in UI rendering paths.

// Enable a beta UI component for configured percentage of users
if (featureFlagManager.get('new_sidebar', false)) {
  showNewSidebar();
}

5. Fresh-Flag Synchronization (waitForFreshFlags)

For critical A/B test assignments that require guaranteed fresh data, the manager exposes waitForFreshFlags() (lines 20-34). This method races the pending fetch promise against a 5-second safety timeout, preventing application hangs during network degradation.

// Guarantee freshest data before A/B assignment
await featureFlagManager.waitForFreshFlags();
const variant = Math.random() < featureFlagManager.get('beta_search_pct', 0.1)
                ? 'new' : 'old';
runSearchVariant(variant);

6. Manual Refresh (refresh)

In development environments or test suites, you can bypass the timer and force an immediate update using refresh() (lines 92-100), which internally calls fetchFlags() and returns the resulting promise.

// Force immediate flag update in tests
await featureFlagManager.refresh();

7. Graceful Shutdown (destroy)

To prevent stray background work during application exit, the destroy() method (lines 38-46) clears the periodic interval timer, ensuring clean process termination.

Integration Points Across the Codebase

The feature flag system integrates with several modules:

Summary

  • Cache-first startup: Loads feature-flags.json immediately, ensuring offline functionality
  • Background synchronization: Non-blocking fetch updates flags without freezing the UI
  • Periodic updates: 5-minute refresh cycle allows real-time rollout percentage adjustments
  • Synchronous access: get() method provides immediate flag checks for UI decisions
  • Fresh data guarantee: waitForFreshFlags() ensures critical paths use the latest remote values
  • Clean lifecycle: destroy() method prevents memory leaks and background process accumulation

Frequently Asked Questions

How does the feature flag manager handle offline scenarios?

The cache-first architecture ensures that if feature-flags.json exists locally, the application initializes with those values regardless of network connectivity. Users see the last known flag states, and the background refresh simply continues retrying on its 5-minute interval until connectivity returns.

Can I force an immediate flag update without waiting for the 5-minute timer?

Yes. Call await featureFlagManager.refresh() to trigger fetchFlags() immediately. This is useful in development environments or when you need to verify flag changes during integration testing without restarting the application.

What happens if the remote flag fetch fails?

The manager silently fails the background fetch and retains the current in-memory flags (either from the initial cache or the last successful refresh). The get() method continues returning the cached values, ensuring application stability during network outages.

How are rollout percentages controlled?

Rollout percentages are stored as numeric values in the remote JSON configuration (e.g., 0.1 for 10%). The consuming code uses featureFlagManager.get('feature_name', 0) to retrieve the percentage, then applies standard randomization logic (Math.random() < percentage) to determine if the current user receives the new feature. Updating the remote JSON instantly changes the percentage for all clients within their next refresh window.

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 →