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
- 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.
- Open 2+ VS Code windows at roughly the same time (e.g.,
code . in two workspaces, or restoring session with multiple windows).
- Each window constructs
McpHub, which calls getMcpSettingsFilePath() → ensureSettingsDirectoryExists() → fileExistsAtPath() → fs.writeFile if not exists.
- Observe file truncated to 122 bytes.
Observed Behavior
- File
mcp_settings.json becomes:
(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 debounceConfigChange → updateServerConnections({}, "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
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.jsonto a 122-byte stub{"mcpServers": {}}, losing all MCP server configurations. Root cause is a race inMcpHub.getMcpSettingsFilePath()which uses directfs.writeFilewithout advisory lock, bypassing the existingsafeWriteJsonutility.Environment
zoocodeorganization.zoo-codev3.80.0Steps to Reproduce
mcp_settings.jsonat~/.config/Code/User/globalStorage/zoocodeorganization.zoo-code/settings/mcp_settings.json(or equivalent globalStorage path) with severalmcpServersentries.code .in two workspaces, or restoring session with multiple windows).McpHub, which callsgetMcpSettingsFilePath()→ensureSettingsDirectoryExists()→fileExistsAtPath()→fs.writeFileif not exists.Observed Behavior
mcp_settings.jsonbecomes:{ "mcpServers": { } }statshowsBirth == 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 viadebounceConfigChange→updateServerConnections({}, "global").McpSettingsSchemavalidation).Expected Behavior
Root Cause
File:
src/services/mcp/McpHub.ts:496-517(getMcpSettingsFilePath())fs.writeFilewithout lock, whilesrc/utils/safeWriteJson.ts:45exists and is already used elsewhere in same file (lines 2073, 2158, 2367) for other config writes.safeWriteJsonprovidesproper-lockfileadvisory lock (stale 31s, 5 retries, exponential backoff), temp file + atomic rename, and backup/rollback. This path bypasses it.fileExists==false(or one checks while other is mid-write), second blind write wins and truncates.Watcher:
watchMcpSettingsFile()atMcpHub.ts:519debounces and callshandleConfigFileChangewhich doesJSON.parse+McpSettingsSchema.safeParse— emptymcpServersis valid, so no error is surfaced.Impact
Proposed Fix
Replace direct write with
safeWriteJson:import { safeWriteJson } from "../../utils/safeWriteJson"atMcpHub.ts:44.safeWriteJsonhandles lock, atomic write, dir creation.Optional stronger guard (handles TOCTOU between
fileExistsAtPathand lock):This makes it atomic read-modify-write under lock.
Patch prepared against
v3.80.0tag; source-only (installeddist/extension.jsis minified, not patched). Patch file available on request.Workaround Until Fixed
~/Desktop/CODE/ZooMCPBakup/mcp_settings.json(or any versioned backup outside globalStorage).cp ~/Desktop/CODE/ZooMCPBakup/mcp_settings.json ~/.config/Code/User/globalStorage/zoocodeorganization.zoo-code/settings/mcp_settings.jsonand reload window.Additional Context
mcp_settings.json race OR McpHub race OR mcp wipe(searched viagh issue list).McpHub.ts:496,safeWriteJson.ts:45,GlobalFileNames.mcpSettings.Checklist