How Config Persistence and Write Serialization Work in DesktopCommanderMCP
DesktopCommanderMCP guarantees atomic configuration writes by funneling all disk operations through a single promise chain, preventing file corruption when concurrent tool calls attempt to persist settings simultaneously.
The ConfigManager class in DesktopCommanderMCP handles runtime settings by persisting them to a JSON file on disk. Understanding its config persistence and write serialization strategy is essential for developers integrating with the server or building similar Node.js tools that demand high-throughput, crash-safe configuration updates.
Where Configuration Is Stored
DesktopCommanderMCP stores its settings in a JSON file located in the user's home directory under .claude-server-commander. According to the source code in src/config.ts (lines 6-10), the absolute path is constructed at startup and exported as CONFIG_FILE.
On Linux and macOS, this resolves to ~/.claude-server-commander/config.json; on Windows, it maps to the equivalent user-profile directory. This location remains constant for the process lifetime, ensuring predictable file access across all platform targets.
The ConfigManager Singleton
All persistence logic is centralized in src/config-manager.ts within the ConfigManager class. This singleton is the sole authority that reads from and writes to the config file, maintaining an in-memory copy to serve tool calls without blocking on disk I/O.
Initialization and Defaults
On first start, ConfigManager.init() (lines 69-101) performs three critical tasks: it creates the .claude-server-commander directory if missing, loads an existing config.json if present, or falls back to getDefaultConfig() to generate a fresh configuration. The private flag _isFirstRun tracks whether the server just instantiated a new config file, allowing downstream logic to detect first-time setup scenarios.
Write Serialization Mechanism
To prevent concurrent writes from interleaving and corrupting the JSON structure, DesktopCommanderMCP implements a strict write serialization protocol using a promise chain that acts as a FIFO queue.
The Promise Chain Queue
At line 55 in src/config-manager.ts, the class initializes writeChain as Promise.resolve(). This chain serves as the backbone for all write operations. Every subsequent persistence request appends to this chain, guaranteeing that only one fs.writeFile operation executes at any moment, regardless of how many asynchronous tool calls trigger updates simultaneously.
Atomic Disk Writes
The writeConfigToDisk() method (lines 97-100) performs the actual file I/O. It serializes the entire in-memory config object and writes it atomically to CONFIG_FILE. Because this method is only ever invoked through the promise chain, physical disk contention is eliminated, and writes occur in strict order.
Explicit and Scheduled Persistence
Two distinct paths trigger writes, optimized for different latency requirements:
-
saveConfig()(lines 105-110): Creates a new promise that awaits the currentwriteChain, then invokeswriteConfigToDisk(). The chain is updated to this new promise immediately, ensuring subsequent writes queue behind it. Even if a write throws an error, the chain continues to the next link, preventing deadlock. -
scheduleSave()(lines 122-130): Used for high-frequency updates. This method sets asaveScheduledflag to prevent duplicate scheduling, then appends a write towriteChain. When the previous write finishes, the most recent config snapshot is persisted. This coalesces rapid changes into single disk operations, reducing I/O overhead.
API Methods for Configuration Updates
The ConfigManager exposes distinct persistence behaviors through its public API:
setValue(key, value) is fully asynchronous and blocking. It updates the in-memory config and awaits saveConfig(), ensuring the file is flushed to disk before returning. Use this when immediate persistence is required.
setValueNonBlocking(key, value) returns instantly after updating the in-memory config, then delegates the disk write to scheduleSave(). The caller proceeds without waiting for I/O, making this ideal for latency-sensitive tool calls that must remain responsive.
updateConfig(updates) merges multiple key-value changes from a single object and persists them with one atomic write via saveConfig().
resetConfig() restores factory defaults and writes them synchronously (within an async context).
getValue(key) and getConfig() ensure initialization via init() before returning deep copies of the configuration, preventing external mutation of the singleton's internal state.
Practical Code Examples
import { configManager } from './config-manager.js';
// Blocking update: waits until config.json is flushed
await configManager.setValue('defaultShell', '/bin/zsh');
// Non-blocking update: returns immediately, writes in background
await configManager.setValueNonBlocking('fileWriteLineLimit', 50);
// Bulk update: persists multiple changes in one atomic write
await configManager.updateConfig({
telemetryEnabled: false,
allowedDirectories: ['/home/user/projects', '/opt/data']
});
Why Serialization Matters
Without write serialization, rapid tool calls could trigger overlapping fs.writeFile operations. On slow or networked filesystems, this interleaving could fragment or corrupt the JSON structure. By enforcing a single active writer through the promise chain, DesktopCommanderMCP maintains file integrity even under heavy MCP server load.
Additionally, blocking the libuv threadpool with synchronous disk I/O would degrade server responsiveness. The setValueNonBlocking() method solves this by decoupling the API response from the physical write, improving throughput while the serialization layer guarantees eventual consistency.
Summary
- DesktopCommanderMCP persists configuration to
~/.claude-server-commander/config.jsonas defined insrc/config.ts. - The
ConfigManagersingleton insrc/config-manager.tscontrols all access via awriteChainpromise queue initialized at line 55. - Write serialization ensures only one disk operation runs at a time, preventing corruption during concurrent updates.
- Use
setValue()for blocking, guaranteed persistence; usesetValueNonBlocking()for high-frequency updates that queue viascheduleSave(). - The
init()method (lines 69-101) handles first-run detection and default configuration generation.
Frequently Asked Questions
How does DesktopCommanderMCP prevent config file corruption during concurrent writes?
DesktopCommanderMCP prevents corruption by serializing all writes through a single promise chain (writeChain). Every write operation, whether triggered by saveConfig() or scheduleSave(), appends to this chain in src/config-manager.ts. This guarantees FIFO ordering and ensures that only one fs.writeFile call executes at a time, even if dozens of tool calls invoke setValueNonBlocking() simultaneously.
What is the difference between blocking and non-blocking config updates?
setValue() is blocking: it awaits saveConfig(), which waits for the current writeChain to settle and flushes the file to disk before returning. setValueNonBlocking() is non-blocking: it updates the in-memory config instantly and returns control to the caller immediately, while scheduleSave() appends the physical write to the background queue. Use blocking for critical settings and non-blocking for high-frequency, latency-sensitive updates.
How is the configuration initialized on first startup?
During ConfigManager.init() (lines 69-101), the code checks for the existence of the config directory and file. If missing, it creates the directory and writes a default configuration generated by getDefaultConfig(). The private _isFirstRun flag tracks this state, allowing the server to detect when it is running with fresh settings.
What happens if a write operation fails?
The promise chain in saveConfig() (lines 105-110) is designed to be resilient. Even if writeConfigToDisk() throws an error, the writeChain is updated to the new promise immediately. This ensures that subsequent writes continue to process in order rather than hanging indefinitely, preventing the configuration system from deadlocking after a transient disk error.
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 →