UNPKG

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

496 lines (418 loc) • 16.4 kB
import { Page } from 'playwright'; export interface TurboEvent { type: 'visit' | 'cache-miss' | 'frame-load' | 'stream'; target: string; timing: number; timestamp: Date; } export interface StimulusController { identifier: string; element: string; actions: string[]; targets: string[]; connected: boolean; } export interface CSRFIssue { endpoint: string; method: string; hasToken: boolean; tokenName: string; timestamp: Date; } export interface LiveReloadEvent { type: 'css' | 'js' | 'html' | 'partial'; file: string; timestamp: Date; reloadTime: number; } export class ServerFrameworkDebugEngine { private page?: Page; private turboEvents: TurboEvent[] = []; private csrfIssues: CSRFIssue[] = []; private liveReloadEvents: LiveReloadEvent[] = []; async attachToPage(page: Page): Promise<void> { this.page = page; await this.injectServerFrameworkMonitoring(); await this.setupCSRFTracking(); await this.setupFormTracking(); } async detectServerFramework(page: Page): Promise<'rails' | 'django' | null> { const detection = await page.evaluate(() => { // Rails detection const hasRailsUJS = !!(window as any).Rails; const hasTurbo = !!(window as any).Turbo; const hasActionCable = !!(window as any).ActionCable; const railsMeta = document.querySelector('meta[name="csrf-token"]'); const railsData = document.querySelector('[data-rails-form]') || document.querySelector('[data-remote="true"]'); // Django detection const djangoCSRF = document.querySelector('input[name="csrfmiddlewaretoken"]'); const djangoAdmin = document.querySelector('.django-admin-container'); const djangoDebugToolbar = document.getElementById('djDebug'); const djangoMessages = document.querySelector('.django-messages'); if (hasRailsUJS || hasTurbo || hasActionCable || railsMeta || railsData) { return 'rails'; } if (djangoCSRF || djangoAdmin || djangoDebugToolbar || djangoMessages) { return 'django'; } return null; }); return detection; } private async injectServerFrameworkMonitoring(): Promise<void> { if (!this.page) return; await this.page.addInitScript(() => { (window as any).__SERVER_FRAMEWORK_DEBUG__ = { turboEvents: [], stimulusControllers: [], formSubmissions: [], ajaxRequests: [], init: function() { // Monitor Turbo (Rails) if ((window as any).Turbo) { this.monitorTurbo(); } // Monitor Stimulus if ((window as any).Stimulus) { this.monitorStimulus(); } // Monitor Rails UJS if ((window as any).Rails) { this.monitorRailsUJS(); } // Monitor Django forms this.monitorForms(); // Monitor AJAX/Fetch this.monitorAjax(); }, monitorTurbo: function() { const debug = this; document.addEventListener('turbo:visit', (event: any) => { debug.turboEvents.push({ type: 'visit', target: event.detail.url, timing: Date.now(), timestamp: new Date().toISOString() }); }); document.addEventListener('turbo:cache-miss', (event: any) => { debug.turboEvents.push({ type: 'cache-miss', target: window.location.href, timing: Date.now(), timestamp: new Date().toISOString() }); }); document.addEventListener('turbo:frame-load', (event: any) => { debug.turboEvents.push({ type: 'frame-load', target: event.target.id, timing: Date.now(), timestamp: new Date().toISOString() }); }); document.addEventListener('turbo:submit-start', (event: any) => { const form = event.target; debug.formSubmissions.push({ action: form.action, method: form.method, turbo: true, hasFile: form.querySelector('input[type="file"]') !== null, timestamp: new Date().toISOString() }); }); }, monitorStimulus: function() { const debug = this; // Try to access Stimulus application const app = (window as any).Stimulus; if (!app) return; // Monitor controller connections const originalRegister = app.register; app.register = function(identifier: string, controller: any) { debug.stimulusControllers.push({ identifier, controller: controller.name, timestamp: new Date().toISOString() }); return originalRegister.call(this, identifier, controller); }; }, monitorRailsUJS: function() { const debug = this; document.addEventListener('ajax:send', (event: any) => { debug.ajaxRequests.push({ url: event.detail[0].url, method: event.detail[0].type, rails: true, timestamp: new Date().toISOString() }); }); document.addEventListener('ajax:error', (event: any) => { const lastRequest = debug.ajaxRequests[debug.ajaxRequests.length - 1]; if (lastRequest) { lastRequest.error = true; lastRequest.status = event.detail[2].status; } }); }, monitorForms: function() { const debug = this; document.addEventListener('submit', (event: any) => { const form = event.target; if (form.tagName !== 'FORM') return; // Check for CSRF token const railsToken = form.querySelector('input[name="authenticity_token"]'); const djangoToken = form.querySelector('input[name="csrfmiddlewaretoken"]'); debug.formSubmissions.push({ action: form.action, method: form.method, hasCSRF: !!(railsToken || djangoToken), csrfField: railsToken ? 'authenticity_token' : djangoToken ? 'csrfmiddlewaretoken' : null, hasFile: form.querySelector('input[type="file"]') !== null, timestamp: new Date().toISOString() }); }); }, monitorAjax: function() { const debug = this; // Monitor fetch const originalFetch = window.fetch; window.fetch = async function(...args) { const url = typeof args[0] === 'string' ? args[0] : (args[0] as Request).url; const options = args[1] || {}; const request: any = { url, method: options.method || 'GET', hasCSRF: false, timestamp: new Date().toISOString() }; // Check for CSRF in headers if (options.headers) { const headers = options.headers as Record<string, string>; request.hasCSRF = !!(headers['X-CSRF-Token'] || headers['X-CSRFToken']); } debug.ajaxRequests.push(request); try { const response = await originalFetch.apply(this, args); request.status = response.status; return response; } catch (error) { request.error = true; throw error; } }; } }; // Initialize after DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { (window as any).__SERVER_FRAMEWORK_DEBUG__.init(); }); } else { (window as any).__SERVER_FRAMEWORK_DEBUG__.init(); } }); } private async setupCSRFTracking(): Promise<void> { if (!this.page) return; // Monitor network requests for CSRF issues this.page.on('response', async (response) => { const request = response.request(); const method = request.method(); // Only check state-changing methods if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) { const headers = request.headers(); const url = request.url(); // Check for CSRF tokens const hasCSRFHeader = !!( headers['x-csrf-token'] || headers['x-csrftoken'] || headers['x-xsrf-token'] || headers['x-requested-with'] === 'XMLHttpRequest' ); // Check response for CSRF errors if (response.status() === 403) { const text = await response.text().catch(() => ''); if (text.includes('CSRF') || text.includes('csrf')) { this.csrfIssues.push({ endpoint: url, method, hasToken: hasCSRFHeader, tokenName: 'Unknown', timestamp: new Date() }); } } } }); } private async setupFormTracking(): Promise<void> { if (!this.page) return; // Track form submissions for multipart issues await this.page.addInitScript(() => { const originalSubmit = HTMLFormElement.prototype.submit; HTMLFormElement.prototype.submit = function() { const hasFile = this.querySelector('input[type="file"]') !== null; const enctype = this.getAttribute('enctype'); if (hasFile && enctype !== 'multipart/form-data') { console.error('Form with file input missing multipart/form-data enctype'); } return originalSubmit.call(this); }; }); } async getTurboEvents(): Promise<TurboEvent[]> { if (!this.page) return []; const events = await this.page.evaluate(() => { return (window as any).__SERVER_FRAMEWORK_DEBUG__?.turboEvents || []; }); return events.map((e: any) => ({ ...e, timestamp: new Date(e.timestamp) })); } async getStimulusControllers(): Promise<StimulusController[]> { if (!this.page) return []; return await this.page.evaluate(() => { const controllers: StimulusController[] = []; // Find all Stimulus controlled elements document.querySelectorAll('[data-controller]').forEach((element) => { const identifier = element.getAttribute('data-controller') || ''; // Extract actions const actionAttr = element.getAttribute('data-action') || ''; const actions = actionAttr.split(' ').filter(a => a.length > 0); // Extract targets const targets: string[] = []; Array.from(element.attributes).forEach(attr => { if (attr.name.startsWith('data-') && attr.name.endsWith('-target')) { targets.push(attr.value); } }); controllers.push({ identifier, element: element.tagName + (element.id ? `#${element.id}` : ''), actions, targets, connected: true // Assume connected if found in DOM }); }); return controllers; }); } async getFormSubmissions(): Promise<any[]> { if (!this.page) return []; return await this.page.evaluate(() => { return (window as any).__SERVER_FRAMEWORK_DEBUG__?.formSubmissions || []; }); } async getCSRFIssues(): Promise<CSRFIssue[]> { return this.csrfIssues; } async detectServerFrameworkProblems(): Promise<Array<{ problem: string; severity: 'low' | 'medium' | 'high'; description: string; solution: string; }>> { const problems = []; const framework = await this.detectServerFramework(this.page!); // CSRF Issues const csrfIssues = await this.getCSRFIssues(); if (csrfIssues.length > 0) { problems.push({ problem: 'CSRF Token Issues', severity: 'high' as const, description: `${csrfIssues.length} requests failed or missing CSRF tokens.`, solution: framework === 'rails' ? 'Ensure <%= csrf_meta_tags %> in layout and use Rails UJS or include token in AJAX headers.' : 'Include {% csrf_token %} in forms and add token to AJAX requests.' }); } // Form Issues const forms = await this.getFormSubmissions(); const formsWithoutCSRF = forms.filter(f => !f.hasCSRF && f.method.toUpperCase() !== 'GET'); if (formsWithoutCSRF.length > 0) { problems.push({ problem: 'Forms Missing CSRF Protection', severity: 'high' as const, description: `${formsWithoutCSRF.length} forms lack CSRF tokens.`, solution: framework === 'rails' ? 'Add <%= form_with %> or <%= form_tag %> helpers which include CSRF automatically.' : 'Ensure {% csrf_token %} is inside all Django forms.' }); } // Multipart form issues const fileFormsWithoutMultipart = forms.filter(f => f.hasFile && f.enctype !== 'multipart/form-data' ); if (fileFormsWithoutMultipart.length > 0) { problems.push({ problem: 'File Upload Forms Misconfigured', severity: 'high' as const, description: `${fileFormsWithoutMultipart.length} file upload forms missing multipart encoding.`, solution: 'Add enctype="multipart/form-data" to forms with file inputs.' }); } // Turbo/Hotwire Issues (Rails) if (framework === 'rails') { const turboEvents = await this.getTurboEvents(); const cacheMisses = turboEvents.filter(e => e.type === 'cache-miss'); if (cacheMisses.length > turboEvents.filter(e => e.type === 'visit').length * 0.5) { problems.push({ problem: 'Poor Turbo Cache Hit Rate', severity: 'low' as const, description: `Over 50% of Turbo visits are cache misses, reducing navigation speed.`, solution: 'Ensure Turbo cache is enabled and pages are cacheable. Avoid dynamic content in cached pages.' }); } // Check for Stimulus issues const controllers = await this.getStimulusControllers(); const duplicateControllers = controllers.filter((c, i) => controllers.findIndex(c2 => c2.identifier === c.identifier) !== i ); if (duplicateControllers.length > 0) { problems.push({ problem: 'Duplicate Stimulus Controllers', severity: 'medium' as const, description: `Found duplicate controller registrations which may cause conflicts.`, solution: 'Ensure each Stimulus controller is registered only once.' }); } } // AJAX without CSRF const ajaxRequests = await this.page!.evaluate(() => (window as any).__SERVER_FRAMEWORK_DEBUG__?.ajaxRequests || [] ); const unsafeAjax = ajaxRequests.filter((r: any) => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(r.method.toUpperCase()) && !r.hasCSRF ); if (unsafeAjax.length > 0) { problems.push({ problem: 'AJAX Requests Missing CSRF', severity: 'high' as const, description: `${unsafeAjax.length} state-changing AJAX requests lack CSRF tokens.`, solution: framework === 'rails' ? 'Use Rails.ajax() or add X-CSRF-Token header from meta tag.' : 'Add X-CSRFToken header from cookie or hidden input.' }); } return problems; } async getLiveReloadStats(): Promise<{ events: LiveReloadEvent[]; avgReloadTime: number; fileTypes: Record<string, number>; }> { const events = this.liveReloadEvents; const avgReloadTime = events.length > 0 ? events.reduce((sum, e) => sum + e.reloadTime, 0) / events.length : 0; const fileTypes: Record<string, number> = {}; events.forEach(e => { fileTypes[e.type] = (fileTypes[e.type] || 0) + 1; }); return { events, avgReloadTime, fileTypes }; } }