Skip to content

MCP settings wiped when multiple windows open — race in McpHub.getMcpSettingsFilePath() direct fs.writeFile #1371

Description

@pajitosingh

MCP settings wiped when multiple windows open — race in McpHub.getMcpSettingsFilePath() direct fs.writeFile

Summary

Opening multiple VS Code windows concurrently can wipe mcp_settings.json to a 122-byte stub {"mcpServers": {}}, losing all MCP server configurations. Root cause is a race in McpHub.getMcpSettingsFilePath() which uses direct fs.writeFile without advisory lock, bypassing the existing safeWriteJson utility.

Environment

  • Extension: zoocodeorganization.zoo-code v3.80.0
  • OS: Linux (generic)
  • VS Code: multiple windows / workspaces open simultaneously

Steps to Reproduce

  1. Have a populated mcp_settings.json at ~/.config/Code/User/globalStorage/zoocodeorganization.zoo-code/settings/mcp_settings.json (or equivalent globalStorage path) with several mcpServers entries.
  2. Open 2+ VS Code windows at roughly the same time (e.g., code . in two workspaces, or restoring session with multiple windows).
  3. Each window constructs McpHub, which calls getMcpSettingsFilePath()ensureSettingsDirectoryExists()fileExistsAtPath()fs.writeFile if not exists.
  4. Observe file truncated to 122 bytes.

Observed Behavior

  • File mcp_settings.json becomes:
    {
      "mcpServers": {
    
      }
    }
    (122 bytes, pretty-printed empty stub)
  • stat shows Birth == Modify == 07:54 (file recreated at that time), indicating overwrite rather than edit.
  • FileSystemWatcher (watchMcpSettingsFile) silently accepts the stub as valid config and propagates empty server list to all windows via debounceConfigChangeupdateServerConnections({}, "global").
  • All MCPs disappear from UI; no error shown (empty object passes McpSettingsSchema validation).

Expected Behavior

  • Concurrent initializations should not clobber existing config.
  • File creation should be atomic and locked, preserving existing servers.

Root Cause

File: src/services/mcp/McpHub.ts:496-517 (getMcpSettingsFilePath())

const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
if (!fileExists) {
  await fs.writeFile(
    mcpSettingsFilePath,
    `{
  "mcpServers": {

  }
}`,
  )
}
  • Uses direct fs.writeFile without lock, while src/utils/safeWriteJson.ts:45 exists and is already used elsewhere in same file (lines 2073, 2158, 2367) for other config writes.
  • safeWriteJson provides proper-lockfile advisory lock (stale 31s, 5 retries, exponential backoff), temp file + atomic rename, and backup/rollback. This path bypasses it.
  • TOCTOU: two processes both see fileExists==false (or one checks while other is mid-write), second blind write wins and truncates.
  • No merge guard: even if file was just created by another window with content, it is overwritten with empty stub.

Watcher: watchMcpSettingsFile() at McpHub.ts:519 debounces and calls handleConfigFileChange which does JSON.parse + McpSettingsSchema.safeParse — empty mcpServers is valid, so no error is surfaced.

Impact

  • Data loss: All MCP server definitions wiped (API keys, commands, URLs, env). User must restore from backup.
  • Silent: No error toast; user discovers only when MCPs missing.
  • Reproducible: Race window is small but reliably hit when opening multiple windows on session restore or via CLI.

Proposed Fix

Replace direct write with safeWriteJson:

- await fs.writeFile(
-   mcpSettingsFilePath,
-   `{
-   "mcpServers": {
-
-   }
- }`,
- )
+ await safeWriteJson(mcpSettingsFilePath, { mcpServers: {} }, { prettyPrint: true })
  • Import already exists: import { safeWriteJson } from "../../utils/safeWriteJson" at McpHub.ts:44.
  • safeWriteJson handles lock, atomic write, dir creation.

Optional stronger guard (handles TOCTOU between fileExistsAtPath and lock):

await safeWriteJson(mcpSettingsFilePath, { mcpServers: {} }, {
  prettyPrint: true,
  merge: (existing, incoming) => {
    if (existing && typeof existing === "object" && "mcpServers" in existing) {
      const ex = existing as { mcpServers?: Record<string, unknown> }
      if (ex.mcpServers && Object.keys(ex.mcpServers).length > 0) return existing
    }
    return incoming
  }
})

This makes it atomic read-modify-write under lock.

Patch prepared against v3.80.0 tag; source-only (installed dist/extension.js is minified, not patched). Patch file available on request.

Workaround Until Fixed

  • Open single window at a time, or stagger window opens by a few seconds.
  • Keep manual backup: e.g., ~/Desktop/CODE/ZooMCPBakup/mcp_settings.json (or any versioned backup outside globalStorage).
  • Restore via cp ~/Desktop/CODE/ZooMCPBakup/mcp_settings.json ~/.config/Code/User/globalStorage/zoocodeorganization.zoo-code/settings/mcp_settings.json and reload window.

Additional Context

  • No duplicate found for mcp_settings.json race OR McpHub race OR mcp wipe (searched via gh issue list).
  • File references: McpHub.ts:496, safeWriteJson.ts:45, GlobalFileNames.mcpSettings.
  • Willing to submit PR with above patch.

Checklist

  • Searched existing issues — no duplicate
  • Anonymized paths (no personal info)
  • Includes code refs and proposed fix

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions