Skip to content

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

Open
easonLiangWorldedtech wants to merge 7 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 7 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.

Review-gate re-trigger (2026-08-30): empty commit 046b78c (no code change) re-runs CI and CodeRabbit current-head review under the org new PR review gate; the code head remains 9dd9825.

…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 StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: c98035e8-3db4-4be8-b9f2-72e71185ff98

📥 Commits

Reviewing files that changed from the base of the PR and between 046b78c and 3519428.

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

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

📜 Recent review details
⚠️ CI failures not shown inline (2)

GitHub Actions: Changed-code mutation testing / 0_mutation-diff.txt: fix(mcp): preserve concurrent MCP settings during initial creation (fixes #1371)

Conclusion: failure

View job details

##[group]Run node scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"
 �[36;1mnode scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"�[0m
 shell: /usr/bin/bash -e {0}
 env:
   PNPM_HOME: /home/runner/setup-pnpm/node_modules/.bin
   STORE_PATH: /home/runner/setup-pnpm/node_modules/.bin/store/v10
   BASE_SHA: 0d937c050810ebc305b34368bf6d6ae8c1f89966
   HEAD_SHA: cdb2958d877a369c505094ab578754d07c5f2799
 ##[endgroup]
 Mutation gate failed: extension has 1009 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.
 ##[error]Process completed with exit code 1.

GitHub Actions: Changed-code mutation testing / mutation-diff: fix(mcp): preserve concurrent MCP settings during initial creation (fixes #1371)

Conclusion: failure

View job details

##[group]Run node scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"
 �[36;1mnode scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"�[0m
 shell: /usr/bin/bash -e {0}
 env:
   PNPM_HOME: /home/runner/setup-pnpm/node_modules/.bin
   STORE_PATH: /home/runner/setup-pnpm/node_modules/.bin/store/v10
   BASE_SHA: 0d937c050810ebc305b34368bf6d6ae8c1f89966
   HEAD_SHA: cdb2958d877a369c505094ab578754d07c5f2799
 ##[endgroup]
 Mutation gate failed: extension has 1009 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.
 ##[error]Process completed with exit code 1.
🧰 Additional context used
📓 Path-based instructions (6)
Check persistence and lifecycle invariants: awaited atomic writes, rollback or explicit partial-failure behavior, cross-window state consistency, stale listeners/watchers, cancellation, idempotency, and safe restart/resume without lost or d...

⚙️ CodeRabbit configuration file

Files:

  • src/services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts
Treat model, provider, MCP, path, command, and tool data as untrusted.

⚙️ CodeRabbit configuration file

Files:

  • src/services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts
Require regression coverage at the lowest valid harness with behavior-focused assertions, including relevant negative, error, false/unset, and boundary cases.

⚙️ CodeRabbit configuration file

Files:

  • src/services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts
Check strict typing and exhaustive behavior across normal, boundary, error, cancellation, retry, and compatibility paths.

⚙️ CodeRabbit configuration file

Files:

  • src/services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts
Verify extension/webview contracts, cancellation and error propagation, VS Code lifecycle correctness, and behavior under retries and partial failure.

⚙️ CodeRabbit configuration file

Files:

  • src/services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts
Act as an adversarial second-opinion reviewer.

⚙️ CodeRabbit configuration file

Files:

  • src/services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts
🪛 ast-grep (0.45.3)
src/services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts

[warning] 128-128: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFile(settingsPath, JSON.stringify(concurrentConfig), "utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)


[warning] 156-156: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFile(settingsPath, "utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🔇 Additional comments (1)
src/services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts (1)

1-175: LGTM!


📝 Summary

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of invalid MCP configuration values, including arrays used where server settings are expected.
    • Prevented configuration files from being overwritten when access is denied or another unrecoverable read error occurs.
    • Preserved valid settings created concurrently during initial configuration-file setup.
    • Added safer defaults when configuration files are missing or incomplete.
  • Tests

    • Expanded coverage for missing, malformed, inaccessible, and concurrently created MCP settings files.

Walkthrough

MCP settings initialization now uses merge-aware safeWriteJson behavior. Valid concurrent mcpServers configuration is preserved. Invalid values use an empty object, and non-recoverable read errors propagate.

Changes

MCP settings preservation

Layer / File(s) Summary
Locked settings creation
src/services/mcp/McpHub.ts
getMcpSettingsFilePath uses a merge callback that preserves non-array object values for mcpServers. Missing, non-object, and array values use { mcpServers: {} }.
Settings creation validation
src/services/mcp/__tests__/McpHub.spec.ts, src/services/mcp/__tests__/McpHub.settingsCreation.integration.spec.ts
Tests model recoverable and non-recoverable read failures. Unit and integration tests cover concurrent configuration preservation, default stub creation, invalid values, advisory locking, cleanup, and EACCES handling.

Priority: ⬆️ High

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: High

Sequence Diagram(s)

sequenceDiagram
  participant McpHub
  participant safeWriteJson
  participant Lock
  participant SettingsFile
  McpHub->>safeWriteJson: create settings with merge callback
  safeWriteJson->>Lock: acquire advisory lock
  Lock->>SettingsFile: read existing configuration
  SettingsFile-->>safeWriteJson: return configuration
  safeWriteJson->>SettingsFile: preserve valid config or write empty mcpServers
Loading

Merge Risk: ⚪ Minimal · up to 35194

The change preserves concurrently written MCP server settings during initialization while retaining default creation and error handling behavior. No unresolved merge-blocking risk was identified.

🚥 Pre-merge checks | ✅ 7
✅ Passed checks (7 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #1371 requires atomic, lock-protected initial creation that does not overwrite MCP servers written after the existence check. getMcpSettingsFilePath() now calls safeWriteJson with a merge ca…
Out of Scope Changes check ✅ Passed The changed production code addresses the linked race directly. The mock update and added unit and integration tests support the merge contract and regression coverage for Issue #1371. No unrelated pr…
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 3…
Regression Evidence ✅ Passed PASS. The changed creation path has focused coverage. McpHub.spec.ts covers concurrent config preservation, absent content, missing mcpServers, non-object values, arrays, and locked-read EACCES failur…
Trust And Persistence Invariants ✅ Passed No changed path meets a stated failure condition. McpHub.getMcpSettingsFilePath() now awaits safeWriteJson with advisory locking, locked read/merge, temporary-file output, and atomic rename. The m…
Title check ✅ Passed The title clearly identifies the MCP race-condition fix and the preservation of concurrent settings. It is concise and directly related to the main change.
Description check ✅ Passed The description explains the issue, implementation, testing, and linked issue. It is mostly complete, although it does not use all template sections, such as the pre-submission checklist and reviewer …
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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).
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Review status

Thanks for contributing. This comment tracks the review sequence and the next action.

Current step: Awaiting fresh human maintainer or CODEOWNER approval.

Automated review is complete for the latest commit but does not replace human approval.

Review-state labels are managed by this workflow; do not edit them manually.

@github-actions github-actions Bot added coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit awaiting-review PR changes are ready and waiting for maintainer re-review and removed awaiting-review PR changes are ready and waiting for maintainer re-review labels Aug 29, 2026
@github-actions github-actions Bot added coderabbit-review-active Required CI passed; CodeRabbit review is active and removed coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-review PR changes are ready and waiting for maintainer re-review labels Aug 30, 2026
@github-actions github-actions Bot removed coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Sep 2, 2026
@github-actions github-actions Bot added the awaiting-maintainer CodeRabbit approved; waiting for a human maintainer label Sep 4, 2026
@github-actions github-actions Bot added awaiting-maintainer CodeRabbit approved; waiting for a human maintainer and removed awaiting-maintainer CodeRabbit approved; waiting for a human maintainer labels Sep 4, 2026
@github-actions github-actions Bot added awaiting-maintainer CodeRabbit approved; waiting for a human maintainer and removed awaiting-maintainer CodeRabbit approved; waiting for a human maintainer labels Sep 7, 2026
@github-actions github-actions Bot added coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit and removed awaiting-maintainer CodeRabbit approved; waiting for a human maintainer labels Sep 12, 2026
@github-actions github-actions Bot added awaiting-maintainer CodeRabbit approved; waiting for a human maintainer and removed coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-maintainer CodeRabbit approved; waiting for a human maintainer

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

3 participants