UNPKG

@gacua/backend

Version:

GACUA Backend

143 lines 5.29 kB
/** * @license * Copyright 2025 MuleRun * SPDX-License-Identifier: Apache-2.0 */ import readline from 'readline'; import path from 'path'; import { fork } from 'child_process'; import { createRequire } from 'module'; import net from 'net'; async function findAvailablePort(startPort = 10001) { let port = startPort; const maxAttempts = 10; let attempts = 0; const isFree = (p) => new Promise((resolve) => { const server = net.createServer(); server.once('error', () => { resolve(false); }); server.once('listening', () => { server.close(() => resolve(true)); }); server.listen(p, '127.0.0.1'); }); while (attempts < maxAttempts) { if (await isFree(port)) { return port; } port++; attempts++; } throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${startPort}`); } function resolveMCPComputerEntry() { const require = createRequire(import.meta.url); const pkgJsonPath = require.resolve('@gacua/mcp-computer/package.json'); const pkgDir = path.dirname(pkgJsonPath); const pkgJson = require(pkgJsonPath); const binField = pkgJson.bin; if (!binField) { throw new Error('No binary field found in @gacua/mcp-computer package.json'); } const binRelative = typeof binField === 'string' ? binField : binField['gacua-mcp-computer']; if (!binRelative) { throw new Error('No gacua-mcp-computer binary found in package.json'); } return path.join(pkgDir, binRelative); } async function startMCPComputer() { const mcpComputerEntry = resolveMCPComputerEntry(); const host = 'localhost'; const port = await findAvailablePort(10001); return new Promise((resolve, reject) => { const url = `http://${host}:${port}/mcp`; const successIndicator = `MCP Server for '.computer' tool listening on ${host}:${port}`; const mcpComputerProcess = fork(mcpComputerEntry, ['--host', host, '--port', port.toString()], { detached: false, stdio: 'pipe', }); if (!mcpComputerProcess.pid) { return reject(new Error('Failed to start MCP computer process.')); } const startTimeout = setTimeout(() => { mcpComputerProcess.kill(); reject(new Error(`Timeout: MCP computer did not start within 15 seconds.`)); }, 15000); readline .createInterface({ input: mcpComputerProcess.stdout, crlfDelay: Infinity, }) .on('line', (line) => { console.log(`[MCP] ${line}`); if (line === successIndicator) { clearTimeout(startTimeout); process.env['GACUA_MCP_COMPUTER_URL'] = url; console.log(`[+] GACUA_MCP_COMPUTER_URL=${url}`); resolve(mcpComputerProcess); } }); readline .createInterface({ input: mcpComputerProcess.stderr, crlfDelay: Infinity, }) .on('line', (line) => { console.error(`[MCP] ${line}`); }); mcpComputerProcess.on('exit', (code, signal) => { reject(new Error(`MCP computer exited with code ${code}` + (signal ? `, signal ${signal}` : ''))); }); }); } export async function startMCPComputerDaemon() { let isShuttingDown = false; let mcpComputerDaemon; let restartAttempts = 0; const MAX_RESTART_ATTEMPTS = 3; const RESTART_DELAY = 1000; const startAndMonitor = async () => { try { mcpComputerDaemon = await startMCPComputer(); restartAttempts = 0; // Reset attempts on a successful start setupExitHandler(mcpComputerDaemon); } catch (error) { console.error('Failed to start MCP computer:', error); handleExit(); // Treat a failed start as an exit } }; const handleExit = () => { if (isShuttingDown) return; if (restartAttempts < MAX_RESTART_ATTEMPTS) { restartAttempts++; console.log(`Restarting MCP computer (attempt ${restartAttempts}/${MAX_RESTART_ATTEMPTS})...`); setTimeout(startAndMonitor, RESTART_DELAY * restartAttempts); } else { console.error('Max restart attempts reached. MCP computer will not be restarted.'); } }; const setupExitHandler = (processInstance) => { processInstance.on('exit', (code, signal) => { if (isShuttingDown) return; console.log(`MCP computer exited with code ${code}, signal ${signal}`); handleExit(); }); }; // For the first start, do not use startAndMonitor to error out if the MCP computer fails to start mcpComputerDaemon = await startMCPComputer(); setupExitHandler(mcpComputerDaemon); process.on('exit', () => { isShuttingDown = true; console.log('Exiting — killing GACUA MCP computer'); if (mcpComputerDaemon && !mcpComputerDaemon.killed) { mcpComputerDaemon.kill(); } }); } //# sourceMappingURL=start-mcp-computer.js.map