Ctrl K
ComunidadGuías…

src/server/mcp-sidecar.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { spawn, type ChildProcess } from 'node:child_process';
import path from 'node:path';
import { getHttpSidecarSpecs, type HttpSidecarSpec } from './shraga-config.ts';
import { APP_ROOT } from './paths.ts';

const sidecars = new Map<string, { proc: ChildProcess; spec: HttpSidecarSpec }>();

async function isPortAlive(url: string): Promise<boolean> {
  try {
    const res = await fetch(url.replace('/mcp', '/health'), { signal: AbortSignal.timeout(2000) });
    return res.ok;
  } catch {
    return false;
  }
}

let shuttingDown = false;

function startOne(spec: HttpSidecarSpec, restarts = 0) {
  const vendorDir = path.join(APP_ROOT, 'vendor', spec.dir);
  const entrypoint = path.join(vendorDir, 'src/mcp/cli.ts');
  const args = ['run', entrypoint, '--port', String(spec.port)];
  const startedAt = Date.now();

  const proc = spawn('bun', args, {
    cwd: vendorDir,
    stdio: ['ignore', 'pipe', 'pipe'],
    env: { ...process.env },
  });

  proc.stdout?.on('data', (d: Buffer) => {
    for (const line of d.toString().trim().split('\n')) {
      if (line) console.log(`[sidecar:${spec.name}] ${line}`);
    }
  });
  proc.stderr?.on('data', (d: Buffer) => {
    for (const line of d.toString().trim().split('\n')) {
      if (line) console.log(`[sidecar:${spec.name}] ${line}`);
    }
  });

  proc.on('exit', (code, signal) => {
    console.log(`[sidecar:${spec.name}] exited code=${code} signal=${signal}`);
    sidecars.delete(spec.name);
    if (shuttingDown) return;
    // Auto-restart on crash (KeepAlive-style). Exponential backoff, capped; reset after a stable run.
    const next = Date.now() - startedAt > 60_000 ? 0 : restarts + 1;
    const delay = Math.min(30_000, 1_000 * 2 ** Math.min(next, 5));
    console.log(`[sidecar:${spec.name}] restarting in ${delay}ms (#${next})`);
    setTimeout(() => {
      if (!shuttingDown && !sidecars.has(spec.name)) startOne(spec, next);
    }, delay);
  });

  sidecars.set(spec.name, { proc, spec });
  console.log(`[sidecar] started ${spec.name} pid=${proc.pid} on port ${spec.port}`);
}

export async function startSidecars() {
  const specs = getHttpSidecarSpecs();
  for (const spec of specs) {
    if (sidecars.has(spec.name)) continue;
    if (await isPortAlive(spec.url)) {
      console.log(`[sidecar] ${spec.name} already running on port ${spec.port}`);
      continue;
    }
    try {
      startOne(spec);
    } catch (e) {
      console.error(`[sidecar] failed to start ${spec.name}:`, e instanceof Error ? e.message : String(e));
    }
  }
}

export function stopSidecars() {
  shuttingDown = true;
  for (const [name, { proc }] of sidecars) {
    console.log(`[sidecar] stopping ${name} pid=${proc.pid}`);
    try { proc.kill('SIGTERM'); } catch {}
  }
  setTimeout(() => {
    for (const [, { proc }] of sidecars) {
      try { proc.kill('SIGKILL'); } catch {}
    }
    sidecars.clear();
  }, 3000);
}