How Feature Flags Are Implemented in Desktop Commander MCP
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 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:
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) 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 valuesthis.cachePath— a local JSON file atfeature-flags.jsonin the user's config directorythis.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, you'll find:
await featureFlagManager.initialize();
The initialize() method runs this sequence:
- Synchronous cache read —
this.loadFromCache()populatesthis.flagsfrom disk if available - Background network fetch —
this.fetchFlags()requests fresh configuration with a 3-second timeout - Periodic refresh —
setIntervalschedules fetches every 5 minutes (this.cacheMaxAge)
// 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:
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:
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:
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():
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:
// 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:
const onboardingEnabled = featureFlagManager.get('welcome_page_enabled', true);
A/B Test Coordination
The welcome page flow in src/utils/welcome-onboarding.ts demonstrates the full pattern:
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 exposes flag state for debugging:
const allFlags = featureFlagManager.getAll();
Tests and admin commands can force a refresh:
await featureFlagManager.refresh();
Cleanup and Lifecycle
The manager includes explicit cleanup via destroy() to prevent timer leaks:
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
FeatureFlagManagerinstance exported fromsrc/utils/feature-flags.tsserves the entire application - Non-blocking startup:
initialize()returns immediately after cache load; network fetch runs in background - Hard timeouts: 3-second
AbortControllertimeout prevents startup hangs on slow networks - Fresh-data promise:
waitForFreshFlags()lets critical paths await guaranteed current configuration - Local caching: JSON file at
feature-flags.jsonenables 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →