| 179 | * daemon at start time. |
| 180 | */ |
| 181 | export class MCPServer { |
| 182 | private projectPath: string | null; |
| 183 | // Direct-mode-only state. In daemon mode the per-connection sessions live |
| 184 | // inside the Daemon class; in proxy mode there is no session at all. |
| 185 | private session: MCPSession | null = null; |
| 186 | private engine: MCPEngine | null = null; |
| 187 | private daemon: Daemon | null = null; |
| 188 | private ppidWatchdog: ReturnType<typeof setInterval> | null = null; |
| 189 | // Worker-thread liveness watchdog (#850). Long-lived modes only; SIGKILLs the |
| 190 | // process if the main thread wedges in a non-yielding sync loop. |
| 191 | private livenessWatchdog: WatchdogHandle | null = null; |
| 192 | // PPID watchdog baseline — captured at construction so we always have a |
| 193 | // baseline, even if start() runs after a fork-style reparent. |
| 194 | private originalPpid: number = process.ppid; |
| 195 | private hostPpid: number | null = parseHostPpid(process.env[HOST_PPID_ENV]); |
| 196 | // Idempotency guard for stop(). |
| 197 | private stopped = false; |
| 198 | private mode: 'unstarted' | 'direct' | 'proxy' | 'daemon' = 'unstarted'; |
| 199 | |
| 200 | constructor(projectPath?: string) { |
| 201 | this.projectPath = projectPath || null; |
| 202 | } |
| 203 | |
| 204 | /** |
| 205 | * Start the MCP server. |
| 206 | * |
| 207 | * Decision order: |
| 208 | * 1. `CODEGRAPH_NO_DAEMON=1` → direct mode (unchanged pre-#411 behavior). |
| 209 | * 2. `CODEGRAPH_DAEMON_INTERNAL=1` → we ARE the detached daemon; listen. |
| 210 | * 3. No `.codegraph/` reachable → direct mode (the daemon's lockfile and |
| 211 | * socket both live under `.codegraph/`). |
| 212 | * 4. Otherwise connect to (or spawn) the shared daemon and proxy to it. |
| 213 | * |
| 214 | * On any unexpected failure in step 4 we transparently fall back to direct |
| 215 | * mode — a misbehaving daemon must never block a session from starting. |
| 216 | */ |
| 217 | async start(): Promise<void> { |
| 218 | // Long-lived process (direct / proxy / daemon alike): flush buffered |
| 219 | // telemetry opportunistically. Fire-and-forget + unref'd — adds nothing |
| 220 | // to the handshake path and never keeps the process alive. |
| 221 | getTelemetry().startInterval(); |
| 222 | |
| 223 | // The detached daemon process itself. Checked before the opt-out so the |
| 224 | // daemon honors the same env it was spawned with (it never sets NO_DAEMON). |
| 225 | if (daemonInternalSet()) { |
| 226 | return this.startDaemonProcess(); |
| 227 | } |
| 228 | |
| 229 | // Direct mode if the user opted out. Setting the env var is sufficient to |
| 230 | // get the pre-#411 single-process behavior. |
| 231 | if (daemonOptOutSet()) { |
| 232 | return this.startDirect('CODEGRAPH_NO_DAEMON set'); |
| 233 | } |
| 234 | |
| 235 | const root = resolveDaemonRoot(this.projectPath); |
| 236 | if (!root) { |
| 237 | // No initialized project found — daemon mode has nowhere to put its |
| 238 | // socket. The fresh-checkout / outside-project case; behave as before. |
nothing calls this directly
no test coverage detected