Skip to content

fix(mcp): defer scan until roots handshake; harden root_policy#348

Merged
justrach merged 3 commits into
release/0.2.579from
issue-346-failing-test
Apr 30, 2026
Merged

fix(mcp): defer scan until roots handshake; harden root_policy#348
justrach merged 3 commits into
release/0.2.579from
issue-346-failing-test

Conversation

@justrach

@justrach justrach commented Apr 30, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #346 — codedb MCP server was crashing on startup when launched by Cursor, Windsurf, or VS Code because those editors spawn the process with an unpredictable cwd (often /), which hit the isIndexableRoot guard and exited before even responding to initialize.

Closes #346
Closes #347
Closes #278

Root cause

Two bugs compounding each other:

  1. isIndexableRoot check ran unconditionally at startup — killed the process before the MCP handshake could complete
  2. scanBg fired immediately on cwd, which could be /, /Applications, /usr, etc.

Fix

  • root_policy.zig: block system path prefixes (/Applications, /usr, /opt, /System, /Library, /bin, /sbin, /etc, /dev, /proc, /sys, /snap, /nix, /var/folders)
  • main.zig: skip the isIndexableRoot check when in MCP mode with no explicit path arg (deferred mode); introduce DeferredScan context and triggerScanFromRoots callback
  • mcp.zig: after roots/list response arrives and first valid root is parsed, fire the scan callback with the real project path and update ProjectCache.default_path

Behaviour after fix

Client Before After
Cursor / Windsurf / VS Code (roots capable) transport closed immediately initialize → roots/list → scan on correct path
Terminal / Codex (explicit path arg) worked unchanged
Client with no roots support worked (scanned wrong dir) works, 0 files until path given

Test plan

  • Failing test issue-346: root_policy rejects dangerous ambient cwd roots now passes
  • Manually verified: launch from cwd=/, client sends roots, hot returns files from correct project
  • All pre-existing tests still pass (408/410 — issue-150 failures are pre-existing, unrelated)

Generated with Devin

justrach and others added 2 commits April 29, 2026 16:24
Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de6b24ffeb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/mcp.zig Outdated
}

if (s.deferred_scan) |ds| {
if (!ds.triggered.swap(true, .acq_rel)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Trigger deferred indexing only after accepting a root

The deferred-scan gate is flipped before confirming that any valid root was parsed, so an initial roots/list response with zero allowed roots permanently disables later scan attempts. In clients that send an empty roots set first and then roots/list_changed, ds.triggered is already true, so indexing never starts even after a valid workspace arrives.

Useful? React with 👍 / 👎.

Comment thread src/main.zig
if (!snapshot_loaded) {
mcp_server.setScanState(.walking);
const scan_thread = std.Thread.spawn(.{}, scanBg, .{ ctx.io, ctx.store, ctx.explorer, abs_root, ctx.allocator, ctx.scan_done, ctx.shutdown, data_dir, abs_root, ctx.telem, ctx.startup_t0 }) catch return;
scan_thread.detach();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep deferred scan thread joinable during shutdown

The deferred path detaches the scanBg worker, but shutdown only joins scan_thread from the non-deferred branch, so this background thread can outlive the MCP loop and continue touching store/explorer while mainImpl is tearing them down. If the client disconnects during the first deferred scan, this creates a race/use-after-free risk.

Useful? React with 👍 / 👎.

Comment thread src/main.zig
std.process.exit(1);
};
if (!root_policy.isIndexableRoot(abs_root)) {
const mcp_deferred_root = std.mem.eql(u8, cmd, "mcp") and std.mem.eql(u8, root, ".");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish implicit root from explicit '.' argument

Deferred mode is keyed on root == ".", which also matches the explicit invocation codedb . mcp. That path is user-provided and should behave like an explicit root, but it is treated as ambient/deferred, so non-roots clients can end up with no indexing despite a path being supplied.

Useful? React with 👍 / 👎.

After resolving conflicts by taking origin/release/0.2.579 for the
tools_list and handleRemote hunks, re-applied all deferred-scan changes
to src/mcp.zig: DeferredScan struct, Session.deferred_scan field,
updated run() signature, and parseRoots trigger call.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

Benchmark Regression Report

Thresholds: 10.00% and 50,000 ns absolute delta

NOISE means the percentage threshold was exceeded, but the absolute delta was too small to fail CI.

Tool Base (ns) Head (ns) Delta Abs Delta (ns) Status
codedb_bundle 481413 514834 +6.94% +33421 OK
codedb_changes 55517 56860 +2.42% +1343 OK
codedb_deps 8993 8952 -0.46% -41 OK
codedb_edit 4900 6212 +26.78% +1312 NOISE
codedb_find 60377 64773 +7.28% +4396 OK
codedb_hot 98161 102266 +4.18% +4105 OK
codedb_outline 266830 291059 +9.08% +24229 OK
codedb_read 89316 89337 +0.02% +21 OK
codedb_search 175689 176409 +0.41% +720 OK
codedb_snapshot 246357 247763 +0.57% +1406 OK
codedb_status 213306 217483 +1.96% +4177 OK
codedb_symbol 58602 58654 +0.09% +52 OK
codedb_tree 50989 66397 +30.22% +15408 NOISE
codedb_word 67009 68069 +1.58% +1060 OK

@justrach justrach merged commit fc8305a into release/0.2.579 Apr 30, 2026
1 check passed
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.

1 participant