Skip to content

fix(mcp): preserve concurrent MCP settings during initial creation (fixes #1371) - #30

Closed
easonLiangWorldedtech wants to merge 1 commit into
mainfrom
fix/mcp-settings-stub-race-1371
Closed

fix(mcp): preserve concurrent MCP settings during initial creation (fixes #1371)#30
easonLiangWorldedtech wants to merge 1 commit into
mainfrom
fix/mcp-settings-stub-race-1371

Conversation

@easonLiangWorldedtech

Copy link
Copy Markdown
Owner

Summary

Fixes Zoo-Code-Org#1371 (part of epic Zoo-Code-Org#1375).

MCP settings initialization has a check-then-create race: getMcpSettingsFilePath() did fileExistsAtPath() followed by an unconditional fs.writeFile of the empty stub. Two VS Code windows (separate extension-host processes) racing at startup both see the file as absent, and the second blind write truncates the first window's config to the 122-byte stub — silent data loss of all configured MCP servers.

Changes

  • src/services/mcp/McpHub.ts — the stub creation in getMcpSettingsFilePath() now goes through safeWriteJson with a merge callback:
    • the merge read happens under the existing advisory lock, so any config written by a concurrent process after the existence check is visible to it;
    • if the on-disk content has a proper mcpServers object, it is preserved; otherwise the default stub is written;
    • the fast path is unchanged (file exists → no write at all), so no new writes, no mtime churn, no watcher-triggered MCP reloads.
  • src/services/mcp/__tests__/McpHub.spec.ts:
    • regression test reproduces the interleaving: the existence check sees an absent file, but the locked read sees a concurrent window's config; asserts the creation write carries that config, not the stub. (This test fails against the old raw fs.writeFile implementation.)
    • the safeWriteJson spec mock now honors options.merge (it previously ignored options, which masked merge semantics for every test in this file).

Notes

  • safeWriteJson was already the established pattern for this file (used by the add/remove-server flows); this change removes the one remaining raw fs.writeFile in McpHub.
  • No suppression count changes; eslint --max-warnings=0 clean on both touched files.
  • pnpm --dir src exec vitest run services/mcp/__tests__/McpHub.spec.ts → 64/64 passing.

…ixes Zoo-Code-Org#1371)

getMcpSettingsFilePath() created the default mcp_settings.json with a check-then-write: fileExistsAtPath() followed by an unconditional fs.writeFile of the empty stub. Two windows racing at startup both saw the file as absent, and the second blind write truncated the first window's config to the 122-byte stub.

The stub write now goes through safeWriteJson with a merge callback: the read happens under the advisory lock, and any config already on disk (written by a concurrent process after the existence check) is preserved instead of clobbered. The fast path (file exists -> no write) is unchanged, so no watcher-triggered reloads or write amplification.

Test: regression test reproduces the interleaving (existence check sees absent file, locked read sees the concurrent config) and asserts the creation write carries the concurrent config, not the stub. The safeWriteJson spec mock now honors options.merge.
@easonLiangWorldedtech

Copy link
Copy Markdown
Owner Author

opened as cross-repo upstream PR instead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants