MCPcopy
hub / github.com/colbymchenry/codegraph / MCPServer

Class MCPServer

src/mcp/index.ts:181–446  ·  view source on GitHub ↗

Source from the content-addressed store, hash-verified

179 * daemon at start time.
180 */
181export 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.

Callers

nothing calls this directly

Calls 1

parseHostPpidFunction · 0.90

Tested by

no test coverage detected