How Configuration Is Managed in Desktop Commander MCP: A Deep Dive into the Singleton ConfigManager

Desktop Commander MCP stores its runtime configuration in a JSON file at ~/.claude-server-commander/config.json, managed by a thread-safe ConfigManager singleton that provides RPC tools, UI editing, and non-blocking persistence.

In the Desktop Commander MCP repository, configuration management is centralized around a single JSON file that serves as the source of truth for all runtime behavior. This article examines how the server loads defaults, persists changes, exposes configuration to clients, and integrates with both programmatic tools and a React-based UI editor.

Where Configuration Lives: File Path and Structure

The configuration file path is hardcoded in src/config.ts through three exported constants:

// src/config.ts
export const USER_HOME = os.homedir();
export const CONFIG_DIR = path.join(USER_HOME, '.claude-server-commander');
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');

This places the configuration in a hidden directory within the user's home folder, following Unix conventions for dotfile storage.

Default Configuration Values

When no config file exists, ConfigManager#getDefaultConfig() in src/config-manager.ts provides these defaults:

  • blockedCommands – Shell commands the server refuses to execute (e.g., rm, dd)
  • defaultShell/bin/sh, /bin/zsh, or powershell.exe on Windows
  • allowedDirectories – Optional whitelist for filesystem access
  • telemetryEnabled – Boolean for analytics collection
  • fileWriteLineLimit / fileReadLineLimit – Safety caps on I/O operations
  • clientId – Anonymous identifier for telemetry

The ConfigManager Singleton: Loading and Initialization

The ConfigManager class in src/config-manager.ts implements a singleton pattern with explicit initialization via ConfigManager.init(). This design prevents race conditions during startup and guarantees exactly one configuration instance exists.

The initialization sequence performs four critical steps:

  1. Directory creation – Uses fs.mkdirSync with recursive: true to ensure ~/.claude-server-commander exists
  2. File reading – Attempts fs.readFile on CONFIG_FILE; falls back to defaults on ENOENT or JSON parse errors
  3. Version stamping – Adds a VERSION field and writes the file if newly created (first-run detection)
  4. Serialization setup – Initializes the writeChain Promise queue and scheduleSave timer for background persistence
// src/config-manager.ts (simplified structure)
export class ConfigManager {
  private static instance: ConfigManager;
  private writeChain: Promise<void> = Promise.resolve();
  private saveTimeout: NodeJS.Timeout | null = null;
  
  async init(): Promise<void> {
    // Steps 1-4 above
  }
  
  static getInstance(): ConfigManager {
    if (!this.instance) this.instance = new ConfigManager();
    return this.instance;
  }
}

RPC Tools for Runtime Configuration Access

The MCP server exposes two tools defined in src/server.ts that allow clients to read and modify configuration without direct filesystem access.

get_config Tool

Returns the complete configuration object. Implemented around line 1329:

// src/server.ts
case "get_config":
  return {
    content: [{
      type: "text",
      text: JSON.stringify(configManager.getConfig(), null, 2)
    }]
  };

set_config_value Tool

Updates a single key and persists to disk. Handles special key __reset__ to restore defaults:

// src/server.ts
case "set_config_value":
  const { key, value } = request.params as { key: string; value: unknown };
  if (key === '__reset__') {
    await configManager.resetConfig();
  } else {
    await configManager.setValue(key, value);
  }
  return { content: [{ type: "text", text: "OK" }] };

Thread-Safe Writes: Blocking vs. Non-Blocking Updates

The ConfigManager distinguishes between critical and non-critical configuration changes through two write paths.

setValue (Blocking)

Waits for disk persistence before returning. Suitable for security-critical updates like blockedCommands:

// Guaranteed durable before the RPC response returns
await configManager.setValue('blockedCommands', ['curl', 'wget']);

setValueNonBlocking (Background)

Updates memory immediately and schedules a coalesced disk write. Used for telemetry flags and high-frequency updates:

// Returns instantly; write happens within SCHEDULE_SAVE_DELAY (typically 5s)
await configManager.setValueNonBlocking('telemetryEnabled', false);

The background save mechanism uses a debounced timer (scheduleSave) that resets on each call, collapsing rapid changes into a single disk write.

Feature Flags and Extended Configuration

The configuration directory also caches remote feature flags in feature-flags.json. The FeatureFlagManager in src/utils/feature-flags.ts reads and writes this file using the same CONFIG_DIR path, keeping related state colocated:


~/.claude-server-commander/
├── config.json          # Primary user configuration

└── feature-flags.json   # Cached remote feature flags

UI Editor Integration

Desktop Commander MCP includes a React-based configuration editor at src/ui/config-editor/src/app.ts. This UI calls the same get_config and set_config_value RPC tools, providing a friendly interface for editing blockedCommands, allowedDirectories, and other settings without manual JSON editing.

The UI editor demonstrates that the configuration management system is protocol-agnostic—any MCP client, whether programmatic or graphical, interacts through identical tool interfaces.

Practical Code Examples

Reading Configuration in a Custom Handler

// src/handlers/example.ts
import { configManager } from '../config-manager.js';

export async function exampleHandler() {
  const shell = await configManager.getValue('defaultShell');
  const blocked = await configManager.getValue('blockedCommands');
  
  // Block execution if command matches blocked list
  if (blocked.includes(proposedCommand)) {
    throw new Error(`Command '${proposedCommand}' is blocked by configuration`);
  }
}

Adding a Command to the Blocked List

// Via MCP tool call
const current = await callTool('get_config', {});
await callTool('set_config_value', {
  key: 'blockedCommands',
  value: [...current.blockedCommands, 'curl']
});

Non-Blocking Telemetry Toggle

// Inside server-side code during request handling
await configManager.setValueNonBlocking('telemetryEnabled', false);
// Response continues immediately; disk write happens in background

MCP Server Registration

The configuration system connects to the broader MCP ecosystem through plugin.json and .mcp.json:

These files enable Claude Desktop and other MCP clients to locate and launch the server, which then initializes its ConfigManager on startup.

Summary

  • Single source of truth: ~/.claude-server-commander/config.json stores all runtime configuration.
  • Safe initialization: ConfigManager.init() creates directories, applies defaults, and handles first-run detection.
  • Dual write paths: setValue() for durability guarantees, setValueNonBlocking() for performance.
  • Tool-based access: get_config and set_config_value RPC tools provide consistent interfaces for all clients.
  • UI integration: The React config editor uses the same tools as programmatic clients.
  • Extensible design: Feature flags and future extensions colocate in the config directory.

Frequently Asked Questions

Where is the Desktop Commander MCP configuration file stored?

The configuration file is stored at ~/.claude-server-commander/config.json (or equivalent on Windows). This path is constructed in src/config.ts using os.homedir() and is consistent across all platforms.

What happens if the configuration file is missing or corrupted?

On first run, or if the file is missing or contains invalid JSON, ConfigManager automatically creates the directory structure and writes a fresh configuration with sensible defaults from getDefaultConfig(). A VERSION field is added to mark the file as initialized.

Can I edit the configuration without using the command line?

Yes. Desktop Commander MCP includes a React-based configuration editor in src/ui/config-editor/src/app.ts that provides a graphical interface for modifying settings. This editor communicates through the same get_config and set_config_value RPC tools used by programmatic clients.

How does the server prevent configuration writes from blocking requests?

For non-critical updates like telemetry flags, the server uses configManager.setValueNonBlocking(), which updates memory immediately and schedules a debounced background write. This prevents the libuv thread pool from starving during heavy tool-call traffic while still ensuring eventual persistence.

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 →