fix(mcp): skip fs.watch on WSL2 /mnt drives that hang MCP startup (#199) (#210)

Recursive fs.watch on a WSL2 /mnt NTFS/9p mount walks the directory tree
with every readdir/stat crossing the Windows boundary, stalling the event
loop long enough to blow past opencode's 30s MCP handshake timeout so the
tools never appear. This is the file-watcher half of the #172 fix, which
moved the DB/WASM open off the handshake but left the watcher on the
critical path.

- Add watchDisabledReason() policy: CODEGRAPH_NO_WATCH (off) >
  CODEGRAPH_FORCE_WATCH (force on) > WSL2 + /mnt auto-detect (off).
  FileWatcher.start() and the MCP server both honor it; the server now
  logs why watching is off and how to refresh.
- Add `codegraph serve --mcp --no-watch`.
- When watching is off, init/install offer git sync hooks (post-commit,
  post-merge, post-checkout) that run `codegraph sync` in the background,
  or fall back to manual sync; either way the user is told the index
  stays frozen until re-synced. uninit removes the hooks.
- Tests: watch-policy + git-hooks (idempotency, user-content preservation,
  core.hooksPath).

Root-cause analysis and workaround by @mengfanbo123.

Closes #199

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-05-20 10:32:08 -05:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 79b9601aae
commit cf7db7cb98
10 changed files with 714 additions and 5 deletions
+11
View File
@@ -13,6 +13,7 @@ import { CodeGraphConfig } from '../types';
import { shouldIncludeFile } from '../extraction';
import { logDebug, logWarn } from '../errors';
import { normalizePath } from '../utils';
import { watchDisabledReason } from './watch-policy';
/**
* Options for the file watcher
@@ -82,6 +83,16 @@ export class FileWatcher {
if (this.watcher) return true; // Already watching
this.stopped = false;
// Some environments make recursive fs.watch unusable — most notably WSL2
// /mnt/ drives, where setup blocks long enough to break MCP startup
// handshakes (issue #199). Skip watching there; callers fall back to
// manual `codegraph sync` or the git sync hooks.
const disabledReason = watchDisabledReason(this.projectRoot);
if (disabledReason) {
logDebug('File watcher disabled', { reason: disabledReason, projectRoot: this.projectRoot });
return false;
}
try {
this.watcher = fs.watch(
this.projectRoot,