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
543 lines (516 loc) • 17.7 kB
JavaScript
/**
* Phoenix Debugging Tools Handler
*
* Provides specialized debugging tools for Phoenix/Elixir applications including:
* - LiveView state inspection and debugging
* - GenServer state monitoring and message tracing
* - PubSub message flow visualization
* - Supervisor tree inspection
* - Process mailbox monitoring
*/
import { BaseToolHandler } from './base-handler.js';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
export class PhoenixDebuggingToolsHandler extends BaseToolHandler {
tools = [
{
name: 'debug_liveview_state',
description: `Inspect LiveView process state and socket assigns.
Shows:
- Current assigns and their values
- Socket state and metadata
- Active components and their state
- Recent events and handlers`,
inputSchema: {
type: 'object',
properties: {
module: {
type: 'string',
description: 'LiveView module name (e.g., "MyAppWeb.PageLive")'
},
pid: {
type: 'string',
description: 'Optional: specific process PID to inspect'
},
includeComponents: {
type: 'boolean',
default: true,
description: 'Include stateful components in the output'
}
},
required: ['module']
}
},
{
name: 'debug_genserver_state',
description: `Monitor GenServer state and message queue.
Shows:
- Current state data
- Message queue contents
- Recent calls and casts
- Process info and metadata`,
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'GenServer name or PID'
},
includeSystemState: {
type: 'boolean',
default: false,
description: 'Include OTP system state information'
},
messageLimit: {
type: 'number',
default: 10,
description: 'Maximum number of messages to show from mailbox'
}
},
required: ['name']
}
},
{
name: 'debug_pubsub_topics',
description: `Monitor PubSub topics and subscribers.
Shows:
- Active topics and subscriber counts
- Recent messages on topics
- Subscriber process information
- Message flow patterns`,
inputSchema: {
type: 'object',
properties: {
pubsub: {
type: 'string',
default: 'MyApp.PubSub',
description: 'PubSub module name'
},
topic: {
type: 'string',
description: 'Optional: specific topic to monitor'
},
traceMessages: {
type: 'boolean',
default: false,
description: 'Enable message tracing (may impact performance)'
}
}
}
},
{
name: 'debug_supervisor_tree',
description: `Visualize OTP supervisor tree and process relationships.
Shows:
- Supervisor hierarchy
- Child process states
- Restart strategies and counts
- Process relationships`,
inputSchema: {
type: 'object',
properties: {
supervisor: {
type: 'string',
default: 'MyApp.Supervisor',
description: 'Supervisor module name'
},
depth: {
type: 'number',
default: 3,
description: 'Maximum depth to traverse'
},
includeSpecs: {
type: 'boolean',
default: false,
description: 'Include child specifications'
}
}
}
},
{
name: 'monitor_process_mailbox',
description: `Monitor process mailbox in real-time.
Shows:
- Message queue size over time
- Message types and patterns
- Processing rate
- Potential bottlenecks`,
inputSchema: {
type: 'object',
properties: {
process: {
type: 'string',
description: 'Process name or PID to monitor'
},
duration: {
type: 'number',
default: 5,
description: 'Monitoring duration in seconds'
},
interval: {
type: 'number',
default: 100,
description: 'Sampling interval in milliseconds'
}
},
required: ['process']
}
}
];
async handle(toolName, args) {
switch (toolName) {
case 'debug_liveview_state':
return this.debugLiveViewState(args);
case 'debug_genserver_state':
return this.debugGenServerState(args);
case 'debug_pubsub_topics':
return this.debugPubSubTopics(args);
case 'debug_supervisor_tree':
return this.debugSupervisorTree(args);
case 'monitor_process_mailbox':
return this.monitorProcessMailbox(args);
default:
throw new Error(`Unknown Phoenix debugging tool: ${toolName}`);
}
}
async debugLiveViewState(args) {
const { module, pid, includeComponents = true } = args;
const elixirScript = `
# LiveView State Inspector
pids = if pid = "${pid || ''}" |> String.trim() |> String.to_charlist() |> :erlang.list_to_pid() rescue nil do
[pid]
else
Process.list()
|> Enum.filter(fn p ->
case Process.info(p, :dictionary) do
{:dictionary, dict} ->
Keyword.get(dict, :"$initial_call") == {${module}, :mount, 3} ||
Keyword.get(dict, :"$ancestors", []) |> Enum.any?(&(&1 == ${module}))
_ -> false
end
end)
end
Enum.map(pids, fn pid ->
case :sys.get_state(pid) do
%Phoenix.LiveView.Socket{} = socket ->
%{
pid: inspect(pid),
module: ${module},
assigns: Map.drop(socket.assigns, [:__changed__, :flash]),
private: Map.keys(socket.private),
connected?: socket.transport_pid != nil,
components: if ${includeComponents}, do: inspect_components(socket), else: []
}
state ->
%{pid: inspect(pid), error: "Not a LiveView socket", state_type: inspect(state.__struct__)}
end
end)
|> Jason.encode!(pretty: true)
`;
try {
const { stdout } = await execAsync(`echo '${elixirScript.replace(/'/g, "'\\''")}' | mix run --no-start`, { cwd: process.cwd() });
return {
content: [{
type: 'text',
text: `🔍 **LiveView State Inspection**\n\n\`\`\`json\n${stdout}\n\`\`\``
}]
};
}
catch (error) {
return {
content: [{
type: 'text',
text: `❌ Failed to inspect LiveView state: ${error.message}`
}]
};
}
}
async debugGenServerState(args) {
const { name, includeSystemState = false, messageLimit = 10 } = args;
const elixirScript = `
# GenServer State Inspector
target = case "${name}" do
<<"#PID", _::binary>> = pid_string ->
pid_string |> String.to_charlist() |> :erlang.list_to_pid()
name ->
Process.whereis(String.to_atom(name)) || {:global, String.to_atom(name)}
end
case target do
nil ->
%{error: "Process not found: ${name}"}
pid when is_pid(pid) ->
info = Process.info(pid, [:messages, :message_queue_len, :current_function, :dictionary, :status])
state = try do
:sys.get_state(pid, 5000)
rescue
_ -> %{error: "Could not retrieve state"}
end
system_state = if ${includeSystemState} do
try do
:sys.get_status(pid, 5000)
rescue
_ -> nil
end
else
nil
end
%{
pid: inspect(pid),
name: "${name}",
status: info[:status],
message_queue_length: info[:message_queue_len],
messages: info[:messages] |> Enum.take(${messageLimit}) |> Enum.map(&inspect/1),
current_function: inspect(info[:current_function]),
state: inspect(state, limit: :infinity, pretty: true),
system_state: if(system_state, do: inspect(system_state, limit: 50))
}
{:global, _} = global_name ->
case GenServer.whereis(global_name) do
nil -> %{error: "Global process not found: ${name}"}
pid -> # Recurse with the resolved PID
# ... same logic as above for pid case
end
end
|> Jason.encode!(pretty: true)
`;
try {
const { stdout } = await execAsync(`echo '${elixirScript.replace(/'/g, "'\\''")}' | mix run --no-start`, { cwd: process.cwd() });
return {
content: [{
type: 'text',
text: `🔧 **GenServer State Inspection**\n\n\`\`\`json\n${stdout}\n\`\`\``
}]
};
}
catch (error) {
return {
content: [{
type: 'text',
text: `❌ Failed to inspect GenServer state: ${error.message}`
}]
};
}
}
async debugPubSubTopics(args) {
const { pubsub = 'MyApp.PubSub', topic, traceMessages = false } = args;
const elixirScript = `
# PubSub Topic Inspector
pubsub_name = String.to_atom("${pubsub}")
# Get Registry for the PubSub
registry = Module.concat([pubsub_name, Registry]) |> Process.whereis()
topics = if topic = "${topic || ''}" |> String.trim() do
if String.length(topic) > 0, do: [topic], else: nil
else
nil
end
# If no specific topic, get all topics
all_topics = if is_nil(topics) do
case registry do
nil -> []
pid when is_pid(pid) ->
Registry.select(Module.concat([pubsub_name, Registry]), [{{:"$1", :"$2", :"$3"}, [], [:"$1"]}])
|> Enum.uniq()
end
else
topics
end
# Get subscriber info for each topic
topic_info = Enum.map(all_topics, fn topic ->
subscribers = case registry do
nil -> []
_ ->
Registry.lookup(Module.concat([pubsub_name, Registry]), topic)
|> Enum.map(fn {pid, _} ->
info = Process.info(pid, [:registered_name, :current_function, :message_queue_len])
%{
pid: inspect(pid),
name: info[:registered_name],
function: inspect(info[:current_function]),
queue_length: info[:message_queue_len]
}
end)
end
%{
topic: topic,
subscriber_count: length(subscribers),
subscribers: subscribers
}
end)
# Setup tracing if requested
trace_info = if ${traceMessages} do
# This would set up actual tracing in a real implementation
%{tracing: "Message tracing would be enabled here"}
else
%{tracing: false}
end
%{
pubsub: "${pubsub}",
registry_pid: inspect(registry),
topics: topic_info,
trace_info: trace_info
}
|> Jason.encode!(pretty: true)
`;
try {
const { stdout } = await execAsync(`echo '${elixirScript.replace(/'/g, "'\\''")}' | mix run --no-start`, { cwd: process.cwd() });
return {
content: [{
type: 'text',
text: `📡 **PubSub Topic Inspection**\n\n\`\`\`json\n${stdout}\n\`\`\``
}]
};
}
catch (error) {
return {
content: [{
type: 'text',
text: `❌ Failed to inspect PubSub topics: ${error.message}`
}]
};
}
}
async debugSupervisorTree(args) {
const { supervisor = 'MyApp.Supervisor', depth = 3, includeSpecs = false } = args;
const elixirScript = `
# Supervisor Tree Inspector
defmodule SupervisorInspector do
def inspect_tree(supervisor_name, max_depth \\\\ 3, include_specs \\\\ false) do
sup_module = String.to_existing_atom(supervisor_name)
case Process.whereis(sup_module) do
nil ->
# Try to find by registered name pattern
Process.list()
|> Enum.find(fn pid ->
case Process.info(pid, :registered_name) do
{:registered_name, ^sup_module} -> true
_ -> false
end
end)
|> case do
nil -> %{error: "Supervisor not found: #{supervisor_name}"}
pid -> build_tree(pid, 0, max_depth, include_specs)
end
pid ->
build_tree(pid, 0, max_depth, include_specs)
end
end
defp build_tree(pid, current_depth, max_depth, include_specs) when current_depth >= max_depth do
%{pid: inspect(pid), depth_limit_reached: true}
end
defp build_tree(pid, current_depth, max_depth, include_specs) do
case Supervisor.which_children(pid) do
children when is_list(children) ->
%{
pid: inspect(pid),
module: get_module_name(pid),
children: Enum.map(children, fn {id, child_pid, type, modules} ->
child_info = %{
id: id,
type: type,
modules: modules,
status: if is_pid(child_pid), do: "running", else: to_string(child_pid)
}
if is_pid(child_pid) and type == :supervisor do
Map.merge(child_info, build_tree(child_pid, current_depth + 1, max_depth, include_specs))
else
child_info
end
end)
}
_ ->
%{pid: inspect(pid), type: "worker"}
end
end
defp get_module_name(pid) do
case Process.info(pid, :registered_name) do
{:registered_name, name} -> to_string(name)
_ -> "unnamed"
end
end
end
SupervisorInspector.inspect_tree("${supervisor}", ${depth}, ${includeSpecs})
|> Jason.encode!(pretty: true)
`;
try {
const { stdout } = await execAsync(`echo '${elixirScript.replace(/'/g, "'\\''")}' | mix run --no-start`, { cwd: process.cwd() });
return {
content: [{
type: 'text',
text: `🌳 **Supervisor Tree Inspection**\n\n\`\`\`json\n${stdout}\n\`\`\``
}]
};
}
catch (error) {
return {
content: [{
type: 'text',
text: `❌ Failed to inspect supervisor tree: ${error.message}`
}]
};
}
}
async monitorProcessMailbox(args) {
const { process, duration = 5, interval = 100 } = args;
const elixirScript = `
# Process Mailbox Monitor
target = case "${process}" do
<<"#PID", _::binary>> = pid_string ->
pid_string |> String.to_charlist() |> :erlang.list_to_pid()
name ->
Process.whereis(String.to_atom(name))
end
case target do
nil ->
%{error: "Process not found: ${process}"}
pid when is_pid(pid) ->
# Collect samples
samples = for _ <- 1..div(${duration * 1000}, ${interval}) do
info = Process.info(pid, [:message_queue_len, :messages, :memory, :reductions])
Process.sleep(${interval})
%{
timestamp: System.monotonic_time(:millisecond),
queue_length: info[:message_queue_len],
memory: info[:memory],
reductions: info[:reductions],
sample_messages: info[:messages] |> Enum.take(3) |> Enum.map(&inspect(&1, limit: 20))
}
end
# Calculate statistics
queue_lengths = Enum.map(samples, & &1.queue_length)
%{
process: "${process}",
pid: inspect(pid),
monitoring_duration: ${duration},
samples_collected: length(samples),
statistics: %{
avg_queue_length: Enum.sum(queue_lengths) / length(queue_lengths),
max_queue_length: Enum.max(queue_lengths),
min_queue_length: Enum.min(queue_lengths),
queue_growth: List.last(queue_lengths) - List.first(queue_lengths)
},
samples: samples
}
end
|> Jason.encode!(pretty: true)
`;
try {
const { stdout } = await execAsync(`echo '${elixirScript.replace(/'/g, "'\\''")}' | mix run --no-start`, { cwd: process.cwd() });
return {
content: [{
type: 'text',
text: `📊 **Process Mailbox Monitoring**\n\n\`\`\`json\n${stdout}\n\`\`\``
}]
};
}
catch (error) {
return {
content: [{
type: 'text',
text: `❌ Failed to monitor process mailbox: ${error.message}`
}]
};
}
}
}
//# sourceMappingURL=phoenix-debugging-tools-handler.js.map