Skip to main content
Glama
relay-server-manager.ts9.63 kB
/** * RelayServerManager handles starting and managing the relay server process */ import { RelayServerProcessConfig } from '@vrchat-mcp-osc/types'; import { createLogger, delay, withTimeout } from '@vrchat-mcp-osc/utils'; import { ChildProcess, spawn } from 'child_process'; import path from 'path'; // Default configuration const DEFAULT_CONFIG: RelayServerProcessConfig = { execPath: 'node', args: ['dist/index.js'], autoRestart: true, maxRestarts: 3, restartDelay: 2000, startupTimeout: 5000 }; // Manager events export enum RelayServerManagerEvent { STARTED = 'started', STOPPED = 'stopped', ERROR = 'error', RESTARTING = 'restarting' } /** * RelayServerManager handles starting and managing the relay server process */ export class RelayServerManager { private config: RelayServerProcessConfig; private process: ChildProcess | null = null; private isRunning: boolean = false; private restartCount: number = 0; private logger = createLogger('RelayServerManager'); private eventHandlers: Map<RelayServerManagerEvent, Array<(...args: any[]) => void>> = new Map(); private startPromise: Promise<boolean> | null = null; /** * Create a new relay server manager * * @param config Configuration options */ constructor(config: Partial<RelayServerProcessConfig> = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; this.logger.info('RelayServerManager initialized'); } /** * Start the relay server process * * @returns Promise that resolves to true if started successfully */ public async start(): Promise<boolean> { if (this.isRunning) { this.logger.info('Relay server is already running'); return true; } if (this.startPromise) { this.logger.info('Relay server is already starting'); return this.startPromise; } this.startPromise = this._startProcess(); const result = await this.startPromise; this.startPromise = null; return result; } /** * Internal method to start the relay server process * * @returns Promise that resolves to true if started successfully */ private async _startProcess(): Promise<boolean> { this.logger.info('Starting relay server process'); // Verify if the configured executable exists if (!this.config.execPath) { this.logger.error('No executable path specified'); this.emitEvent(RelayServerManagerEvent.ERROR, new Error('No executable path specified')); return false; } try { // Get working directory const workingDir = path.dirname(this.resolveExecPath()); // Prepare environment variables const env = { ...process.env, ...this.config.env }; // Start the process this.process = spawn( this.config.execPath, this.config.args || [], { cwd: workingDir, env, stdio: ['ignore', 'pipe', 'pipe'] } ); // Set up listeners this.setupProcessListeners(); // Wait for startup try { const success = await withTimeout( this.waitForStartup(), this.config.startupTimeout || 5000, 'Relay server startup timed out' ); if (success) { this.isRunning = true; this.restartCount = 0; this.logger.info('Relay server started successfully'); this.emitEvent(RelayServerManagerEvent.STARTED); return true; } else { this.logger.error('Failed to start relay server'); this.killProcess(); this.emitEvent(RelayServerManagerEvent.ERROR, new Error('Failed to start relay server')); return false; } } catch (error) { this.logger.error(`Relay server startup error: ${error instanceof Error ? error.message : String(error)}`); this.killProcess(); this.emitEvent(RelayServerManagerEvent.ERROR, error instanceof Error ? error : new Error(String(error))); return false; } } catch (error) { this.logger.error(`Error starting relay server: ${error instanceof Error ? error.message : String(error)}`); this.emitEvent(RelayServerManagerEvent.ERROR, error instanceof Error ? error : new Error(String(error))); return false; } } /** * Setup listeners for the child process */ private setupProcessListeners(): void { if (!this.process) return; // Handle process output if (this.process.stdout) { this.process.stdout.on('data', (data) => { const lines = data.toString().split('\n').filter(Boolean); lines.forEach((line: any) => this.logger.info(`[RelayServer] ${line}`)); }); } if (this.process.stderr) { this.process.stderr.on('data', (data) => { const lines = data.toString().split('\n').filter(Boolean); lines.forEach((line: any) => this.logger.error(`[RelayServer] ${line}`)); }); } // Handle process exit this.process.on('exit', (code, signal) => { this.logger.info(`Relay server process exited with code ${code} and signal ${signal}`); this.isRunning = false; this.process = null; // // Handle automatic restart // // // if (this.config.autoRestart && this.restartCount < (this.config.maxRestarts || 3)) { // this.handleAutoRestart(); // } else { // this.emitEvent(RelayServerManagerEvent.STOPPED, { code, signal }); // } } ); // Handle process errors this.process.on('error', (error) => { this.logger.error(`Relay server process error: ${error.message}`); this.emitEvent(RelayServerManagerEvent.ERROR, error); }); } /** * Handle automatic restart of the relay server */ private async handleAutoRestart(): Promise<void> { this.restartCount++; this.logger.info(`Restarting relay server (attempt ${this.restartCount})`); this.emitEvent(RelayServerManagerEvent.RESTARTING, { attempt: this.restartCount }); // Wait before restart await delay(this.config.restartDelay || 2000); // Try to start again this._startProcess().catch(error => { this.logger.error(`Failed to restart relay server: ${error instanceof Error ? error.message : String(error)}`); }); } /** * Wait for the relay server to start up * * @returns Promise that resolves to true when server is ready */ private async waitForStartup(): Promise<boolean> { // TODO: Implement a real health check here, e.g., connect to the WebSocket server // For now, just wait a bit and assume it's running if the process is still alive await delay(1000); return this.process !== null && this.process.exitCode === null; } /** * Stop the relay server process * * @returns Promise that resolves when the server is stopped */ public async stop(): Promise<void> { if (!this.isRunning || !this.process) { this.logger.info('Relay server is not running'); return; } this.logger.info('Stopping relay server process'); this.killProcess(); // Wait for the process to exit await new Promise<void>((resolve) => { if (!this.process) { resolve(); return; } this.process.once('exit', () => { resolve(); }); // Force kill after timeout setTimeout(() => { this.killProcess(true); resolve(); }, 5000); }); this.isRunning = false; this.process = null; this.logger.info('Relay server stopped'); this.emitEvent(RelayServerManagerEvent.STOPPED); } /** * Kill the relay server process * * @param force Whether to force kill the process */ private killProcess(force: boolean = false): void { if (!this.process) return; try { if (force) { this.process.kill('SIGKILL'); } else { this.process.kill('SIGTERM'); } } catch (error) { this.logger.error(`Error killing relay server process: ${error instanceof Error ? error.message : String(error)}`); } } /** * Resolve the executable path * * @returns Resolved executable path */ private resolveExecPath(): string { // Check if it's a relative path if (this.config.execPath && !path.isAbsolute(this.config.execPath)) { return path.resolve(process.cwd(), this.config.execPath); } return this.config.execPath || ''; } /** * Register an event handler * * @param event Event to handle * @param handler Handler function */ public on(event: RelayServerManagerEvent, handler: (...args: any[]) => void): void { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, []); } this.eventHandlers.get(event)!.push(handler); } /** * Emit an event to registered handlers * * @param event Event to emit * @param args Arguments to pass to handlers */ private emitEvent(event: RelayServerManagerEvent, ...args: any[]): void { if (this.eventHandlers.has(event)) { for (const handler of this.eventHandlers.get(event)!) { try { handler(...args); } catch (error) { this.logger.error(`Error in event handler: ${error instanceof Error ? error.message : String(error)}`); } } } } /** * Check if the relay server is running * * @returns True if running, false otherwise */ public isServerRunning(): boolean { return this.isRunning; } }

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/Krekun/vrchat-mcp-osc'

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