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

755 lines (739 loc) โ€ข 31.1 kB
import { BaseToolHandler } from './base-handler.js'; import { TidewaveIntegration } from '../tidewave-integration.js'; /** * Handler for advanced Elixir/BEAM debugging tools */ export class ElixirHandler extends BaseToolHandler { tidewave; constructor() { super(); this.tidewave = new TidewaveIntegration(); } tools = [ // Process Inspection Tools { name: 'elixir_inspect_process', description: 'Inspect specific process state, message queue, and memory usage', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, pid: { type: 'string', description: 'Process ID (e.g., "<0.123.0>" or registered name)' }, includeMessages: { type: 'boolean', description: 'Include message queue contents', default: true }, includeState: { type: 'boolean', description: 'Include process state', default: true } }, required: ['sessionId', 'pid'] } }, { name: 'elixir_trace_function', description: 'Trace function calls with arguments and return values', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, module: { type: 'string', description: 'Module name (e.g., "MyApp.Users")' }, function: { type: 'string', description: 'Function name' }, arity: { type: 'number', description: 'Function arity' }, maxCalls: { type: 'number', description: 'Maximum calls to trace', default: 100 } }, required: ['sessionId', 'module', 'function', 'arity'] } }, { name: 'elixir_profile_memory', description: 'Profile memory usage for processes and identify memory leaks', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, sortBy: { type: 'string', enum: ['memory', 'message_queue', 'reductions'], description: 'Sort criteria', default: 'memory' }, limit: { type: 'number', description: 'Number of processes to show', default: 20 } }, required: ['sessionId'] } }, { name: 'elixir_inspect_ets', description: 'Inspect ETS tables contents and configuration', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, tableName: { type: 'string', description: 'ETS table name or ID' }, limit: { type: 'number', description: 'Limit number of entries', default: 100 }, pattern: { type: 'string', description: 'Match pattern (optional)' } }, required: ['sessionId'] } }, { name: 'elixir_inspect_supervision_tree', description: 'Visualize supervisor trees and child processes', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, supervisor: { type: 'string', description: 'Supervisor name (optional, defaults to root)' } }, required: ['sessionId'] } }, { name: 'elixir_hot_reload_module', description: 'Hot reload modules during debugging without restarting', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, module: { type: 'string', description: 'Module to reload (e.g., "MyApp.Users")' } }, required: ['sessionId', 'module'] } }, // GenServer/OTP Tools { name: 'genserver_inspect_state', description: 'Inspect GenServer state without affecting the process', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, serverName: { type: 'string', description: 'GenServer name or PID' } }, required: ['sessionId', 'serverName'] } }, { name: 'genserver_trace_calls', description: 'Trace handle_call, handle_cast, and handle_info messages', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, serverName: { type: 'string', description: 'GenServer name or PID' }, messageTypes: { type: 'array', items: { type: 'string', enum: ['call', 'cast', 'info'] }, description: 'Message types to trace', default: ['call', 'cast', 'info'] }, duration: { type: 'number', description: 'Trace duration in seconds', default: 30 } }, required: ['sessionId', 'serverName'] } }, { name: 'genserver_monitor_mailbox', description: 'Monitor process mailbox size and message flow', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, processName: { type: 'string', description: 'Process name or PID' }, alertThreshold: { type: 'number', description: 'Alert when mailbox exceeds size', default: 1000 } }, required: ['sessionId', 'processName'] } }, { name: 'supervisor_restart_child', description: 'Restart supervised processes for debugging', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, supervisor: { type: 'string', description: 'Supervisor name' }, childId: { type: 'string', description: 'Child specification ID' } }, required: ['sessionId', 'supervisor', 'childId'] } }, { name: 'application_env_inspect', description: 'Inspect application environment and configuration', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, application: { type: 'string', description: 'Application name (e.g., :my_app)' }, key: { type: 'string', description: 'Config key (optional, shows all if not provided)' } }, required: ['sessionId', 'application'] } }, // Testing & Quality Tools { name: 'exunit_run_single_test', description: 'Run a single test with debugging output', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, testFile: { type: 'string', description: 'Test file path' }, testName: { type: 'string', description: 'Test name or line number' }, trace: { type: 'boolean', description: 'Enable tracing', default: false } }, required: ['sessionId', 'testFile'] } }, { name: 'exunit_trace_test_process', description: 'Trace test execution with detailed process information', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, testModule: { type: 'string', description: 'Test module name' } }, required: ['sessionId', 'testModule'] } }, { name: 'dialyzer_check_current_file', description: 'Run Dialyzer type checking on current file', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, filePath: { type: 'string', description: 'File to analyze' } }, required: ['sessionId', 'filePath'] } }, { name: 'credo_analyze_current', description: 'Run Credo code analysis on current file or module', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, filePath: { type: 'string', description: 'File to analyze' }, strict: { type: 'boolean', description: 'Use strict mode', default: false } }, required: ['sessionId', 'filePath'] } }, { name: 'mix_task_runner', description: 'Execute any mix task with captured output', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, task: { type: 'string', description: 'Mix task to run (e.g., "test", "compile")' }, args: { type: 'array', items: { type: 'string' }, description: 'Task arguments' } }, required: ['sessionId', 'task'] } } ]; 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 Elixir tool: ${toolName}`); } convertToolNameToMethod(toolName) { return toolName.replace(/_(.)/g, (_, char) => char.toUpperCase()); } // Remove duplicate getSession - it's already in BaseToolHandler // Process Inspection Methods async elixirInspectProcess(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` pid = ${args.pid} info = Process.info(pid) state = :sys.get_state(pid) rescue nil messages = Process.info(pid, :messages)[:messages] rescue [] %{ info: info, state: state, messages: messages, alive: Process.alive?(pid) } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to inspect process: ${result.error}`); } return this.createTextResponse(this.formatProcessInfo(result.data)); } formatProcessInfo(data) { let output = '๐Ÿ” **Process Inspection**\n\n'; if (!data.alive) { return output + 'โŒ Process is not alive\n'; } output += '**Process Info:**\n'; if (data.info) { output += `- PID: ${data.info.pid || 'N/A'}\n`; output += `- Status: ${data.info.status || 'N/A'}\n`; output += `- Memory: ${this.formatBytes(data.info.memory || 0)}\n`; output += `- Reductions: ${data.info.reductions || 0}\n`; output += `- Message Queue: ${data.info.message_queue_len || 0} messages\n`; } if (data.state) { output += '\n**Process State:**\n'; output += '```elixir\n' + this.formatElixirData(data.state) + '\n```\n'; } if (data.messages && data.messages.length > 0) { output += '\n**Message Queue:**\n'; data.messages.slice(0, 10).forEach((msg, i) => { output += `${i + 1}. ${this.formatElixirData(msg)}\n`; }); if (data.messages.length > 10) { output += `... and ${data.messages.length - 10} more messages\n`; } } return output; } async elixirTraceFunction(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` :dbg.start() :dbg.tracer() :dbg.tp(${args.module}, :${args.function}, ${args.arity}, [{:_, [], [{:return_trace}]}]) :dbg.p(:all, :c) # Collect traces for a short time Process.sleep(1000) # Stop tracing :dbg.stop_clear() "Tracing started for ${args.module}.${args.function}/${args.arity}" `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to start tracing: ${result.error}`); } return this.createTextResponse(`๐Ÿ” **Function Tracing Started**\n\n` + `Tracing: \`${args.module}.${args.function}/${args.arity}\`\n\n` + `Traces will appear in your application logs.\n` + `Maximum calls: ${args.maxCalls}`); } async elixirProfileMemory(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` processes = Process.list() |> Enum.map(fn pid -> info = Process.info(pid, [:memory, :message_queue_len, :reductions, :registered_name]) if info, do: Map.put(info, :pid, pid), else: nil end) |> Enum.reject(&is_nil/1) |> Enum.sort_by(&(&1[:${args.sortBy}]), :desc) |> Enum.take(${args.limit}) `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to profile memory: ${result.error}`); } return this.createTextResponse(this.formatMemoryProfile(result.data, args.sortBy)); } formatMemoryProfile(processes, sortBy) { let output = '๐Ÿ’พ **Memory Profile**\n\n'; output += `Top ${processes.length} processes by ${sortBy}:\n\n`; processes.forEach((proc, i) => { const name = proc.registered_name ? `[${proc.registered_name}]` : ''; output += `${i + 1}. PID: ${proc.pid} ${name}\n`; output += ` Memory: ${this.formatBytes(proc.memory)}\n`; output += ` Message Queue: ${proc.message_queue_len} messages\n`; output += ` Reductions: ${proc.reductions.toLocaleString()}\n\n`; }); return output; } // GenServer Methods async genserverInspectState(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` try do state = :sys.get_state(${args.serverName}) %{ success: true, state: state, process_info: Process.info(Process.whereis(${args.serverName})) } rescue e -> %{success: false, error: Exception.message(e)} end `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to inspect GenServer: ${result.error}`); } if (!result.data.success) { throw new Error(`GenServer inspection failed: ${result.data.error}`); } return this.createTextResponse(`๐Ÿ” **GenServer State**\n\n` + `Server: \`${args.serverName}\`\n\n` + `**State:**\n` + '```elixir\n' + this.formatElixirData(result.data.state) + '\n```'); } // Testing Methods async exunitRunSingleTest(args, sessions) { const session = this.getSession(args.sessionId, sessions); const mixArgs = [`test`, args.testFile]; if (args.testName) { mixArgs.push(`--only`, `test:${args.testName}`); } if (args.trace) { mixArgs.push(`--trace`); } const result = await this.tidewave.executeTool(session.url, { tool: 'mix', params: { task: 'test', args: mixArgs } }); if (!result.success) { throw new Error(`Test execution failed: ${result.error}`); } return this.createTextResponse(`๐Ÿงช **Test Execution Results**\n\n` + '```\n' + result.data + '\n```'); } async inspectEts(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = args.tableName ? ` table = :ets.whereis(${args.tableName}) if table do info = :ets.info(table) entries = :ets.tab2list(table) |> Enum.take(${args.limit}) %{ info: info, entries: entries, size: :ets.info(table, :size) } else %{error: "Table not found"} end ` : ` tables = :ets.all() |> Enum.map(fn table -> info = :ets.info(table) %{ id: table, name: info[:name], type: info[:type], size: info[:size], memory: info[:memory] } end) `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to inspect ETS: ${result.error}`); } return this.createTextResponse(this.formatEtsInfo(result.data, args.tableName)); } async inspectSupervisionTree(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` supervisor = ${args.supervisor || ':application_controller'} children = Supervisor.which_children(supervisor) |> Enum.map(fn {id, child, type, modules} -> %{ id: id, pid: inspect(child), type: type, modules: modules } end) %{ supervisor: inspect(supervisor), children: children, count: length(children) } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to inspect supervision tree: ${result.error}`); } return this.createTextResponse(this.formatSupervisionTree(result.data)); } async hotReloadModule(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` module = ${args.module} case Code.ensure_loaded(module) do {:module, _} -> # Get the beam file path beam_file = :code.which(module) # Purge old version :code.purge(module) # Load new version case :code.load_file(module) do {:module, ^module} -> %{success: true, message: "Module #{module} reloaded successfully"} {:error, reason} -> %{success: false, error: "Failed to reload: #{inspect(reason)}"} end {:error, reason} -> %{success: false, error: "Module not found: #{inspect(reason)}"} end `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to hot reload module: ${result.error}`); } if (!result.data.success) { throw new Error(result.data.error); } return this.createTextResponse(`๐Ÿ”ฅ **Hot Module Reload**\n\n` + `โœ… ${result.data.message}\n\n` + `๐Ÿ’ก All processes using this module will now use the new version.`); } async genserverTraceCall(args, sessions) { const session = this.getSession(args.sessionId, sessions); const messageTypes = args.messageTypes || ['call', 'cast', 'info']; const code = ` server = ${args.serverName} # Start tracing :sys.trace(server, true) # Capture traces for duration Process.sleep(${args.duration || 30} * 1000) # Stop tracing :sys.trace(server, false) "Tracing completed for #{inspect(server)}" `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to trace GenServer: ${result.error}`); } return this.createTextResponse(`๐Ÿ“ก **GenServer Call Tracing**\n\n` + `Server: \`${args.serverName}\`\n` + `Message Types: ${messageTypes.join(', ')}\n` + `Duration: ${args.duration || 30} seconds\n\n` + `โœ… Tracing completed. Check your application logs for trace output.`); } async genserverMonitorMailbox(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` process = Process.whereis(${args.processName}) if process do info = Process.info(process, [:message_queue_len, :messages]) queue_len = info[:message_queue_len] alert = queue_len > ${args.alertThreshold || 1000} %{ process: inspect(process), queue_length: queue_len, alert: alert, threshold: ${args.alertThreshold || 1000}, sample_messages: Enum.take(info[:messages] || [], 5) } else %{error: "Process not found"} end `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to monitor mailbox: ${result.error}`); } if (result.data.error) { throw new Error(result.data.error); } const alertIcon = result.data.alert ? '๐Ÿšจ' : 'โœ…'; return this.createTextResponse(`๐Ÿ“ฌ **Mailbox Monitor**\n\n` + `Process: \`${args.processName}\`\n` + `Queue Length: ${result.data.queue_length} messages ${alertIcon}\n` + `Alert Threshold: ${result.data.threshold}\n\n` + (result.data.alert ? `โš ๏ธ **WARNING**: Mailbox size exceeds threshold!\n\n` : '') + (result.data.sample_messages.length > 0 ? `**Sample Messages:**\n` + result.data.sample_messages.map((msg, i) => `${i + 1}. ${this.formatElixirData(msg)}`).join('\n') : '')); } async supervisorRestartChild(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` supervisor = ${args.supervisor} child_id = ${args.childId} case Supervisor.terminate_child(supervisor, child_id) do :ok -> case Supervisor.restart_child(supervisor, child_id) do {:ok, pid} -> %{success: true, pid: inspect(pid), message: "Child restarted successfully"} {:ok, pid, _info} -> %{success: true, pid: inspect(pid), message: "Child restarted successfully"} {:error, reason} -> %{success: false, error: "Failed to restart: #{inspect(reason)}"} end {:error, reason} -> %{success: false, error: "Failed to terminate: #{inspect(reason)}"} end `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to restart child: ${result.error}`); } if (!result.data.success) { throw new Error(result.data.error); } return this.createTextResponse(`๐Ÿ”„ **Supervisor Child Restart**\n\n` + `โœ… ${result.data.message}\n` + `New PID: ${result.data.pid}\n\n` + `Supervisor: \`${args.supervisor}\`\n` + `Child ID: \`${args.childId}\``); } async applicationEnvInspect(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = args.key ? ` Application.get_env(${args.application}, ${args.key}) ` : ` Application.get_all_env(${args.application}) `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to inspect application env: ${result.error}`); } return this.createTextResponse(`โš™๏ธ **Application Environment**\n\n` + `Application: \`${args.application}\`\n` + (args.key ? `Key: \`${args.key}\`\n\n` : '\n') + `**Configuration:**\n` + '```elixir\n' + this.formatElixirData(result.data) + '\n```'); } async exunitTraceTestProcess(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` # Enable ExUnit tracing for the test module ExUnit.configure(trace: true, slowest: 10) # Run the specific test module with tracing Mix.Task.run("test", ["--trace", "--only", "module:#{${args.testModule}}"]) "Test tracing enabled for #{${args.testModule}}" `; const result = await this.tidewave.executeTool(session.url, { tool: 'eval', params: { code } }); if (!result.success) { throw new Error(`Failed to trace test process: ${result.error}`); } return this.createTextResponse(`๐Ÿ” **Test Process Tracing**\n\n` + `Module: \`${args.testModule}\`\n\n` + `โœ… Tracing enabled. Run your tests to see:\n` + `โ€ข Process spawning and lifecycle\n` + `โ€ข Message passing between processes\n` + `โ€ข Timing information\n` + `โ€ข The 10 slowest tests\n\n` + `๐Ÿ’ก Check your test output for detailed trace information.`); } async dialyzerCheckCurrentFile(args, sessions) { const session = this.getSession(args.sessionId, sessions); const result = await this.tidewave.executeTool(session.url, { tool: 'mix', params: { task: 'dialyzer', args: ['--no-check', args.filePath] } }); if (!result.success) { throw new Error(`Dialyzer check failed: ${result.error}`); } return this.createTextResponse(`๐Ÿ”ฌ **Dialyzer Type Check**\n\n` + `File: \`${args.filePath}\`\n\n` + '```\n' + result.data + '\n```'); } async credoAnalyzeCurrent(args, sessions) { const session = this.getSession(args.sessionId, sessions); const credoArgs = ['--format', 'flycheck', args.filePath]; if (args.strict) { credoArgs.unshift('--strict'); } const result = await this.tidewave.executeTool(session.url, { tool: 'mix', params: { task: 'credo', args: credoArgs } }); if (!result.success) { throw new Error(`Credo analysis failed: ${result.error}`); } return this.createTextResponse(`๐Ÿ“ **Credo Code Analysis**\n\n` + `File: \`${args.filePath}\`\n` + `Mode: ${args.strict ? 'Strict' : 'Normal'}\n\n` + '```\n' + result.data + '\n```'); } async mixTaskRunner(args, sessions) { const session = this.getSession(args.sessionId, sessions); const result = await this.tidewave.executeTool(session.url, { tool: 'mix', params: { task: args.task, args: args.args || [] } }); if (!result.success) { throw new Error(`Mix task failed: ${result.error}`); } return this.createTextResponse(`โšก **Mix Task Execution**\n\n` + `Task: \`mix ${args.task}\`\n` + (args.args && args.args.length > 0 ? `Arguments: ${args.args.join(' ')}\n\n` : '\n') + '```\n' + result.data + '\n```'); } // Helper Methods formatBytes(bytes) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`; return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } formatElixirData(data) { // Simple formatter for Elixir data structures if (typeof data === 'string') return data; return JSON.stringify(data, null, 2); } formatEtsInfo(data, tableName) { if (data.error) { return `โŒ **Error**: ${data.error}`; } if (tableName) { // Single table info let output = `๐Ÿ“Š **ETS Table: ${tableName}**\n\n`; if (data.info) { output += `**Table Info:**\n`; output += `โ€ข Type: ${data.info.type}\n`; output += `โ€ข Size: ${data.size} entries\n`; output += `โ€ข Memory: ${this.formatBytes(data.info.memory)}\n`; output += `โ€ข Owner: ${data.info.owner}\n\n`; } if (data.entries && data.entries.length > 0) { output += `**Sample Entries:**\n`; data.entries.forEach((entry, i) => { output += `${i + 1}. ${this.formatElixirData(entry)}\n`; }); } return output; } else { // All tables let output = `๐Ÿ“Š **ETS Tables Overview**\n\n`; output += `Total tables: ${data.length}\n\n`; data.forEach((table) => { output += `โ€ข **${table.name}** (${table.id})\n`; output += ` Type: ${table.type}, Size: ${table.size}, Memory: ${this.formatBytes(table.memory)}\n`; }); return output; } } formatSupervisionTree(data) { let output = `๐ŸŒณ **Supervision Tree**\n\n`; output += `Supervisor: \`${data.supervisor}\`\n`; output += `Children: ${data.count}\n\n`; if (data.children && data.children.length > 0) { output += `**Child Processes:**\n`; data.children.forEach((child, i) => { output += `${i + 1}. **${child.id}**\n`; output += ` โ€ข PID: ${child.pid}\n`; output += ` โ€ข Type: ${child.type}\n`; output += ` โ€ข Modules: ${Array.isArray(child.modules) ? child.modules.join(', ') : child.modules}\n\n`; }); } return output; } } //# sourceMappingURL=elixir-handler.js.map