How DesktopCommander MCP's Feature Flag System Enables Dynamic Behavior Changes
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. 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 lookupgetAll()– Returns entire flag payloadwasLoadedFromCache()– Indicates if current data came from diskwaitForFreshFlags()– Promise that resolves when network fetch completesinitialize()– Async bootstrap that starts fetch without blocking
// 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 lines 65‑67. The main entry point calls initialize() non‑blocking so flag data begins loading immediately while the UI renders.
// 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:
- First, attempt to read cached file
- If present and valid, load into memory (
wasLoadedFromCache()returnstrue) - Regardless, trigger background network fetch to refresh data
- On success, overwrite cache with new payload
// 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 demonstrates this pattern at lines 424‑425:
// 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 (lines 41‑115) resolves variant assignment:
// 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:
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 shows two patterns:
Conditional Rendering (Line 69)
// src/utils/welcome-onboarding.ts
if (!featureFlagManager.get('welcome_page_enabled', true)) {
return; // Skip welcome flow entirely
}
Blocking for Fresh Data (Lines 62‑63)
// 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 utility file provides higher‑level helpers for common UI experiment patterns.
Complete Implementation Example
// 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.tsprovides single source of truth for all conditional behavior - Non-blocking initialization at startup (
src/index.tslines 65‑67) eliminates boot latency - Local caching to
feature-flags.jsonenables 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.tssupports 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 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.
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 →