How the Config Write Chain Prevents Race Conditions in Desktop Commander MCP
Desktop Commander MCP eliminates configuration corruption by serializing all disk writes through a private writeChain promise that queues file operations, guaranteeing atomic updates even when multiple tool calls modify config.json simultaneously.
Desktop Commander MCP is a Model Context Protocol (MCP) server that manages system commands and file operations. Because multiple tool calls can simultaneously update the same config.json file, the repository implements a config write chain to guarantee atomic, ordered persistence. This pattern eliminates the classic "read-modify-write" race condition without requiring complex file locking mechanisms.
The Promise Chain Architecture
Initiating the Write Chain
In src/config-manager.ts, the configuration manager initializes a private promise chain that represents the last pending write operation:
// src/config-manager.ts line 55
private writeChain: Promise<void> = Promise.resolve();
This writeChain starts as a resolved promise, establishing the foundation for serializing all subsequent file system operations.
Serializing Writes with saveConfig()
When a caller requests immediate persistence, the saveConfig() method appends the actual disk write to the end of the chain:
// src/config-manager.ts lines 95-98
private async saveConfig(): Promise<void> {
const write = this.writeChain.then(() => this.writeConfigToDisk());
// Keep the chain alive even if this write rejects
this.writeChain = write.catch(() => {});
return write;
}
Each call builds a new promise that executes after the previous one resolves. If two tool calls invoke saveConfig() simultaneously, the second waits for the first to finish, preventing the "lost update" problem where interleaved writes could overwrite each other.
Coalescing Background Updates
Non-Blocking Persistence with scheduleSave()
For high-frequency, non-critical updates—such as usage statistics—the manager provides scheduleSave() to prevent thread pool saturation:
// src/config-manager.ts lines 101-108
scheduleSave(): void {
if (this.saveScheduled) return;
this.saveScheduled = true;
this.writeChain = this.writeChain.then(async () => {
this.saveScheduled = false;
await this.writeConfigToDisk();
});
}
The saveScheduled boolean ensures that only one queued background write exists at any time. A burst of scheduleSave() calls collapses into a single write operation, preventing the libuv thread pool from being flooded with redundant file system operations.
Race Condition Prevention Mechanisms
The config write chain addresses two specific concurrency hazards:
Simultaneous conflicting updates Without the chain, two calls reading the same config, modifying it, and writing back could interleave operations, resulting in corrupted JSON. With the chain, writes are strictly serialized, ensuring the second write sees the first write's result or the updated in-memory state.
Rapid non-critical saves
Without coalescing, each call would spawn a separate fs.writeFile, saturating the thread pool and delaying responses. The scheduleSave() method queues only one background write, ignoring duplicate requests until completion.
Implementation Examples
Blocking Configuration Updates
For critical changes that must persist before returning, use setValue() which internally calls saveConfig():
// Change default shell and wait for disk write
await configManager.setValue('defaultShell', '/bin/bash');
Behind the scenes, this appends the write to writeChain (lines 95-99). The caller awaits the returned promise, ensuring the file is safely written before execution continues.
Fire-and-Forget Telemetry
For telemetry updates that shouldn't block the tool-call response:
// Update telemetry without blocking
await configManager.setValueNonBlocking('telemetryEnabled', false);
This updates the in-memory config immediately and enqueues a single background write via scheduleSave() (lines 101-108), allowing the function to return instantly while the write happens later.
Simulating Concurrent Updates
To verify race condition prevention:
// Simulate two concurrent updates
Promise.all([
configManager.setValue('allowedDirectories', ['/tmp']),
configManager.setValue('allowedDirectories', ['/var'])
]);
Because each setValue call adds its write to the chain, the operations run sequentially. The final on-disk config contains the value from the last completed write, but the JSON structure remains uncorrupted.
Key Source Files
src/config-manager.ts: Central singleton implementing the write chain logic,saveConfig(), andscheduleSave()methods.src/config.ts: Defines theCONFIG_FILElocation used by the manager.src/version.ts: Provides version strings injected into the configuration during initialization.
Summary
- Desktop Commander MCP prevents race conditions by treating every config write as a chained promise rather than an immediate file operation.
- The
writeChaininsrc/config-manager.tsensures writes never overlap by queuing eachwriteConfigToDisk()call after the previous one completes. saveConfig()provides blocking persistence for critical updates, whilescheduleSave()coalesces background saves to prevent thread pool exhaustion.- This pattern guarantees JSON integrity without file locking, even under heavy concurrent tool-call activity.
Frequently Asked Questions
What is a write chain in Node.js?
A write chain is a pattern where file system operations are serialized through a promise chain. Each new write waits for the previous one to complete, ensuring that async operations execute in order rather than racing against each other. Desktop Commander MCP implements this as a private Promise<void> that accumulates .then() handlers for each pending write.
How does Desktop Commander MCP handle concurrent config updates?
According to the source code in src/config-manager.ts, concurrent updates are queued through the writeChain promise. When saveConfig() is called, it creates a new promise that runs only after the current chain resolves. This serializes access to the config.json file, preventing the corruption that would occur if two processes wrote simultaneously.
What is the difference between saveConfig() and scheduleSave()?
saveConfig() is a blocking method that immediately queues a write and returns a promise that resolves when the file is persisted. It is used for critical configuration changes. scheduleSave() is a non-blocking method that marks a save as needed but only executes one write regardless of how many times it is called, making it ideal for high-frequency updates like telemetry logging.
Why use a promise chain instead of file locking?
File locking requires platform-specific implementations and can leave stale locks if processes crash. The promise chain approach is purely JavaScript-based, works identically across all platforms, and automatically cleans up failed operations through the .catch() handler in saveConfig(). It also provides natural coalescing capabilities for background writes that traditional file locking cannot easily achieve.
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 →