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 via CONFIG_FILE in src/config.js to store settings durably.
  • The ConfigManager singleton loads this file during init(), defaulting to getDefaultConfig() if missing, and maintains state in memory for performance.
  • Synchronous writes via setValue() use a serialized writeChain to prevent race conditions and guarantee durability before promise resolution.
  • Non-blocking writes via setValueNonBlocking() and scheduleSave() coalesce rapid updates into single disk operations to protect system resources.
  • Special fields like clientId and VERSION are 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →