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
text/typescript
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 };
}
}