Problem
When codedb mcp is launched with no explicit root (the default — running from cwd), it enters deferred scan mode: scan state is set to loading_snapshot, and the actual filesystem walk only kicks off after the client responds to a roots/list request. The trigger is fired in parseRoots only when at least one root survives the root_policy.isIndexableRoot filter:
if (s.deferred_scan) |ds| {
if (s.roots.items.len > 0 and !ds.triggered.swap(true, .acq_rel)) {
const uri_raw = s.roots.items[0].uri;
const abs_path = if (std.mem.startsWith(u8, uri_raw, "file://")) uri_raw[7..] else uri_raw;
ds.triggerFn(ds, abs_path);
}
}
Several real client behaviors leave the scan stuck in loading_snapshot forever:
- Client doesn't advertise
capabilities.roots — requestRoots is never sent, parseRoots is never reached.
- Client supports roots but never responds to
roots/list — pending_roots_id stays unmatched.
- Client returns an empty
roots array — parseRoots runs but s.roots.items.len == 0, so the trigger doesn't fire.
- Client returns roots that are all denied by
root_policy.isIndexableRoot — same: s.roots.items.len == 0.
While stuck, every codedb_search/codedb_outline/etc. response gets the appended hint:
note: scan still in progress (state=loading_snapshot); results may be incomplete — retry shortly
…which never clears, because nothing transitions the state. CODEDB_ROOT is the only existing escape hatch.
Failing Test
test "issue-384: deferred scan falls back to cwd when client returns no indexable roots" {
// [ records call to triggerFn ]
var ds: mcp_mod.DeferredScan = .{ /* ...fields... */, .triggerFn = Recorder.cb };
const empty_roots: []const mcp_mod.Root = &.{};
const fired = mcp_mod.triggerDeferredScanWithFallback(&ds, empty_roots, "/Users/me/proj");
try testing.expect(fired);
try testing.expectEqualStrings("/Users/me/proj", Recorder.path_buf[0..Recorder.path_len]);
// Idempotent — second call must not re-fire
const again = mcp_mod.triggerDeferredScanWithFallback(&ds, empty_roots, "/elsewhere");
try testing.expect(!again);
}
This helper does not exist on main, so the test fails to compile until the fix lands.
Expected
When no indexable roots arrive (empty roots, all denied, or no response within a few seconds), the deferred scan should fall back to scanning cwd — assuming cwd itself is policy-allowed — instead of sitting in loading_snapshot indefinitely.
Fix
- Add
pub fn triggerDeferredScanWithFallback(ds, indexable_roots, fallback_cwd) bool in mcp.zig. It picks the first root if available, otherwise the policy-checked fallback_cwd, and fires the trigger atomically.
- Add
fallback_cwd: []const u8 to DeferredScan. Populate it in main.zig (from the resolved abs_root at startup).
- Replace the inline trigger logic in
parseRoots with a call to the helper, so empty-roots responses now resolve to cwd.
- Add a 3-second watchdog in
watcherDeferredLoop (main.zig) that calls the same helper, covering the case where the client doesn't respond to roots/list (or doesn't support roots at all).
Problem
When
codedb mcpis launched with no explicit root (the default — running from cwd), it enters deferred scan mode: scan state is set toloading_snapshot, and the actual filesystem walk only kicks off after the client responds to aroots/listrequest. The trigger is fired inparseRootsonly when at least one root survives theroot_policy.isIndexableRootfilter:Several real client behaviors leave the scan stuck in
loading_snapshotforever:capabilities.roots—requestRootsis never sent,parseRootsis never reached.roots/list—pending_roots_idstays unmatched.rootsarray —parseRootsruns buts.roots.items.len == 0, so the trigger doesn't fire.root_policy.isIndexableRoot— same:s.roots.items.len == 0.While stuck, every
codedb_search/codedb_outline/etc. response gets the appended hint:…which never clears, because nothing transitions the state.
CODEDB_ROOTis the only existing escape hatch.Failing Test
This helper does not exist on
main, so the test fails to compile until the fix lands.Expected
When no indexable roots arrive (empty roots, all denied, or no response within a few seconds), the deferred scan should fall back to scanning cwd — assuming cwd itself is policy-allowed — instead of sitting in
loading_snapshotindefinitely.Fix
pub fn triggerDeferredScanWithFallback(ds, indexable_roots, fallback_cwd) boolinmcp.zig. It picks the first root if available, otherwise the policy-checkedfallback_cwd, and fires the trigger atomically.fallback_cwd: []const u8toDeferredScan. Populate it inmain.zig(from the resolvedabs_rootat startup).parseRootswith a call to the helper, so empty-roots responses now resolve to cwd.watcherDeferredLoop(main.zig) that calls the same helper, covering the case where the client doesn't respond toroots/list(or doesn't support roots at all).