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

452 lines (431 loc) 17.6 kB
import { BaseToolHandler } from './base-handler.js'; import { TidewaveIntegration } from '../tidewave-integration.js'; /** * Handler for Ecto database debugging tools */ export class EctoHandler extends BaseToolHandler { tidewave; constructor() { super(); this.tidewave = new TidewaveIntegration(); } tools = [ { name: 'ecto_inspect_repo_config', description: 'Inspect runtime Ecto repository configuration', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, repoModule: { type: 'string', description: 'Repository module name (e.g., "MyApp.Repo")' } }, required: ['sessionId', 'repoModule'] } }, { name: 'ecto_trace_queries', description: 'Trace all database queries with execution time and source', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, duration: { type: 'number', description: 'Trace duration in seconds', default: 30 }, minTime: { type: 'number', description: 'Minimum query time to capture (ms)', default: 0 } }, required: ['sessionId'] } }, { name: 'ecto_analyze_n_plus_one', description: 'Detect N+1 query problems in your application', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, action: { type: 'string', description: 'Controller action or LiveView to analyze' }, threshold: { type: 'number', description: 'Query count threshold for detection', default: 5 } }, required: ['sessionId', 'action'] } }, { name: 'ecto_inspect_changesets', description: 'Debug Ecto changeset validation and errors', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, module: { type: 'string', description: 'Schema module (e.g., "User")' }, action: { type: 'string', description: 'Changeset function (e.g., "registration_changeset")' }, params: { type: 'object', description: 'Parameters to test with' } }, required: ['sessionId', 'module'] } }, { name: 'ecto_migration_status', description: 'Check migration status and pending migrations', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, repo: { type: 'string', description: 'Repository module', default: 'MyApp.Repo' } }, required: ['sessionId'] } }, { name: 'ecto_query_analyzer', description: 'Analyze query performance with EXPLAIN', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, query: { type: 'string', description: 'Ecto query or raw SQL' }, format: { type: 'string', enum: ['text', 'json', 'yaml'], default: 'text' } }, required: ['sessionId', 'query'] } }, { name: 'ecto_connection_pool_status', description: 'Monitor database connection pool health', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, repo: { type: 'string', description: 'Repository module', default: 'MyApp.Repo' } }, required: ['sessionId'] } }, { name: 'ecto_sandbox_status', description: 'Check Ecto sandbox mode for testing', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, pid: { type: 'string', description: 'Process ID to check' } }, required: ['sessionId'] } } ]; async handle(toolName, args, sessions) { const methodName = this.convertToolNameToMethod(toolName); const method = this[methodName]; if (typeof method === 'function') { return method.call(this, args, sessions); } throw new Error(`Unknown Ecto tool: ${toolName}`); } convertToolNameToMethod(toolName) { return toolName.replace(/_(.)/g, (_, char) => char.toUpperCase()); } // Remove duplicate getSession - it's already in BaseToolHandler async ectoInspectRepoConfig(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` repo = ${args.repoModule} config = repo.config() %{ adapter: config[:adapter], database: config[:database], hostname: config[:hostname], port: config[:port], pool_size: config[:pool_size], timeout: config[:timeout], queue_target: config[:queue_target], queue_interval: config[:queue_interval], migration_source: config[:migration_source], priv: config[:priv] } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to inspect repo config: ${result.error}`); } return this.createTextResponse(this.formatRepoConfig(result.data)); } async ectoTraceQueries(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` # Enable query logging Logger.configure_backend(:console, [ format: "$time $metadata[$level] $message\\n", metadata: [:query_time, :source, :query] ]) # Start telemetry handler :telemetry.attach( "ecto-query-tracer", [:my_app, :repo, :query], fn _event, measurements, metadata, _config -> if measurements.query_time > ${args.minTime * 1000} do IO.puts("Query took: #{measurements.query_time / 1_000_000}ms") IO.puts("Source: #{metadata.source}") IO.puts("Query: #{metadata.query}") end end, nil ) Process.sleep(${args.duration} * 1000) :telemetry.detach("ecto-query-tracer") "Query tracing completed" `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to trace queries: ${result.error}`); } return this.createTextResponse(`🔍 **Ecto Query Tracing**\n\n` + `Duration: ${args.duration} seconds\n` + `Min Time: ${args.minTime}ms\n\n` + `✅ Tracing completed. Check your application logs for query details.`); } async ectoAnalyzeNPlusOne(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` # Track queries for N+1 detection query_counts = %{} :telemetry.attach( "n-plus-one-detector", [:my_app, :repo, :query], fn _event, _measurements, metadata, _config -> key = {metadata.source, metadata.query} Agent.update(:query_counter, fn state -> Map.update(state, key, 1, &(&1 + 1)) end) end, nil ) Agent.start_link(fn -> %{} end, name: :query_counter) # Simulate action execution # In real implementation, this would trigger the actual action Process.sleep(2000) # Get results results = Agent.get(:query_counter, & &1) |> Enum.filter(fn {_, count} -> count > ${args.threshold} end) |> Enum.sort_by(fn {_, count} -> count end, :desc) :telemetry.detach("n-plus-one-detector") Agent.stop(:query_counter) results `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to analyze N+1: ${result.error}`); } return this.createTextResponse(this.formatNPlusOneResults(result.data, args.threshold)); } async ectoInspectChangesets(args, sessions) { const session = this.getSession(args.sessionId, sessions); const params = args.params ? JSON.stringify(args.params) : '{}'; const action = args.action || 'changeset'; const code = ` module = ${args.module} params = Jason.decode!(${JSON.stringify(params)}) # Create struct and changeset struct = struct(module) changeset = apply(module, :${action}, [struct, params]) %{ valid?: changeset.valid?, changes: changeset.changes, errors: changeset.errors, action: changeset.action, types: Map.keys(changeset.types || %{}), required: changeset.required || [], validations: changeset.validations || [] } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to inspect changeset: ${result.error}`); } return this.createTextResponse(this.formatChangesetInfo(result.data)); } async ectoMigrationStatus(args, sessions) { const session = this.getSession(args.sessionId, sessions); const result = await this.tidewave.executeTool(session.url, { tool: 'mix', params: { task: 'ecto.migrations', args: ['--repo', args.repo || 'MyApp.Repo'] } }); if (!result.success) { throw new Error(`Failed to check migrations: ${result.error}`); } return this.createTextResponse(`📋 **Migration Status**\n\n` + `Repository: \`${args.repo || 'MyApp.Repo'}\`\n\n` + '```\n' + result.data + '\n```'); } async ectoQueryAnalyzer(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` query = ${args.query} # Convert to SQL if it's an Ecto query sql = if is_struct(query) do {sql, params} = Ecto.Adapters.SQL.to_sql(:all, Repo, query) sql else query end # Run EXPLAIN result = Ecto.Adapters.SQL.query!(Repo, "EXPLAIN #{sql}", []) %{ query: sql, plan: result.rows, format: "${args.format}" } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to analyze query: ${result.error}`); } return this.createTextResponse(this.formatQueryPlan(result.data)); } async ectoConnectionPoolStatus(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` repo = ${args.repo || 'MyApp.Repo'} # Get pool telemetry pool_pid = Process.whereis(repo) pool_info = if pool_pid do :sys.get_state(pool_pid) else nil end # Get basic stats %{ alive?: Process.alive?(pool_pid || self()), pool_size: repo.config()[:pool_size], timeout: repo.config()[:timeout], queue_target: repo.config()[:queue_target], queue_interval: repo.config()[:queue_interval] } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to check pool status: ${result.error}`); } return this.createTextResponse(this.formatPoolStatus(result.data)); } async ectoSandboxStatus(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` pid = ${args.pid || 'self()'} # Check if sandbox is enabled sandbox_enabled = Code.ensure_loaded?(Ecto.Adapters.SQL.Sandbox) # Check if process is sandboxed is_sandboxed = if sandbox_enabled do try do Ecto.Adapters.SQL.Sandbox.checkin(Repo) true rescue _ -> false end else false end %{ sandbox_available: sandbox_enabled, process_sandboxed: is_sandboxed, pid: inspect(pid) } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to check sandbox status: ${result.error}`); } return this.createTextResponse(`🏖️ **Ecto Sandbox Status**\n\n` + `PID: ${result.data.pid}\n` + `Sandbox Available: ${result.data.sandbox_available ? '✅' : '❌'}\n` + `Process Sandboxed: ${result.data.process_sandboxed ? '✅' : '❌'}\n\n` + (result.data.process_sandboxed ? `💡 This process is running in sandbox mode for testing.` : `💡 This process is using the real database connection.`)); } // Helper methods formatRepoConfig(config) { return `🗄️ **Repository Configuration**\n\n` + `**Connection:**\n` + `• Adapter: ${config.adapter}\n` + `• Database: ${config.database}\n` + `• Host: ${config.hostname}:${config.port}\n\n` + `**Pool Settings:**\n` + `• Pool Size: ${config.pool_size}\n` + `• Timeout: ${config.timeout}ms\n` + `• Queue Target: ${config.queue_target}ms\n` + `• Queue Interval: ${config.queue_interval}ms\n\n` + `**Other:**\n` + `• Migration Source: ${config.migration_source || 'schema_migrations'}\n` + `• Priv Directory: ${config.priv}`; } formatNPlusOneResults(results, threshold) { if (!results || results.length === 0) { return `✅ **No N+1 Queries Detected**\n\nNo queries exceeded the threshold of ${threshold} executions.`; } let output = `⚠️ **Potential N+1 Queries Detected**\n\n`; output += `Found ${results.length} queries exceeding threshold (${threshold}):\n\n`; results.forEach(([[source, query], count], i) => { output += `${i + 1}. **${count} executions**\n`; output += ` Source: ${source}\n`; output += ` Query: \`${query.substring(0, 100)}${query.length > 100 ? '...' : ''}\`\n\n`; }); output += `💡 **Recommendation**: Consider using \`preload\` or \`join\` to load associations.`; return output; } formatChangesetInfo(data) { let output = `📝 **Changeset Inspection**\n\n`; output += `Valid: ${data.valid ? '✅' : '❌'}\n`; output += `Action: ${data.action || 'none'}\n\n`; if (data.changes && Object.keys(data.changes).length > 0) { output += `**Changes:**\n`; Object.entries(data.changes).forEach(([key, value]) => { output += `• ${key}: ${JSON.stringify(value)}\n`; }); output += '\n'; } if (data.errors && data.errors.length > 0) { output += `**Errors:**\n`; data.errors.forEach(([field, { message }]) => { output += `• ${field}: ${message}\n`; }); output += '\n'; } if (data.required && data.required.length > 0) { output += `**Required Fields:** ${data.required.join(', ')}\n`; } if (data.types && data.types.length > 0) { output += `**Field Types:** ${data.types.join(', ')}\n`; } return output; } formatQueryPlan(data) { let output = `🔍 **Query Analysis**\n\n`; output += `**Query:**\n\`\`\`sql\n${data.query}\n\`\`\`\n\n`; output += `**Execution Plan:**\n`; if (data.format === 'text' && data.plan) { output += '```\n'; data.plan.forEach((row) => { output += row.join(' ') + '\n'; }); output += '```'; } else { output += JSON.stringify(data.plan, null, 2); } return output; } formatPoolStatus(data) { const status = data.alive ? '✅ Healthy' : '❌ Not Running'; return `🏊 **Connection Pool Status**\n\n` + `Status: ${status}\n\n` + `**Configuration:**\n` + `• Pool Size: ${data.pool_size}\n` + `• Timeout: ${data.timeout}ms\n` + `• Queue Target: ${data.queue_target}ms\n` + `• Queue Interval: ${data.queue_interval}ms\n\n` + `💡 These settings control how connections are managed and queued.`; } } //# sourceMappingURL=ecto-handler.js.map