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

543 lines (516 loc) 17.7 kB
/** * 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