* Start the MCP server. * * Decision order: * 1. `CODEGRAPH_NO_DAEMON=1` → direct mode (unchanged pre-#411 behavior). * 2. `CODEGRAPH_DAEMON_INTERNAL=1` → we ARE the detached daemon; listen. * 3. No `.codegraph/` reachable → direct mode (the daemon's lockfile and * socke
()
| 265 | * mode — a misbehaving daemon must never block a session from starting. |
| 266 | */ |
| 267 | async start(): Promise<void> { |
| 268 | // Long-lived process (direct / proxy / daemon alike): flush buffered |
| 269 | // telemetry opportunistically. Fire-and-forget + unref'd — adds nothing |
| 270 | // to the handshake path and never keeps the process alive. |
| 271 | getTelemetry().startInterval(); |
| 272 | |
| 273 | // #1243: the MCP config launches the local binary, so a server left |
| 274 | // running drifts behind releases with no signal. Refresh the shared |
| 275 | // update-check cache in the background and log ONE stderr notice when a |
| 276 | // newer version exists (stderr only — stdout is the protocol channel). |
| 277 | // The notice also reaches the agent via the initialize instructions and |
| 278 | // codegraph_status. Fire-and-forget: adds nothing to the handshake path. |
| 279 | checkForUpdateInBackground(); |
| 280 | |
| 281 | // The detached daemon process itself. Checked before the opt-out so the |
| 282 | // daemon honors the same env it was spawned with (it never sets NO_DAEMON). |
| 283 | if (daemonInternalSet()) { |
| 284 | return this.startDaemonProcess(); |
| 285 | } |
| 286 | |
| 287 | // Direct mode if the user opted out. Setting the env var is sufficient to |
| 288 | // get the pre-#411 single-process behavior. |
| 289 | if (daemonOptOutSet()) { |
| 290 | return this.startDirect('CODEGRAPH_NO_DAEMON set'); |
| 291 | } |
| 292 | |
| 293 | const root = resolveDaemonRoot(this.projectPath); |
| 294 | if (!root) { |
| 295 | // No initialized project found — daemon mode has nowhere to put its |
| 296 | // socket. The fresh-checkout / outside-project case; behave as before. |
| 297 | return this.startDirect('no .codegraph/ root found'); |
| 298 | } |
| 299 | |
| 300 | try { |
| 301 | // Answer the MCP handshake LOCALLY (instant tool registration — no waiting |
| 302 | // ~600ms for the daemon to spawn+bind, which produced the cold-start race) |
| 303 | // and forward tool CALLS to the shared daemon, connected in the background. |
| 304 | // Runs until the host disconnects; the proxy installs its own watchdog and |
| 305 | // falls back to an in-process engine if the daemon never comes up. |
| 306 | this.mode = 'proxy'; |
| 307 | await this.runProxyWithLocalHandshake(root); |
| 308 | return; |
| 309 | } catch (err) { |
| 310 | // Belt-and-braces: a throw during proxy SETUP (before the client was served) |
| 311 | // is still safe to recover from with a direct-mode session. |
| 312 | const msg = err instanceof Error ? err.message : String(err); |
| 313 | process.stderr.write(`[CodeGraph MCP] Proxy path failed (${msg}); falling back to direct mode.\n`); |
| 314 | return this.startDirect('proxy path threw'); |
| 315 | } |
| 316 | } |
| 317 | |
| 318 | /** |
| 319 | * Stop the server. In daemon mode this triggers graceful shutdown of every |
no test coverage detected