Skip to content

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

Open
easonLiangWorldedtech wants to merge 3 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/mcp-settings-stub-race-1371
Open

fix(mcp): preserve concurrent MCP settings during initial creation (fixes #1371)#1380
easonLiangWorldedtech wants to merge 3 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/mcp-settings-stub-race-1371

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Tracking issue: #1385

Summary

Fixes #1371 (part of epic #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.
This is an auto-generated comment: release notes by coderabbit.ai -->

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability when creating MCP settings.
    • Preserves settings added concurrently instead of overwriting them.
end of auto-generated comment: release notes by coderabbit.ai -->

Update (CodeRabbit-sync from trial #1413): head 9dd9825c4 — mcp_settings merge array guard, spec-mock production error parity, unknown-safe error.code read (trial addenda 178e6f4 + 4fc14c4). Review context: trial PR #1413.

…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.
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 08851180-b17d-4a45-bb8a-66f4d6f5ea97

📥 Commits

Reviewing files that changed from the base of the PR and between 78c712a and e11ef14.

📒 Files selected for processing (2)
  • src/services/mcp/McpHub.ts
  • src/services/mcp/__tests__/McpHub.spec.ts

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

MCP settings initialization now uses merge-aware safeWriteJson behavior. Existing non-empty mcpServers configuration is preserved when concurrent initialization writes occur. Tests simulate the race and verify the resulting configuration.

Changes

MCP settings preservation

Layer / File(s) Summary
Locked settings creation and regression coverage
src/services/mcp/McpHub.ts, src/services/mcp/__tests__/McpHub.spec.ts
getMcpSettingsFilePath now writes through safeWriteJson with a merge callback. Existing non-empty mcpServers data is retained. The test mock supports merge callbacks, and a regression test covers concurrent settings creation.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Merge Risk: ⚪ Minimal · up to e11ef

This localized change preserves concurrent MCP configuration during initial settings creation and includes a passing regression test; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: edelauna

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #1371 by replacing the unsafe initial write with locked safeWriteJson merge logic, preserving valid concurrent mcpServers configuration. The regression test covers the report…
Out of Scope Changes check ✅ Passed The changes are limited to the MCP settings initialization fix and its supporting test mock and regression test. No unrelated code changes are identified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
Title check ✅ Passed The title clearly and concisely identifies the MCP race-condition fix and the preservation of concurrent settings.
Description check ✅ Passed The description provides issue references, implementation details, regression-test coverage, lint results, and test results. It does not reproduce every template heading or checklist item, but it cont…
Full details: Linked Issues check

Explanation

The changes satisfy issue #1371 by replacing the unsafe initial write with locked safeWriteJson merge logic, preserving valid concurrent mcpServers configuration. The regression test covers the reported interleaving.

Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2 files.

Full details: Description check

Explanation

The description provides issue references, implementation details, regression-test coverage, lint results, and test results. It does not reproduce every template heading or checklist item, but it contains the required information and is mostly complete.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Codecov reported 2 patch lines (1 missing, 1 partial) in the safeWriteJson merge callback. Add the three remaining fallback cases: absent file (merge sees null), existing content without an mcpServers object, and mcpServers present but not an object - all must write the default stub. All changed lines and branches of the merge callback are now covered.
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Patch coverage gap (Codecov: 2 lines, 1 missing + 1 partial in the merge callback) addressed in 332253a: added the three remaining fallback cases of getMcpSettingsFilePath (absent file, existing content without an mcpServers object, and mcpServers present but not an object). All changed lines and all merge-callback branches are now covered locally (v8 lcov: lines 519/520/521/523 all hit; BRDA outcomes all > 0); 67/67 McpHub spec tests pass.

…, unknown-safe code read

The mcp_settings merge callback now requires a plain object (!Array.isArray), so an existing mcpServers: [] is replaced by the empty stub instead of being preserved and later rejected by McpSettingsSchema; the safeWriteJson spec mock mirrors the production merge contract (only ENOENT and SyntaxError are recoverable, any other read failure rejects before the merge callback runs, with an EACCES regression); the error.code read in the mock uses an unknown-safe type guard instead of an object cast. (CodeRabbit findings on trial Zoo-Code-Org#1413).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

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