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
JavaScript
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