Skip to main content
Glama

MCP Console Automation Server

by ooples
test-ssh-crash-scenarios.ts16.9 kB
#!/usr/bin/env node /** * SSH Crash Test Scenarios for MCP Console Automation Server * * This test harness systematically reproduces SSH errors that can cause * the MCP server to crash, and verifies that our fixes work correctly. * * Test Categories: * 1. SSH Connection with invalid host (should fail fast) * 2. SSH Connection with wrong credentials (should fail fast) * 3. SSH executable not found (ENOENT error) * 4. SSH process dying unexpectedly * 5. Multiple simultaneous SSH errors */ import { spawn, ChildProcess } from 'child_process'; import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs'; import { join } from 'path'; interface TestResult { testName: string; success: boolean; error?: string; serverCrashed: boolean; debugLogOutput: string[]; mcpResponseReceived: boolean; processPid?: number; executionTimeMs: number; } interface TestScenario { name: string; description: string; sshOptions: any; expectedBehavior: string; expectedInDebugLog: string[]; shouldServerSurvive: boolean; } export class SSHCrashTestHarness { private serverProcess?: ChildProcess; private serverPid?: number; private debugLogFile = 'C:\\Users\\yolan\\source\\repos\\mcp-console-automation\\mcp-debug.log'; private testLogFile = 'C:\\Users\\yolan\\source\\repos\\mcp-console-automation\\ssh-crash-test.log'; private testResults: TestResult[] = []; private startTime = Date.now(); constructor() { this.initializeTestEnvironment(); } private initializeTestEnvironment() { // Clear previous test logs if (existsSync(this.debugLogFile)) { writeFileSync(this.debugLogFile, ''); } writeFileSync(this.testLogFile, `SSH Crash Test Started: ${new Date().toISOString()}\n`); console.log('🔧 Initializing SSH Crash Test Environment...'); } private log(message: string) { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}\n`; appendFileSync(this.testLogFile, logMessage); console.log(message); } /** * Test Scenario 1: SSH Connection with Invalid Host */ private getInvalidHostScenario(): TestScenario { return { name: 'invalid_host', description: 'SSH connection to non-existent host', sshOptions: { host: 'invalid-host-that-does-not-exist-12345.local', username: 'testuser', password: 'testpass', port: 22 }, expectedBehavior: 'Should fail fast with connection timeout, server must continue running', expectedInDebugLog: [ 'SSH connection attempt', 'Connection timeout', 'SSH/Network error isolated', 'Server MUST continue' ], shouldServerSurvive: true }; } /** * Test Scenario 2: SSH Connection with Wrong Credentials */ private getWrongCredentialsScenario(): TestScenario { return { name: 'wrong_credentials', description: 'SSH connection with invalid credentials', sshOptions: { host: 'localhost', username: 'invalid_user_999', password: 'wrong_password_999', port: 22 }, expectedBehavior: 'Should fail fast with authentication error, server must continue', expectedInDebugLog: [ 'SSH authentication', 'Authentication failed', 'SSH/Network error isolated', 'Check SSH credentials' ], shouldServerSurvive: true }; } /** * Test Scenario 3: SSH Executable Not Found (ENOENT) */ private getExecutableNotFoundScenario(): TestScenario { return { name: 'ssh_enoent', description: 'SSH executable not found error', sshOptions: { host: 'localhost', username: 'testuser', password: 'testpass', sshExecutable: '/non/existent/ssh/path' // Force ENOENT }, expectedBehavior: 'Should handle ENOENT gracefully, server must continue', expectedInDebugLog: [ 'ENOENT', 'SSH client not found', 'trying alternative', 'SSH/Network error isolated' ], shouldServerSurvive: true }; } /** * Test Scenario 4: SSH Process Dies Unexpectedly */ private getProcessDiesScenario(): TestScenario { return { name: 'ssh_process_dies', description: 'SSH process terminates unexpectedly', sshOptions: { host: 'localhost', username: 'testuser', password: 'testpass', killAfterConnect: true // Custom flag for test }, expectedBehavior: 'Should recover from process death, server must continue', expectedInDebugLog: [ 'SSH process terminated', 'Connection lost', 'Recovery attempt', 'SSH/Network error isolated' ], shouldServerSurvive: true }; } /** * Test Scenario 5: Multiple Simultaneous SSH Errors */ private getMultipleErrorsScenario(): TestScenario { return { name: 'multiple_ssh_errors', description: 'Multiple SSH errors happening simultaneously', sshOptions: { concurrent: true, sessions: [ { host: 'invalid1.local', username: 'user1' }, { host: 'invalid2.local', username: 'user2' }, { host: 'invalid3.local', username: 'user3' } ] }, expectedBehavior: 'Should handle multiple errors without crashing', expectedInDebugLog: [ 'Multiple SSH errors', 'Error isolation active', 'SSH/Network error isolated', 'Server MUST continue' ], shouldServerSurvive: true }; } /** * Start the MCP server process */ private async startMCPServer(): Promise<boolean> { return new Promise((resolve) => { this.log('🚀 Starting MCP Server...'); const serverScript = 'C:\\Users\\yolan\\source\\repos\\mcp-console-automation\\dist\\mcp\\server.js'; this.serverProcess = spawn('node', [serverScript], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, MCP_SERVER_MODE: 'true', DEBUG_MODE: 'true' } }); this.serverPid = this.serverProcess.pid; this.log(`📌 Server PID: ${this.serverPid}`); // Give server time to start setTimeout(() => { if (this.serverProcess && !this.serverProcess.killed) { this.log('✅ MCP Server started successfully'); resolve(true); } else { this.log('❌ MCP Server failed to start'); resolve(false); } }, 2000); this.serverProcess.on('error', (error) => { this.log(`❌ Server Error: ${error.message}`); }); this.serverProcess.on('exit', (code, signal) => { this.log(`⚠️ Server Exited: code=${code}, signal=${signal}`); }); }); } /** * Check if MCP server is still running */ private isServerRunning(): boolean { if (!this.serverProcess || !this.serverPid) return false; try { // Check if process exists process.kill(this.serverPid, 0); return true; } catch (error) { return false; } } /** * Send MCP request to create SSH session */ private async sendSSHSessionRequest(scenario: TestScenario): Promise<{ success: boolean; response?: any; error?: string }> { return new Promise((resolve) => { if (!this.serverProcess) { resolve({ success: false, error: 'Server not running' }); return; } const request = { jsonrpc: '2.0', id: Date.now(), method: 'tools/call', params: { name: 'console_create_session', arguments: { command: 'echo "test"', consoleType: 'ssh', sshOptions: scenario.sshOptions, timeout: 5000 } } }; const requestStr = JSON.stringify(request) + '\n'; this.log(`📤 Sending request for ${scenario.name}: ${requestStr}`); let responseReceived = false; const timeout = setTimeout(() => { if (!responseReceived) { resolve({ success: false, error: 'Request timeout' }); } }, 10000); // Listen for response const responseHandler = (data: Buffer) => { if (responseReceived) return; responseReceived = true; clearTimeout(timeout); const response = data.toString(); this.log(`📥 Response received: ${response}`); resolve({ success: true, response }); }; this.serverProcess.stdout?.once('data', responseHandler); // Send request this.serverProcess.stdin?.write(requestStr); }); } /** * Read and analyze debug log */ private readDebugLog(): string[] { try { if (!existsSync(this.debugLogFile)) return []; const content = readFileSync(this.debugLogFile, 'utf8'); return content.split('\n').filter(line => line.trim()); } catch (error) { this.log(`⚠️ Could not read debug log: ${error}`); return []; } } /** * Execute a single test scenario */ private async executeTestScenario(scenario: TestScenario): Promise<TestResult> { const testStartTime = Date.now(); this.log(`\n🧪 Executing Test: ${scenario.name}`); this.log(`📝 Description: ${scenario.description}`); this.log(`🎯 Expected: ${scenario.expectedBehavior}`); const result: TestResult = { testName: scenario.name, success: false, serverCrashed: false, debugLogOutput: [], mcpResponseReceived: false, processPid: this.serverPid, executionTimeMs: 0 }; try { // Clear debug log before test writeFileSync(this.debugLogFile, ''); // Check server is running before test const serverRunningBefore = this.isServerRunning(); if (!serverRunningBefore) { throw new Error('Server not running before test'); } // Send SSH request const requestResult = await this.sendSSHSessionRequest(scenario); result.mcpResponseReceived = requestResult.success; // Wait a bit for logs to be written await new Promise(resolve => setTimeout(resolve, 2000)); // Check server is still running after test const serverRunningAfter = this.isServerRunning(); result.serverCrashed = !serverRunningAfter; // Read debug log result.debugLogOutput = this.readDebugLog(); // Analyze results const expectedLogsFound = scenario.expectedInDebugLog.every(expected => result.debugLogOutput.some(line => line.toLowerCase().includes(expected.toLowerCase())) ); result.success = result.mcpResponseReceived && !result.serverCrashed && expectedLogsFound && scenario.shouldServerSurvive; this.log(`📊 Test Results:`); this.log(` ✅ Response Received: ${result.mcpResponseReceived}`); this.log(` ✅ Server Survived: ${!result.serverCrashed}`); this.log(` ✅ Expected Logs Found: ${expectedLogsFound}`); this.log(` 🎯 Overall Success: ${result.success}`); } catch (error) { result.error = error instanceof Error ? error.message : String(error); this.log(`❌ Test Error: ${result.error}`); } result.executionTimeMs = Date.now() - testStartTime; return result; } /** * Execute multiple error scenario (special case) */ private async executeMultipleErrorsTest(): Promise<TestResult> { const testStartTime = Date.now(); this.log(`\n🧪 Executing Multiple SSH Errors Test`); const result: TestResult = { testName: 'multiple_ssh_errors', success: false, serverCrashed: false, debugLogOutput: [], mcpResponseReceived: false, processPid: this.serverPid, executionTimeMs: 0 }; try { writeFileSync(this.debugLogFile, ''); // Send multiple concurrent SSH requests const scenarios = [ this.getInvalidHostScenario(), this.getWrongCredentialsScenario(), this.getExecutableNotFoundScenario() ]; const promises = scenarios.map(scenario => this.sendSSHSessionRequest(scenario)); const results = await Promise.allSettled(promises); result.mcpResponseReceived = results.some(r => r.status === 'fulfilled'); // Wait for all processing await new Promise(resolve => setTimeout(resolve, 5000)); result.serverCrashed = !this.isServerRunning(); result.debugLogOutput = this.readDebugLog(); const hasErrorIsolation = result.debugLogOutput.some(line => line.includes('SSH/Network error isolated') ); result.success = result.mcpResponseReceived && !result.serverCrashed && hasErrorIsolation; this.log(`📊 Multiple Errors Test Results:`); this.log(` ✅ Some Responses: ${result.mcpResponseReceived}`); this.log(` ✅ Server Survived: ${!result.serverCrashed}`); this.log(` ✅ Error Isolation: ${hasErrorIsolation}`); this.log(` 🎯 Overall Success: ${result.success}`); } catch (error) { result.error = error instanceof Error ? error.message : String(error); this.log(`❌ Multiple Errors Test Error: ${result.error}`); } result.executionTimeMs = Date.now() - testStartTime; return result; } /** * Stop the MCP server */ private stopMCPServer() { if (this.serverProcess) { this.log('🛑 Stopping MCP Server...'); this.serverProcess.kill('SIGTERM'); setTimeout(() => { if (this.serverProcess && !this.serverProcess.killed) { this.log('🔨 Force killing server...'); this.serverProcess.kill('SIGKILL'); } }, 5000); } } /** * Generate detailed test report */ private generateTestReport() { const totalTime = Date.now() - this.startTime; const passedTests = this.testResults.filter(r => r.success).length; const failedTests = this.testResults.length - passedTests; const serverCrashes = this.testResults.filter(r => r.serverCrashed).length; const report = ` === SSH Crash Test Report === Generated: ${new Date().toISOString()} Total Execution Time: ${totalTime}ms 📊 Summary: - Total Tests: ${this.testResults.length} - Passed: ${passedTests} - Failed: ${failedTests} - Server Crashes: ${serverCrashes} 🧪 Individual Test Results: ${this.testResults.map(result => ` Test: ${result.testName} Success: ${result.success ? '✅' : '❌'} Server Crashed: ${result.serverCrashed ? '💥' : '✅'} Response Received: ${result.mcpResponseReceived ? '✅' : '❌'} Execution Time: ${result.executionTimeMs}ms ${result.error ? `Error: ${result.error}` : ''} Debug Log Entries: ${result.debugLogOutput.length} ---`).join('\n')} 🔍 Debug Log Analysis: ${this.testResults.map(result => ` ${result.testName} Debug Output: ${result.debugLogOutput.slice(-10).map(line => ` ${line}`).join('\n')} ---`).join('\n')} ✅ Recommendations: ${serverCrashes === 0 ? '- All tests passed! Server properly isolates SSH errors.' : `- ${serverCrashes} tests caused server crashes. Fix error isolation.`} ${passedTests === this.testResults.length ? '- Error handling is working correctly.' : '- Some tests failed. Review error classification and recovery.'} `; writeFileSync('C:\\Users\\yolan\\source\\repos\\mcp-console-automation\\ssh-crash-test-report.txt', report); this.log(report); } /** * Run all SSH crash test scenarios */ public async runAllTests(): Promise<void> { this.log('🎬 Starting SSH Crash Test Suite...'); try { // Start server const serverStarted = await this.startMCPServer(); if (!serverStarted) { this.log('❌ Could not start MCP server. Aborting tests.'); return; } // Test scenarios const scenarios = [ this.getInvalidHostScenario(), this.getWrongCredentialsScenario(), this.getExecutableNotFoundScenario(), this.getProcessDiesScenario() ]; // Execute individual tests for (const scenario of scenarios) { const result = await this.executeTestScenario(scenario); this.testResults.push(result); // If server crashed, restart it if (result.serverCrashed) { this.log('💥 Server crashed! Restarting for next test...'); this.stopMCPServer(); await new Promise(resolve => setTimeout(resolve, 2000)); await this.startMCPServer(); } } // Execute multiple errors test const multipleErrorsResult = await this.executeMultipleErrorsTest(); this.testResults.push(multipleErrorsResult); } finally { this.stopMCPServer(); this.generateTestReport(); } } } // Main execution if (import.meta.url === `file://${process.argv[1]}`) { const testHarness = new SSHCrashTestHarness(); testHarness.runAllTests().catch(console.error); }

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/ooples/mcp-console-automation'

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