ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
490 lines • 19.5 kB
JavaScript
/**
* Intelligent Server Readiness & Auto-Startup System
*
* Addresses the common AI pattern where inject_debugging is attempted before
* the development server is running, causing inefficient retry cycles.
*
* Features:
* - Proactive server readiness detection
* - Intelligent auto-startup with framework detection
* - Development server health monitoring
* - Zero-wait injection through predictive readiness
*/
import { exec, spawn } from 'child_process';
import { promisify } from 'util';
import { UserFriendlyLogger } from './user-friendly-logger.js';
import * as fs from 'fs/promises';
import * as path from 'path';
const execAsync = promisify(exec);
export class IntelligentServerReadiness {
logger;
frameworkConfigs;
serverProcesses;
readinessCache;
constructor() {
this.logger = new UserFriendlyLogger('ServerReadiness');
this.frameworkConfigs = new Map();
this.serverProcesses = new Map();
this.readinessCache = new Map();
this.initializeFrameworkConfigs();
}
/**
* Main method: Check if server is ready, auto-start if needed
*/
async ensureServerReadiness(url, options = {}) {
const startTime = performance.now();
const { autoStart = true, maxWaitMs = 30000, framework = 'auto' } = options;
try {
// Step 1: Quick readiness check (cached if available)
let readinessResult = await this.checkServerReadiness(url);
if (readinessResult.isReady) {
return {
...readinessResult,
readinessTimeMs: performance.now() - startTime,
recommendedAction: 'proceed'
};
}
// Step 2: Detect framework if not specified
const detectedFramework = framework === 'auto'
? await this.detectProjectFramework(options.workingDirectory)
: framework;
// Step 3: Auto-start server if enabled and framework detected
if (autoStart && detectedFramework && detectedFramework !== 'unknown') {
this.logger.info(`🚀 Auto-starting ${detectedFramework} development server...`);
const startupResult = await this.autoStartDevelopmentServer(detectedFramework, url, options.workingDirectory);
if (startupResult.success) {
// Wait for server to be ready
readinessResult = await this.waitForServerReadiness(url, maxWaitMs - (performance.now() - startTime));
return {
...readinessResult,
autoStarted: true,
readinessTimeMs: performance.now() - startTime,
recommendedAction: readinessResult.isReady ? 'proceed' : 'wait',
serverInfo: startupResult.serverInfo
};
}
}
// Step 4: Provide guidance if auto-start failed or disabled
const recommendedAction = this.getRecommendedAction(detectedFramework, readinessResult);
return {
...readinessResult,
framework: detectedFramework,
readinessTimeMs: performance.now() - startTime,
recommendedAction
};
}
catch (error) {
this.logger.error(`Server readiness check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
return {
isReady: false,
url,
framework: 'unknown',
autoStarted: false,
readinessTimeMs: performance.now() - startTime,
recommendedAction: 'check_config'
};
}
}
/**
* Check if server is ready to accept debugging connections
*/
async checkServerReadiness(url) {
// Check cache first
const cached = this.readinessCache.get(url);
if (cached && (Date.now() - cached.timestamp) < cached.ttl) {
return {
isReady: cached.ready,
url,
framework: 'cached',
autoStarted: false
};
}
try {
// Extract port from URL
const urlObj = new URL(url);
const port = parseInt(urlObj.port) || (urlObj.protocol === 'https:' ? 443 : 80);
// Check if port is listening
const isListening = await this.isPortListening(port);
if (!isListening) {
this.cacheReadinessResult(url, false, 5000); // Cache failure for 5s
return { isReady: false, url, framework: 'unknown', autoStarted: false };
}
// Try HTTP request to verify server responds
const isResponding = await this.testHttpResponse(url);
// Cache result
this.cacheReadinessResult(url, isResponding, isResponding ? 30000 : 5000);
return {
isReady: isResponding,
url,
framework: 'unknown', // Will be detected separately
autoStarted: false
};
}
catch (error) {
this.cacheReadinessResult(url, false, 5000);
return { isReady: false, url, framework: 'unknown', autoStarted: false };
}
}
/**
* Auto-start development server based on detected framework
*/
async autoStartDevelopmentServer(framework, targetUrl, workingDirectory) {
const config = this.frameworkConfigs.get(framework);
if (!config) {
return { success: false, error: `No configuration found for framework: ${framework}` };
}
const urlObj = new URL(targetUrl);
const targetPort = parseInt(urlObj.port) || config.defaultPort;
const workDir = workingDirectory || process.cwd();
try {
// Find the best start command for the detected framework
const startCommand = await this.selectBestStartCommand(config, workDir);
if (!startCommand) {
return { success: false, error: 'No suitable start command found' };
}
this.logger.info(`⚡ Starting ${framework} server with: ${startCommand}`);
// Start the server process
const serverProcess = await this.spawnServerProcess(startCommand, workDir, targetPort);
if (serverProcess) {
const serverInfo = {
pid: serverProcess.pid,
port: targetPort,
status: 'starting',
startupTimeMs: Date.now()
};
// Track the process
this.serverProcesses.set(targetUrl, {
pid: serverInfo.pid,
port: serverInfo.port,
status: 'starting',
startTime: Date.now()
});
// Give the server a moment to start
await new Promise(resolve => setTimeout(resolve, 2000));
return { success: true, serverInfo: {
pid: serverInfo.pid,
port: serverInfo.port,
status: serverInfo.status,
startupTimeMs: serverInfo.startupTimeMs
} };
}
return { success: false, error: 'Failed to spawn server process' };
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown startup error'
};
}
}
/**
* Wait for server to become ready with intelligent polling
*/
async waitForServerReadiness(url, maxWaitMs) {
const startTime = Date.now();
const pollInterval = 500; // Check every 500ms
while ((Date.now() - startTime) < maxWaitMs) {
const readinessCheck = await this.checkServerReadiness(url);
if (readinessCheck.isReady) {
this.logger.success(`✅ Server ready after ${Date.now() - startTime}ms`);
return readinessCheck;
}
// Exponential backoff for polling
const waitTime = Math.min(pollInterval * Math.pow(1.2, (Date.now() - startTime) / 1000), 2000);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.logger.warn(`⏰ Server readiness timeout after ${maxWaitMs}ms`);
return { isReady: false, url, framework: 'timeout', autoStarted: false };
}
/**
* Detect project framework from file system
*/
async detectProjectFramework(workingDirectory) {
const workDir = workingDirectory || process.cwd();
try {
// Check for framework-specific files
for (const [framework, config] of this.frameworkConfigs) {
for (const detectionFile of config.detectionFiles) {
const filePath = path.join(workDir, detectionFile);
try {
await fs.access(filePath);
this.logger.info(`📦 Detected ${framework} framework (found ${detectionFile})`);
return framework;
}
catch {
// File doesn't exist, continue checking
}
}
}
// Check package.json for additional clues
const packageJsonPath = path.join(workDir, 'package.json');
try {
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
const frameworkFromDeps = this.detectFrameworkFromPackageJson(packageJson);
if (frameworkFromDeps) {
this.logger.info(`📦 Detected ${frameworkFromDeps} from package.json dependencies`);
return frameworkFromDeps;
}
}
catch {
// No package.json or parsing error
}
return 'unknown';
}
catch (error) {
this.logger.warn(`Failed to detect framework: ${error instanceof Error ? error.message : 'Unknown error'}`);
return 'unknown';
}
}
/**
* Initialize framework configurations
*/
initializeFrameworkConfigs() {
const configs = [
{
framework: 'react',
detectionFiles: ['src/App.js', 'src/App.tsx', 'public/index.html'],
startCommands: ['npm start', 'yarn start', 'pnpm start'],
defaultPort: 3000,
readinessPath: '/',
startupTimeoutMs: 15000,
healthCheckEndpoints: ['/', '/static/js/', '/favicon.ico']
},
{
framework: 'next.js',
detectionFiles: ['next.config.js', 'next.config.mjs', 'pages/', 'app/'],
startCommands: ['npm run dev', 'yarn dev', 'pnpm dev', 'next dev'],
defaultPort: 3000,
readinessPath: '/',
startupTimeoutMs: 20000,
healthCheckEndpoints: ['/', '/_next/', '/api/']
},
{
framework: 'vue',
detectionFiles: ['src/main.js', 'src/main.ts', 'vue.config.js'],
startCommands: ['npm run serve', 'yarn serve', 'pnpm serve', 'npm run dev'],
defaultPort: 8080,
readinessPath: '/',
startupTimeoutMs: 15000,
healthCheckEndpoints: ['/', '/js/', '/css/']
},
{
framework: 'angular',
detectionFiles: ['angular.json', 'src/main.ts', '.angular-cli.json'],
startCommands: ['ng serve', 'npm start', 'yarn start'],
defaultPort: 4200,
readinessPath: '/',
startupTimeoutMs: 25000,
healthCheckEndpoints: ['/', '/main.js', '/polyfills.js']
},
{
framework: 'svelte',
detectionFiles: ['svelte.config.js', 'src/main.js', 'rollup.config.js'],
startCommands: ['npm run dev', 'yarn dev', 'pnpm dev'],
defaultPort: 5000,
readinessPath: '/',
startupTimeoutMs: 12000,
healthCheckEndpoints: ['/', '/build/', '/global.css']
},
{
framework: 'flutter',
detectionFiles: ['pubspec.yaml', 'lib/main.dart', 'web/index.html'],
startCommands: ['flutter run -d web-server', 'flutter run -d chrome'],
defaultPort: 3000,
readinessPath: '/',
startupTimeoutMs: 30000,
healthCheckEndpoints: ['/', '/main.dart.js', '/flutter.js']
},
{
framework: 'phoenix',
detectionFiles: ['mix.exs', 'config/config.exs', 'lib/'],
startCommands: ['mix phx.server', 'iex -S mix phx.server'],
defaultPort: 4000,
readinessPath: '/',
startupTimeoutMs: 20000,
healthCheckEndpoints: ['/', '/assets/', '/phoenix/']
}
];
for (const config of configs) {
this.frameworkConfigs.set(config.framework, config);
}
}
/**
* Select the best start command for the detected environment
*/
async selectBestStartCommand(config, workDir) {
// Check which package managers are available
const packageManagers = await this.detectAvailablePackageManagers(workDir);
for (const command of config.startCommands) {
// Check if the command is available
if (command.startsWith('npm') && packageManagers.includes('npm')) {
return command;
}
if (command.startsWith('yarn') && packageManagers.includes('yarn')) {
return command;
}
if (command.startsWith('pnpm') && packageManagers.includes('pnpm')) {
return command;
}
if (command.startsWith('flutter') && await this.isCommandAvailable('flutter')) {
return command;
}
if (command.startsWith('mix') && await this.isCommandAvailable('mix')) {
return command;
}
if (command.startsWith('ng') && await this.isCommandAvailable('ng')) {
return command;
}
}
return null;
}
/**
* Spawn server process with proper error handling
*/
async spawnServerProcess(command, workDir, port) {
const [cmd, ...args] = command.split(' ');
const serverProcess = spawn(cmd, args, {
cwd: workDir,
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
PORT: port.toString(),
NODE_ENV: 'development'
}
});
// Capture startup output for debugging
let startupOutput = '';
serverProcess.stdout?.on('data', (data) => {
startupOutput += data.toString();
});
serverProcess.stderr?.on('data', (data) => {
startupOutput += data.toString();
});
// Handle process errors
serverProcess.on('error', (error) => {
this.logger.error(`Server process error: ${error.message}`);
});
return serverProcess;
}
// Helper methods
async isPortListening(port) {
try {
const { stdout } = await execAsync(`lsof -i :${port}`);
return stdout.trim().length > 0;
}
catch {
return false;
}
}
async testHttpResponse(url) {
try {
const response = await fetch(url, {
method: 'GET',
signal: AbortSignal.timeout(5000)
});
return response.status < 500; // Accept any non-server-error status
}
catch {
return false;
}
}
cacheReadinessResult(url, ready, ttlMs) {
this.readinessCache.set(url, {
ready,
timestamp: Date.now(),
ttl: ttlMs
});
}
async detectAvailablePackageManagers(workDir) {
const managers = [];
const checks = [
{ name: 'npm', file: 'package-lock.json' },
{ name: 'yarn', file: 'yarn.lock' },
{ name: 'pnpm', file: 'pnpm-lock.yaml' }
];
for (const { name, file } of checks) {
try {
await fs.access(path.join(workDir, file));
managers.push(name);
}
catch {
// File doesn't exist
}
}
// If no lock files found, check if commands are available
if (managers.length === 0) {
for (const name of ['npm', 'yarn', 'pnpm']) {
if (await this.isCommandAvailable(name)) {
managers.push(name);
}
}
}
return managers;
}
async isCommandAvailable(command) {
try {
await execAsync(`which ${command}`);
return true;
}
catch {
return false;
}
}
detectFrameworkFromPackageJson(packageJson) {
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
if (dependencies['next'])
return 'next.js';
if (dependencies['react'])
return 'react';
if (dependencies['vue'])
return 'vue';
if (dependencies['@angular/core'])
return 'angular';
if (dependencies['svelte'])
return 'svelte';
if (dependencies['phoenix'])
return 'phoenix';
return null;
}
getRecommendedAction(framework, readinessResult) {
if (readinessResult.isReady)
return 'proceed';
if (framework === 'unknown')
return 'check_config';
return 'start_server';
}
/**
* Get current server readiness metrics
*/
getReadinessMetrics() {
const activeProcesses = Array.from(this.serverProcesses.values()).length;
const avgStartupTime = activeProcesses > 0
? Array.from(this.serverProcesses.values())
.reduce((sum, proc) => sum + (Date.now() - proc.startTime), 0) / activeProcesses
: 0;
return {
cachedUrls: this.readinessCache.size,
activeProcesses,
frameworksSupported: this.frameworkConfigs.size,
averageStartupTime: avgStartupTime
};
}
/**
* Cleanup server processes
*/
async cleanup() {
for (const [url, processInfo] of this.serverProcesses) {
try {
process.kill(processInfo.pid, 'SIGTERM');
this.logger.info(`🛑 Stopped server process ${processInfo.pid} for ${url}`);
}
catch (error) {
// Process might already be stopped
}
}
this.serverProcesses.clear();
this.readinessCache.clear();
}
}
//# sourceMappingURL=intelligent-server-readiness.js.map