UNPKG

@iflow-mcp/ejmockler-brutalist

Version:

Deploy Claude, Codex & Gemini CLI agents to demolish your work before users do. Real file analysis. Brutal honesty. Now with conversation continuation & intelligent pagination.

297 lines 10 kB
import { BrutalistServer } from '../brutalist-server.js'; import { logger } from '../logger.js'; /** * Harness for deterministic server lifecycle management in tests. * Provides event-based readiness detection and proper cleanup. */ export class ServerHarness { options; server = null; actualPort; baseUrl; startTime; httpServer; sessionId; constructor(options = {}) { this.options = options; this.options = { maxStartupTime: 30000, healthCheckInterval: 100, shutdownTimeout: 5000, ...options }; } /** * Start server and wait for it to be ready (not just started) */ async start(config = {}) { if (this.server) { throw new Error('Server already started'); } this.startTime = Date.now(); logger.info('ServerHarness: Starting server...'); try { // Create server instance this.server = new BrutalistServer({ ...config, httpPort: config.httpPort ?? 0 // Use 0 for random port if not specified }); // Start the server await this.server.start(); // Get actual port for HTTP transport if (config.transport === 'http' || !config.transport) { this.actualPort = this.server.getActualPort(); if (!this.actualPort) { throw new Error('Failed to get actual server port'); } this.baseUrl = `http://localhost:${this.actualPort}`; // Wait for HTTP server to be ready await this.waitForHttpReady(); // Initialize MCP connection await this.initializeMCP(); } const startupTime = Date.now() - this.startTime; logger.info(`ServerHarness: Server ready in ${startupTime}ms on port ${this.actualPort}`); } catch (error) { // Cleanup on startup failure logger.error('ServerHarness: Startup failed:', error); this.cleanup(); throw error; } } /** * Wait for HTTP server to respond to health checks */ async waitForHttpReady() { const deadline = Date.now() + this.options.maxStartupTime; let lastError; while (Date.now() < deadline) { try { const response = await fetch(`${this.baseUrl}/health`); if (response.ok) { const data = await response.json(); if (data.status === 'ok') { logger.debug('ServerHarness: Health check passed'); return; } } } catch (error) { lastError = error; // Server not ready yet, keep trying } await new Promise(resolve => setTimeout(resolve, this.options.healthCheckInterval)); } throw new Error(`Server failed to become ready within ${this.options.maxStartupTime}ms: ${lastError?.message}`); } /** * Parse SSE response format to extract JSON data */ parseSSEResponse(responseText) { const lines = responseText.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { return JSON.parse(line.substring(6)); // Remove "data: " prefix } catch (e) { // Continue looking for valid JSON } } } throw new Error(`Failed to parse SSE response: ${responseText}`); } /** * Initialize MCP connection with handshake */ async initializeMCP() { const initRequest = { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: { tools: {}, logging: {} // Add logging capability to support notifications }, clientInfo: { name: 'test-client', version: '1.0.0' } } }; const response = await this.testRequest('/mcp', { method: 'POST', body: JSON.stringify(initRequest) }); // Handle SSE response format const responseText = await response.text(); const jsonData = this.parseSSEResponse(responseText); if (jsonData.error) { throw new Error(`MCP initialization failed: ${JSON.stringify(jsonData.error)}`); } // Extract session ID from response headers const sessionIdHeader = response.headers.get('mcp-session-id'); if (sessionIdHeader) { this.sessionId = sessionIdHeader; logger.debug(`MCP session ID: ${this.sessionId}`); } logger.debug('MCP server initialized successfully'); } /** * Stop server with graceful shutdown and forced kill if needed */ async stop() { if (!this.server) { return; } logger.info('ServerHarness: Stopping server...'); const stopStart = Date.now(); try { // Try graceful shutdown first if (this.httpServer) { await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Server shutdown timed out')); }, this.options.shutdownTimeout); this.httpServer.close((err) => { clearTimeout(timeout); if (err) reject(err); else resolve(); }); }); } // Additional server cleanup if needed if (this.server) { await this.server.cleanup(); } const stopTime = Date.now() - stopStart; logger.info(`ServerHarness: Server stopped in ${stopTime}ms`); } catch (error) { logger.error('ServerHarness: Graceful shutdown failed, forcing stop:', error); // Force cleanup this.httpServer = undefined; } finally { this.server = null; this.actualPort = undefined; this.baseUrl = undefined; } } /** * Reset server state (for cleanup after failed startup) */ cleanup() { this.server = null; this.actualPort = undefined; this.baseUrl = undefined; this.httpServer = undefined; this.sessionId = undefined; this.startTime = undefined; } /** * Get the actual port the server is listening on */ getPort() { if (!this.actualPort) { throw new Error('Server not started or port not available'); } return this.actualPort; } /** * Get the base URL for HTTP requests */ getBaseUrl() { if (!this.baseUrl) { throw new Error('Server not started or not using HTTP transport'); } return this.baseUrl; } /** * Get the server instance for direct access if needed */ getServer() { if (!this.server) { throw new Error('Server not started'); } return this.server; } /** * Check if server is currently running */ isRunning() { return this.server !== null; } /** * Make a test request to the server */ async testRequest(path, options = {}) { if (!this.baseUrl) { throw new Error('Server not started or not using HTTP transport'); } const url = `${this.baseUrl}${path}`; const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', ...options.headers }; // Add session ID if available and this is an MCP request if (this.sessionId && path === '/mcp') { headers['Mcp-Session-Id'] = this.sessionId; } const response = await fetch(url, { ...options, headers }); if (!response.ok && !options.allowFailure) { const text = await response.text(); throw new Error(`Request failed: ${response.status} ${response.statusText}\n${text}`); } return response; } /** * Execute an MCP tool via HTTP */ async executeTool(toolName, args, progressToken) { const request = { jsonrpc: '2.0', id: Date.now(), method: 'tools/call', params: { name: toolName, arguments: args, _meta: progressToken ? { progressToken } : undefined } }; const response = await this.testRequest('/mcp', { method: 'POST', body: JSON.stringify(request) }); // Handle SSE response format const responseText = await response.text(); const jsonData = this.parseSSEResponse(responseText); if (jsonData.error) { throw new Error(`Tool execution failed: ${JSON.stringify(jsonData.error)}`); } return jsonData.result; } /** * Get diagnostic information about the server */ getDiagnostics() { const lines = ['ServerHarness diagnostics:']; lines.push(` Running: ${this.isRunning()}`); if (this.server) { lines.push(` Port: ${this.actualPort}`); lines.push(` Base URL: ${this.baseUrl}`); lines.push(` Uptime: ${Date.now() - this.startTime}ms`); } return lines.join('\n'); } } //# sourceMappingURL=server-harness.js.map