Skip to content

perf(mcp): keep the watcher and an idle query worker out of startup - #419

Merged
bompus merged 1 commit into
fork/consolidatedfrom
perf/lazy-mcp-startup
Oct 9, 2026
Merged

bompus merged 1 commit into
fork/consolidatedfrom
perf/lazy-mcp-startup

Conversation

@bompus

@bompus bompus commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

What changes

  • serve --mcp no longer loads the file watcher, extraction or resolution at startup. project-lifecycle.ts imported LockUnavailableError and watchDisabledReason from the sync barrel, and the barrel pulls in the watcher and everything it uses: about 125 modules. Both names now come from small modules. LockUnavailableError moved to src/sync/lock-unavailable.ts, and the watcher re-exports it, so there is still one class.
  • A query pool with no default project (a session started outside any index) no longer starts a worker thread right away. The first read call that names a project starts one. That call runs in-process meanwhile, as calls already do until a worker is ready. A pool with a default project still starts its worker eagerly.

Measured

Resident memory of a fresh serve --mcp process 8 seconds after the handshake, under Bun 1.4.2 on Linux. Each value is the range of three runs. The base is 54188d0c built the same way.

Session Before (MiB) After (MiB) Change
Proxy to a running daemon 76.2–78.8 65.2–67.0 about 15% less
No index found (direct mode) 87.7–91.7 63.2–64.0 about 29% less

The proxy session loads 66 fewer modules. The direct session drops from 217 modules to 93 and runs one less thread. Sessions with a project open still load these modules when they start watching, so their memory is unchanged.

Not included

A proxy session still loads the engine and tool handlers, which it uses only when the daemon is unreachable. Splitting them out would save about 5 MiB more per proxy session (prototype measurement). It needs an asynchronous engine factory, because the source-level tests cannot follow a lazy require() of a TypeScript sibling. I left it for a separate change.

Checks

  • npm run build, then npx vitest run: 672 files, 8,591 tests passed, 39 skipped.
  • npm run test:bun: 37 failures across 10 files. The same tests fail on the unchanged base in this environment, by name. They include runtime-control, ui-highlight, writer-lock and grammar-load-failure. One base run failed 36, so one of them is flaky.
  • New tests: __tests__/mcp-startup-modules.test.ts fails if the server entry loads the watcher, extraction, resolution or the CodeGraph core again. A query-pool unit test covers warm(). The real-project pool test now expects no worker before the first call in a session with no default project.

README rows checked

  • "About this fork" feature table, the "Reloading MCP launcher" row: unchanged.
  • The "MCP server" memory table on n8n, and the "Node or Bun" paragraph's idle memory figures: both were measured with a project open, which this change does not affect. Not re-run.

Summary by CodeRabbit

  • Improvements
    • Rootless sessions no longer start a background worker immediately. A worker starts when needed, such as when a project-specific read request is made.
    • Repeated warm-up requests start at most one worker, and the pool becomes ready after the worker initializes.

Every `serve --mcp` process loaded the file watcher, and through it all of
extraction and resolution, because project-lifecycle imported two small
names from the sync barrel. Import them from their own modules instead;
LockUnavailableError moves to sync/lock-unavailable.ts so the watcher and
the lifecycle share one class.

A query pool with no default project no longer starts a worker at once. Its
first read call that names a project starts one and runs in-process
meanwhile, as calls already do before a worker is ready.
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository YAML (base), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 327a1b98-2c96-4e80-80bf-4bf277da6495

📥 Commits

Reviewing files that changed from the base of the PR and between 54188d0 and b4acf1b.


📒 Files selected for processing (7)
  • __tests__/mcp-startup-modules.test.ts
  • __tests__/query-pool.test.ts
  • src/mcp/project-lifecycle.ts
  • src/mcp/query-pool.ts
  • src/mcp/tools.ts
  • src/sync/lock-unavailable.ts
  • src/sync/watcher.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.



📝 Walkthrough

Walkthrough

MCP startup imports sync symbols directly. Rootless query pools no longer start a worker during construction. Read-tool execution warms the pool when a project path or default CodeGraph is available.

Changes

MCP startup and query-pool initialization

Layer / File(s) Summary
Startup imports and module loading
src/sync/lock-unavailable.ts, src/sync/watcher.ts, src/mcp/project-lifecycle.ts, __tests__/mcp-startup-modules.test.ts
LockUnavailableError is defined in its own module and imported by the watcher. The lifecycle module imports sync symbols directly. A test checks which modules load when requiring the built MCP entry.
Deferred query-pool workers
src/mcp/query-pool.ts, src/mcp/tools.ts, __tests__/query-pool.test.ts
A rootless pool starts without workers and starts one when warmed. Read-tool execution warms the pool when a project path or default CodeGraph is available. Tests cover warm-up and readiness.

Priority: ➖ Normal

Merge Risk

Merge Risk: ⚪ Minimal · up to b4acf

This change defers worker startup and narrows MCP startup imports to cut memory use. No concrete merge-blocking risk was found.

Security Architecture Review

Security architecture risk: 🔵 Low · up to b4acf

The change delays query-thread startup without changing the inspected project-access checks or execution authority. Worker ownership and shutdown protections remain in place. Residual uncertainty concerns production behavior and incomplete security coverage, rather than an identified new vulnerability.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected change affects the MCP process and its existing query workers over projects already accepted by the handler. Request input can now trigger deferred worker creation, but does not select a new executable or gain additional project-opening authority through this change.

Trust Boundaries and Controls

  • observed — Caller-supplied projectPath still passes the main handler’s validation and index-resolution controls before dispatch. Canonical-root connection selection and explicit project identity remain in place; the existing sensitive-directory check applies when the supplied path exists and should not be interpreted as a complete filesystem sandbox.

Resilience and Maintainability Implications

  • observed — The pre-existing containment mechanisms remain: worker failures consume a crash budget, retries are bounded, late completion cannot settle a job twice, and destruction prevents later warm-up. The soft timeout answers callers but does not cancel synchronous worker execution; that limitation predates this PR.



Pre-merge checks | Passed 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the two primary changes: excluding the watcher from startup and deferring startup of an idle query worker.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Suppressions Explained Passed The pull request adds no lint, type-check, compiler-suppression directive, or ignore configuration entry. The changed lines only add imports, logic, tests, comments, and the LockUnavailableError cla…
User-Visible Changes Documented Passed PASS. The reviewed diff changes only tests and internal MCP/sync implementation files. It does not add, remove, or rename a CLI command or flag, MCP tool or argument, supported language/framework, age…


✨ Finishing Touches 💡 1
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR

🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR


🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@bompus
bompus merged commit 5c60037 into fork/consolidated Oct 9, 2026
6 of 7 checks passed
@bompus
bompus deleted the perf/lazy-mcp-startup branch October 9, 2026 20:18
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