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

Class Daemon

src/mcp/daemon.ts:167–483  ·  view source on GitHub ↗

Source from the content-addressed store, hash-verified

165 * daemons.
166 */
167export class Daemon {
168 private server: net.Server | null = null;
169 private clients = new Set<MCPSession>();
170 /** Per-client peer pids from the optional client-hello, for the liveness sweep. */
171 private clientPeers = new Map<MCPSession, { pid: number | null; hostPid: number | null }>();
172 private idleTimer: NodeJS.Timeout | null = null;
173 private idleTimeoutMs: number;
174 private maxIdleMs: number;
175 private lastActivityAt = Date.now();
176 private maxIdleTimer: NodeJS.Timeout | null = null;
177 private clientSweepTimer: NodeJS.Timeout | null = null;
178 private engine: MCPEngine;
179 private stopping = false;
180 private socketPath: string;
181 private pidPath: string;
182
183 constructor(
184 private projectRoot: string,
185 opts: { idleTimeoutMs?: number; maxIdleMs?: number } = {},
186 ) {
187 this.socketPath = getDaemonSocketPath(projectRoot);
188 this.pidPath = getDaemonPidPath(projectRoot);
189 this.idleTimeoutMs = opts.idleTimeoutMs ?? resolveIdleTimeoutMs();
190 this.maxIdleMs = opts.maxIdleMs ?? resolveMaxIdleMs();
191 // Daemon mode serves many concurrent clients on one event loop, so off-load
192 // read-tool dispatch to a worker pool — otherwise concurrent explores
193 // serialize and starve the MCP transport (clients time out). Direct mode
194 // (one stdio client) leaves the pool off; `CODEGRAPH_QUERY_POOL_SIZE=0`
195 // disables it here too.
196 this.engine = new MCPEngine({ queryPool: true });
197 this.engine.setProjectPathHint(projectRoot);
198 }
199
200 /**
201 * Bind the socket, kick off engine init, and register signal handlers. The
202 * lockfile body was already written atomically by `tryAcquireDaemonLock`, so
203 * there is nothing to write here. The promise resolves once the server is
204 * listening — the daemon then sticks around until idle/shutdown.
205 */
206 async start(): Promise<DaemonStartResult> {
207 // Engine init is deliberately backgrounded — see #172. The first session
208 // to land waits on `ensureInitialized` either way, and unloaded sessions
209 // (cross-project tool calls only) shouldn't pay any open cost.
210 void this.engine.ensureInitialized(this.projectRoot);
211
212 // Walk the ordered socket candidates and bind the first that works. The
213 // in-project path comes first; the deterministic tmpdir path is the fallback
214 // for a filesystem that can't host an AF_UNIX node at all (ExFAT/FAT external
215 // volumes, some network mounts, WSL2 DrvFs → ENOTSUP/EACCES; #997, #974). The
216 // `listen` closure clears a stale socket (left by a SIGKILL'd previous daemon)
217 // before each attempt — safe because we hold the lockfile, so no live daemon
218 // owns it; without it `listen` would wedge on EADDRINUSE.
219 const candidates = getDaemonSocketCandidates(this.projectRoot);
220 const listen = (socketPath: string): Promise<net.Server> =>
221 new Promise<net.Server>((resolve, reject) => {
222 if (process.platform !== 'win32') {
223 try { fs.unlinkSync(socketPath); } catch { /* not-exists is fine */ }
224 }

Callers

nothing calls this directly

Calls

no outgoing calls

Tested by

no test coverage detected