Skip to main content
Glama
runner.ts10.1 kB
import * as chokidar from 'chokidar'; import { spawn, ChildProcess } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import chalk from 'chalk'; /** * Development runner for Unity-MCP * * This script: * - Watches for changes in TypeScript and C# code * - Triggers compilation when changes are detected * - Restarts the appropriate services * - Provides a unified development experience */ // Configuration const config = { // TypeScript tsSourceDir: path.resolve(__dirname, '../../src'), tsConfigPath: path.resolve(__dirname, '../../tsconfig.json'), // Unity Client unityClientDir: path.resolve(__dirname, '../../unity-client'), // Docker dockerComposeDevPath: path.resolve(__dirname, '../../docker-compose.dev.yml'), // Build output buildOutputDir: path.resolve(__dirname, '../../dist'), // Debounce time in ms debounceTime: 500, }; // State let tscProcess: ChildProcess | null = null; let serverProcess: ChildProcess | null = null; let unityClientProcess: ChildProcess | null = null; let isCompiling = false; let pendingRestart = false; let lastTsChange = 0; let lastCsChange = 0; /** * Log a message with timestamp */ function log(message: string, type: 'info' | 'error' | 'success' | 'warning' = 'info'): void { const timestamp = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); const prefix = `[${timestamp}]`; switch (type) { case 'info': console.log(chalk.blue(prefix), message); break; case 'error': console.error(chalk.red(prefix), message); break; case 'success': console.log(chalk.green(prefix), message); break; case 'warning': console.log(chalk.yellow(prefix), message); break; } } /** * Start the TypeScript compiler in watch mode */ function startTypeScriptCompiler(): void { if (tscProcess) { log('TypeScript compiler is already running', 'warning'); return; } log('Starting TypeScript compiler in watch mode...', 'info'); tscProcess = spawn('npx', ['tsc', '--watch', '--preserveWatchOutput'], { cwd: path.resolve(__dirname, '../..'), shell: true, stdio: 'pipe', }); tscProcess.stdout?.on('data', (data) => { const output = data.toString(); // Check if compilation is starting if (output.includes('Starting compilation')) { isCompiling = true; log('TypeScript compilation started...', 'info'); } // Check if compilation is complete if (output.includes('Watching for file changes')) { isCompiling = false; log('TypeScript compilation completed', 'success'); // If a restart was requested during compilation, restart now if (pendingRestart) { pendingRestart = false; restartServices(); } } // Check for errors if (output.includes('error TS')) { log('TypeScript compilation error', 'error'); console.error(output); } }); tscProcess.stderr?.on('data', (data) => { log(`TypeScript compiler error: ${data}`, 'error'); }); tscProcess.on('close', (code) => { log(`TypeScript compiler exited with code ${code}`, code === 0 ? 'info' : 'error'); tscProcess = null; }); } /** * Start Docker Compose services */ function startDockerServices(): void { log('Starting Docker services...', 'info'); const dockerComposeUp = spawn('docker-compose', [ '-f', config.dockerComposeDevPath, 'up', '--build', '-d' ], { cwd: path.resolve(__dirname, '../..'), shell: true, stdio: 'inherit', }); dockerComposeUp.on('close', (code) => { if (code === 0) { log('Docker services started successfully', 'success'); startDockerLogs(); } else { log(`Docker services failed to start with code ${code}`, 'error'); } }); } /** * Start Docker Compose logs */ function startDockerLogs(): void { log('Starting Docker logs...', 'info'); const dockerComposeLogs = spawn('docker-compose', [ '-f', config.dockerComposeDevPath, 'logs', '-f' ], { cwd: path.resolve(__dirname, '../..'), shell: true, stdio: 'inherit', }); dockerComposeLogs.on('close', (code) => { log(`Docker logs exited with code ${code}`, code === 0 ? 'info' : 'error'); }); } /** * Restart Docker services */ function restartServices(): void { // If we're currently compiling, set a flag to restart after compilation if (isCompiling) { log('Compilation in progress, will restart services when complete', 'info'); pendingRestart = true; return; } log('Restarting Docker services...', 'info'); const dockerComposeRestart = spawn('docker-compose', [ '-f', config.dockerComposeDevPath, 'restart' ], { cwd: path.resolve(__dirname, '../..'), shell: true, stdio: 'inherit', }); dockerComposeRestart.on('close', (code) => { if (code === 0) { log('Docker services restarted successfully', 'success'); } else { log(`Docker services failed to restart with code ${code}`, 'error'); } }); } /** * Watch for TypeScript file changes */ function watchTypeScriptFiles(): void { log('Watching for TypeScript file changes...', 'info'); const watcher = chokidar.watch(`${config.tsSourceDir}/**/*.ts`, { ignored: /(^|[\/\\])\../, persistent: true, usePolling: true, // Use polling for more reliable detection on Windows/Docker interval: 1000, // Poll every 1 second awaitWriteFinish: true, // Wait for writes to finish }); watcher.on('change', (filePath: string) => { const now = Date.now(); // Debounce changes if (now - lastTsChange < config.debounceTime) { return; } lastTsChange = now; log(`TypeScript file changed: ${filePath}`, 'info'); // TypeScript files are automatically compiled by the tsc --watch process // We don't need to trigger a manual compilation }); } /** * Watch for C# file changes */ function watchCSharpFiles(): void { log('Watching for C# file changes...', 'info'); const watcher = chokidar.watch(`${config.unityClientDir}/**/*.cs`, { ignored: /(^|[\/\\])\../, persistent: true, usePolling: true, // Use polling for more reliable detection on Windows/Docker interval: 1000, // Poll every 1 second awaitWriteFinish: true, // Wait for writes to finish }); watcher.on('change', (filePath: string) => { const now = Date.now(); // Debounce changes if (now - lastCsChange < config.debounceTime) { return; } lastCsChange = now; log(`C# file changed: ${filePath}`, 'info'); // Restart the Unity client container restartUnityClient(); }); } /** * Restart the Unity client container */ function restartUnityClient(): void { log('Restarting Unity client container...', 'info'); const dockerComposeRestart = spawn('docker-compose', [ '-f', config.dockerComposeDevPath, 'restart', 'unity-client' ], { cwd: path.resolve(__dirname, '../..'), shell: true, stdio: 'inherit', }); dockerComposeRestart.on('close', (code) => { if (code === 0) { log('Unity client container restarted successfully', 'success'); } else { log(`Unity client container failed to restart with code ${code}`, 'error'); } }); } /** * Watch for compiled JavaScript file changes */ function watchCompiledJsFiles(): void { log('Watching for compiled JavaScript file changes...', 'info'); const watcher = chokidar.watch(`${config.buildOutputDir}/**/*.js`, { ignored: /(^|[\/\\])\../, persistent: true, usePolling: true, // Use polling for more reliable detection on Windows/Docker interval: 1000, // Poll every 1 second awaitWriteFinish: true, // Wait for writes to finish }); watcher.on('change', (filePath: string) => { const now = Date.now(); // Debounce changes if (now - lastTsChange < config.debounceTime) { return; } lastTsChange = now; log(`Compiled JavaScript file changed: ${filePath}`, 'info'); // Restart the MCP server container restartMcpServer(); }); } /** * Restart the MCP server container */ function restartMcpServer(): void { log('Restarting MCP server container...', 'info'); const dockerComposeRestart = spawn('docker-compose', [ '-f', config.dockerComposeDevPath, 'restart', 'mcp-server' ], { cwd: path.resolve(__dirname, '../..'), shell: true, stdio: 'inherit', }); dockerComposeRestart.on('close', (code) => { if (code === 0) { log('MCP server container restarted successfully', 'success'); } else { log(`MCP server container failed to restart with code ${code}`, 'error'); } }); } /** * Stop all processes and exit */ function cleanup(): void { log('Cleaning up...', 'info'); if (tscProcess) { tscProcess.kill(); } if (serverProcess) { serverProcess.kill(); } if (unityClientProcess) { unityClientProcess.kill(); } // Stop Docker services const dockerComposeDown = spawn('docker-compose', [ '-f', config.dockerComposeDevPath, 'down' ], { cwd: path.resolve(__dirname, '../..'), shell: true, stdio: 'inherit', }); dockerComposeDown.on('close', (code) => { log(`Docker services stopped with code ${code}`, code === 0 ? 'success' : 'error'); process.exit(0); }); } /** * Main function */ function main(): void { log('Starting Unity-MCP development runner...', 'info'); // Register cleanup handlers process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); // Start TypeScript compiler startTypeScriptCompiler(); // Start Docker services startDockerServices(); // Watch for file changes watchTypeScriptFiles(); watchCSharpFiles(); watchCompiledJsFiles(); log('Development runner started successfully', 'success'); log('Press Ctrl+C to stop', 'info'); } // Run the main function main();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TSavo/Unity-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server