How DesktopCommanderMCP Persists Configuration Settings Between Server Restarts
DesktopCommanderMCP persists settings by storing them in a JSON file on disk that is loaded into memory by a singleton ConfigManager on startup, with all changes synchronized back to disk through a serialized write queue or coalesced background writes.
The DesktopCommanderMCP server, an open-source Model Context Protocol (MCP) implementation, maintains stateful configuration across process lifecycles using a file-based persistence layer. Understanding how this configuration system persists settings between server restarts is essential for developers integrating custom tools, managing containerized deployments, or ensuring consistent behavior across system reboots.
Configuration File Location and Structure
The system stores all user-editable settings in a file named config.json. The exact path is determined by the CONFIG_FILE export in src/config.js, which resolves to a location in the user's configuration directory. Because the file resides on the local filesystem rather than in ephemeral memory, settings survive process termination, container restarts, and system reboots.
The JSON structure is human-readable, written with JSON.stringify(..., null, 2) formatting to preserve indentation and support manual editing when necessary.
The ConfigManager Singleton Pattern
Initialization and First-Run Handling
When the server launches, the ConfigManager singleton executes its init() method in src/config-manager.ts (lines 70-92). This routine checks for the existence of config.json. If the file exists, it parses the JSON into this.config; otherwise, it generates a default configuration via getDefaultConfig(), marks the session as first-run, and immediately writes the defaults to disk.
This initialization sequence ensures that every server restart begins with a valid, fully-populated configuration object held in memory, regardless of whether the persisted file existed previously.
In-Memory State Management
After initialization, all configuration reads serve from the in-memory this.config object. This guarantees that every tool and plugin sees a consistent view of settings without repeated disk I/O, while the persistence layer handles background synchronization to maintain durability.
Persistence Mechanisms and Write Safety
Synchronous Updates for Critical Changes
User-driven configuration changes flow through setValue(key, value), implemented in src/config-manager.ts (lines 41-50). This method updates this.config[key] and immediately invokes saveConfig().
To prevent race conditions during concurrent writes, the implementation serializes all disk operations through a writeChain promise queue. Each write uses JSON.stringify(..., null, 2) for readable formatting, and the promise returned by setValue() does not resolve until the data is safely persisted to config.json.
// Blocking write: ensures data is on disk before continuing
await configManager.setValue('telemetryEnabled', false);
console.log('Setting persisted safely');
Non-Blocking Updates for High-Frequency Operations
Background processes and high-frequency updates use setValueNonBlocking() (lines 82-90), which delegates to scheduleSave(). This method queues a coalesced write via a debounce mechanism, ensuring that many rapid updates collapse into a single disk operation. This pattern protects the libuv thread pool from I/O congestion while still guaranteeing eventual persistence.
// Non-blocking: returns immediately, writes coalesced in background
await configManager.setValueNonBlocking('lastSeen', Date.now());
await configManager.setValueNonBlocking('sessionCount', 42);
// Both values written to disk in a single operation
Special Persistence Cases
Stable Client ID Generation
The getOrCreateClientId() method (lines 15-24) ensures analytics identifiers survive restarts. It checks the clientId field in the configuration; if absent, it generates a UUID, stores it via setValue('clientId', ...), and returns the identifier. This creates a stable fingerprint for telemetry and A/B testing across server lifecycles.
const clientId = await configManager.getOrCreateClientId();
console.log('Persistent client ID:', clientId);
Version Stamping and Migration
After loading the configuration, the manager injects the current library VERSION into the in-memory object. This version stamp is written to config.json on the next save, allowing the system to detect outdated configuration formats and trigger migration logic on subsequent restarts.
Bulk Configuration Operations
For atomic configuration changes, resetConfig() and updateConfig() replace the entire in-memory this.config object and immediately invoke saveConfig(). These operations ensure that bulk updates—such as restoring defaults or importing new settings—are persisted atomically without partial write states.
// Reset to factory defaults and persist immediately
await configManager.resetConfig();
Summary
- DesktopCommanderMCP uses a JSON file (
config.json) located viaCONFIG_FILEinsrc/config.jsto store settings durably. - The
ConfigManagersingleton loads this file duringinit(), defaulting togetDefaultConfig()if missing, and maintains state in memory for performance. - Synchronous writes via
setValue()use a serializedwriteChainto prevent race conditions and guarantee durability before promise resolution. - Non-blocking writes via
setValueNonBlocking()andscheduleSave()coalesce rapid updates into single disk operations to protect system resources. - Special fields like
clientIdandVERSIONare automatically managed to ensure stable identifiers and version tracking across restarts.
Frequently Asked Questions
Where does DesktopCommanderMCP store its configuration file?
The server stores settings in config.json, with the path defined by the CONFIG_FILE constant in src/config.js. This typically resolves to a user-specific configuration directory, ensuring that settings persist across system reboots and container restarts without requiring environment variables or external databases.
How does the server prevent configuration corruption during concurrent writes?
All write operations serialize through a writeChain promise queue in src/config-manager.ts. When setValue() is called, the update joins this chain, ensuring that even if multiple tools attempt simultaneous updates, each write completes before the next begins. The file is written atomically using JSON.stringify, preventing partial JSON corruption.
What is the difference between setValue() and setValueNonBlocking()?
setValue() performs a synchronous, blocking write that returns only after the configuration is saved to disk, making it suitable for critical user preferences. setValueNonBlocking() queues the change via scheduleSave() for a coalesced background write, allowing high-frequency updates (like usage statistics) to batch into a single I/O operation rather than hammering the disk.
How does the server maintain a stable client ID across restarts?
The getOrCreateClientId() method in src/config-manager.ts checks for an existing clientId field in the configuration. If none exists, it generates a UUID, persists it immediately via setValue(), and returns the value. This ensures analytics and telemetry systems can track the same installation across multiple server lifecycles.
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 →