feat(mcp): share one serve --mcp per project across MCP clients (#411)

One shared, detached daemon per project root: every `codegraph serve --mcp` is a thin stdio<->socket proxy (Unix socket / Windows named pipe) to it, so N agents in one repo share a single file watcher, SQLite connection, and tree-sitter warm-up instead of N copies. The daemon outlives any single session and reaps via client-refcount + idle timeout; `CODEGRAPH_NO_DAEMON=1` opts out.

Hardened during review: detached-process lifecycle (preserves the #277 watchdog via the proxy; the daemon no longer orphans on host SIGKILL), atomic lockfile + pid-verified stale-clear (no double-daemon on concurrent startup), realpath root canonicalization. Validated on macOS, Linux (Docker - 3x fewer inotify watches for 3 agents), and Windows (named pipes); A/B confirms byte-identical tool output vs direct mode. Closes #411.

Co-Authored-By: Colby McHenry <me@colbymchenry.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Morgan
2026-05-25 19:54:56 -05:00
committed by GitHub
co-authored by Colby McHenry Claude Opus 4.7
parent 2721165604
commit 995da54430
12 changed files with 2256 additions and 547 deletions
+8
View File
@@ -23,6 +23,14 @@ function spawnServer(cwd: string): ChildProcessWithoutNullStreams {
return spawn(process.execPath, [BIN, 'serve', '--mcp'], {
cwd,
stdio: ['pipe', 'pipe', 'pipe'],
// Pin to direct (in-process) mode. #172 is a contract about the in-process
// server's init ordering — the "File watcher active" log this test observes
// is emitted in-process. In daemon mode the watcher runs in the detached
// daemon (logging to .codegraph/daemon.log, not the child's stderr); the
// same response-before-init guarantee lives in the shared session code and
// is covered by mcp-daemon.test.ts. Direct mode also avoids leaking a
// detached daemon from this suite.
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1' },
}) as ChildProcessWithoutNullStreams;
}