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

672 lines (639 loc) β€’ 27.1 kB
import { BaseToolHandler } from './base-handler.js'; import { lazyTidewave } from '../utils/lazy-dependencies.js'; /** * Handler for advanced Phoenix framework debugging tools */ export class PhoenixAdvancedHandler extends BaseToolHandler { tidewave; // Lazy loaded constructor() { super(); } async getTidewave() { if (!this.tidewave) { this.tidewave = await lazyTidewave.load(); } return this.tidewave; } tools = [ { name: 'phoenix_trace_plug_pipeline', description: 'Trace HTTP request through entire Plug pipeline with timing', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, method: { type: 'string', description: 'HTTP method (GET, POST, etc.)' }, path: { type: 'string', description: 'Request path' }, includeParams: { type: 'boolean', description: 'Include request params', default: true } }, required: ['sessionId', 'method', 'path'] } }, { name: 'phoenix_inspect_endpoint_config', description: 'Inspect runtime Phoenix endpoint configuration', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, endpoint: { type: 'string', description: 'Endpoint module', default: 'MyAppWeb.Endpoint' } }, required: ['sessionId'] } }, { name: 'phoenix_profile_view_render', description: 'Profile view rendering times and identify slow templates', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, duration: { type: 'number', description: 'Profile duration in seconds', default: 30 } }, required: ['sessionId'] } }, { name: 'phoenix_analyze_router', description: 'Analyze route patterns, conflicts, and helpers', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, router: { type: 'string', description: 'Router module', default: 'MyAppWeb.Router' }, path: { type: 'string', description: 'Specific path to analyze (optional)' } }, required: ['sessionId'] } }, { name: 'phoenix_telemetry_events', description: 'Monitor Phoenix telemetry events and metrics', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, events: { type: 'array', items: { type: 'string' }, description: 'Specific events to monitor', default: ['phoenix.router_dispatch', 'phoenix.endpoint'] }, duration: { type: 'number', description: 'Monitor duration in seconds', default: 30 } }, required: ['sessionId'] } }, { name: 'phoenix_controller_action_trace', description: 'Trace controller action execution with detailed timing', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, controller: { type: 'string', description: 'Controller module' }, action: { type: 'string', description: 'Action atom' } }, required: ['sessionId', 'controller', 'action'] } }, { name: 'phoenix_conn_inspector', description: 'Deep inspect Plug.Conn struct at any pipeline stage', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, stage: { type: 'string', enum: ['before_router', 'after_router', 'before_controller', 'after_controller'], description: 'Pipeline stage to inspect' } }, required: ['sessionId', 'stage'] } }, { name: 'phoenix_session_debug', description: 'Debug session storage and cookie configuration', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, sessionKey: { type: 'string', description: 'Specific session key to inspect' } }, required: ['sessionId'] } }, { name: 'phoenix_presence_tracker', description: 'Track Phoenix Presence updates and sync states', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, topic: { type: 'string', description: 'Presence topic' }, duration: { type: 'number', description: 'Tracking duration in seconds', default: 30 } }, required: ['sessionId', 'topic'] } }, { name: 'phoenix_error_handler_trace', description: 'Trace error handling through ErrorView and error pages', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debug session ID' }, errorType: { type: 'string', description: 'Error type to simulate (404, 500, etc.)' } }, required: ['sessionId', 'errorType'] } } ]; 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 Phoenix advanced tool: ${toolName}`); } convertToolNameToMethod(toolName) { return toolName.replace(/_(.)/g, (_, char) => char.toUpperCase()); } // Remove duplicate getSession - it's already in BaseToolHandler async phoenixTracePlugPipeline(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` # Set up telemetry handlers for plug pipeline handlers = [ "plug-pipeline-tracer-router", "plug-pipeline-tracer-endpoint", "plug-pipeline-tracer-controller" ] # Detach any existing handlers Enum.each(handlers, &:telemetry.detach/1) # Attach new handlers :telemetry.attach( "plug-pipeline-tracer-router", [:phoenix, :router_dispatch, :start], fn event, measurements, metadata, _config -> IO.puts("Router dispatch started: #{inspect(metadata.route)}") end, nil ) :telemetry.attach( "plug-pipeline-tracer-endpoint", [:phoenix, :endpoint, :stop], fn event, %{duration: duration}, metadata, _config -> IO.puts("Endpoint completed in #{duration / 1_000_000}ms") end, nil ) # Make test request conn = Phoenix.ConnTest.build_conn() |> Phoenix.ConnTest.dispatch(MyAppWeb.Endpoint, :${args.method}, "${args.path}", ${args.includeParams ? "params" : "%{}"}) # Clean up Enum.each(handlers, &:telemetry.detach/1) %{ status: conn.status, resp_headers: conn.resp_headers, assigns: Map.keys(conn.assigns), private: Map.keys(conn.private), halted: conn.halted } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to trace plug pipeline: ${result.error}`); } return this.createTextResponse(this.formatPlugTrace(result.data, args)); } async phoenixInspectEndpointConfig(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` endpoint = ${args.endpoint || 'MyAppWeb.Endpoint'} config = endpoint.config(:all) %{ url: config[:url], http: config[:http], https: config[:https], secret_key_base: config[:secret_key_base] && "***REDACTED***", render_errors: config[:render_errors], pubsub_server: config[:pubsub_server], live_view: config[:live_view], static_url: config[:static_url], cache_static_manifest: config[:cache_static_manifest], check_origin: config[:check_origin], server: config[:server] } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to inspect endpoint: ${result.error}`); } return this.createTextResponse(this.formatEndpointConfig(result.data)); } async phoenixProfileViewRender(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` # Profile view rendering view_timings = %{} :telemetry.attach( "view-render-profiler", [:phoenix, :view, :render], fn _event, %{duration: duration}, %{view: view, template: template}, _config -> key = "#{inspect(view)}##{template}" Agent.update(:view_profiler, fn state -> Map.update(state, key, [duration], &[duration | &1]) end) end, nil ) Agent.start_link(fn -> %{} end, name: :view_profiler) # Profile for duration Process.sleep(${args.duration} * 1000) # Get results results = Agent.get(:view_profiler, & &1) |> Enum.map(fn {view_template, timings} -> %{ view_template: view_template, render_count: length(timings), avg_time: Enum.sum(timings) / length(timings) / 1_000_000, # ms min_time: Enum.min(timings) / 1_000_000, max_time: Enum.max(timings) / 1_000_000, total_time: Enum.sum(timings) / 1_000_000 } end) |> Enum.sort_by(& &1.avg_time, :desc) :telemetry.detach("view-render-profiler") Agent.stop(:view_profiler) results `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to profile views: ${result.error}`); } return this.createTextResponse(this.formatViewProfile(result.data)); } async phoenixAnalyzeRouter(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` router = ${args.router || 'MyAppWeb.Router'} # Get all routes routes = router.__routes__() |> Enum.map(fn route -> %{ verb: route.verb, path: route.path, plug: inspect(route.plug), plug_opts: inspect(route.plug_opts), helper: route.helper, metadata: route.metadata } end) # Check specific path if provided path_info = if "${args.path || ''}" != "" do Phoenix.Router.route_info(router, "${args.method || 'GET'}", "${args.path}", "") else nil end %{ total_routes: length(routes), routes: Enum.take(routes, 20), # First 20 routes path_analysis: path_info, helpers: router.__helpers__() |> Map.keys() |> Enum.take(10) } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to analyze router: ${result.error}`); } return this.createTextResponse(this.formatRouterAnalysis(result.data, args.path)); } async phoenixTelemetryEvents(args, sessions) { const session = this.getSession(args.sessionId, sessions); const events = args.events || ['phoenix.router_dispatch', 'phoenix.endpoint']; const eventHandlers = events.map((e) => `"telemetry-monitor-${e}"`); const code = ` # Monitor telemetry events events = ${JSON.stringify(events)} captured_events = [] Enum.each(events, fn event -> handler_id = "telemetry-monitor-#{event}" :telemetry.detach(handler_id) :telemetry.attach( handler_id, [String.split(event, ".") |> Enum.map(&String.to_atom/1)], fn event_name, measurements, metadata, _config -> Agent.update(:telemetry_capture, fn state -> [{event_name, measurements, metadata, :os.system_time(:millisecond)} | state] end) end, nil ) end) Agent.start_link(fn -> [] end, name: :telemetry_capture) # Monitor for duration Process.sleep(${args.duration} * 1000) # Get captured events captured = Agent.get(:telemetry_capture, & &1) |> Enum.reverse() # Clean up Enum.each(events, fn event -> :telemetry.detach("telemetry-monitor-#{event}") end) Agent.stop(:telemetry_capture) captured |> Enum.take(50) # Last 50 events `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to monitor telemetry: ${result.error}`); } return this.createTextResponse(this.formatTelemetryEvents(result.data)); } async phoenixControllerActionTrace(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` # Trace specific controller action :dbg.start() :dbg.tracer() :dbg.tp(${args.controller}, :${args.action}, [{:_, [], [{:return_trace}]}]) :dbg.p(:all, :c) # Also trace conn transformations :dbg.tp(Plug.Conn, :put_resp_header, [{:_, [], [{:return_trace}]}]) :dbg.tp(Plug.Conn, :assign, [{:_, [], [{:return_trace}]}]) "Tracing ${args.controller}.${args.action}/2 - check your logs for trace output" `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to trace controller: ${result.error}`); } return this.createTextResponse(`πŸ” **Controller Action Tracing**\n\n` + `Controller: \`${args.controller}\`\n` + `Action: \`${args.action}\`\n\n` + `βœ… Tracing enabled. The next request to this action will be traced.\n\n` + `Also tracing:\n` + `β€’ Plug.Conn.put_resp_header/3\n` + `β€’ Plug.Conn.assign/3\n\n` + `πŸ’‘ Check your application logs for detailed trace output.`); } async phoenixConnInspector(args, sessions) { const session = this.getSession(args.sessionId, sessions); // This would require hooking into the plug pipeline // For now, provide guidance on how to inspect conn const code = ` # Conn inspection helper %{ stage: "${args.stage}", inspection_point: case "${args.stage}" do "before_router" -> "Add plug in endpoint before router" "after_router" -> "Add plug after router in endpoint" "before_controller" -> "Add plug in controller pipeline" "after_controller" -> "Inspect in controller action" end, example_plug: """ defmodule MyAppWeb.ConnInspector do def init(opts), do: opts def call(conn, _opts) do IO.inspect(conn, label: "Conn at ${args.stage}") conn end end """ } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to setup conn inspector: ${result.error}`); } return this.createTextResponse(`πŸ”Œ **Conn Inspector Setup**\n\n` + `Stage: \`${args.stage}\`\n` + `Inspection Point: ${result.data.inspection_point}\n\n` + `**Example Plug:**\n` + '```elixir\n' + result.data.example_plug + '\n```\n\n' + `πŸ’‘ Add this plug to your pipeline to inspect conn state.`); } async phoenixSessionDebug(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` # Get session configuration endpoint = MyAppWeb.Endpoint session_config = endpoint.config(:session_options) || [] %{ store: session_config[:store], key: session_config[:key], signing_salt: session_config[:signing_salt] && "***REDACTED***", encryption_salt: session_config[:encryption_salt] && "***REDACTED***", same_site: session_config[:same_site], secure: session_config[:secure], http_only: session_config[:http_only], max_age: session_config[:max_age] } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to debug session: ${result.error}`); } return this.createTextResponse(this.formatSessionDebug(result.data, args.sessionKey)); } async phoenixPresenceTracker(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` # Track presence updates presence_module = MyApp.Presence topic = "${args.topic}" # Get current presences presences = presence_module.list(topic) # Set up tracking Phoenix.PubSub.subscribe(MyApp.PubSub, topic) %{ topic: topic, current_presences: map_size(presences), tracking_duration: ${args.duration}, message: "Subscribed to presence updates for #{topic}" } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to track presence: ${result.error}`); } return this.createTextResponse(`πŸ‘₯ **Presence Tracking**\n\n` + `Topic: \`${result.data.topic}\`\n` + `Current Presences: ${result.data.current_presences}\n` + `Duration: ${result.data.tracking_duration}s\n\n` + `βœ… ${result.data.message}\n\n` + `πŸ’‘ Presence updates will appear in your application logs.`); } async phoenixErrorHandlerTrace(args, sessions) { const session = this.getSession(args.sessionId, sessions); const code = ` # Simulate error and trace handling error_type = "${args.errorType}" # Trace ErrorView rendering :dbg.start() :dbg.tracer() :dbg.tp(MyAppWeb.ErrorView, :render, [{:_, [], [{:return_trace}]}]) :dbg.p(:all, :c) # Get error view template info templates = MyAppWeb.ErrorView.__templates__() %{ error_type: error_type, available_templates: templates, error_helpers: MyAppWeb.ErrorView.__info__(:functions) |> Enum.filter(fn {name, _} -> String.contains?(to_string(name), "error") end), tracing: true } `; const result = await this.tidewave.evaluateCode(session.url, code); if (!result.success) { throw new Error(`Failed to trace error handler: ${result.error}`); } return this.createTextResponse(this.formatErrorHandlerTrace(result.data)); } // Helper methods formatPlugTrace(data, args) { return `πŸ”Œ **Plug Pipeline Trace**\n\n` + `Request: ${args.method} ${args.path}\n` + `Status: ${data.status}\n` + `Halted: ${data.halted ? 'β›” Yes' : 'βœ… No'}\n\n` + `**Assigns:** ${data.assigns.join(', ') || 'None'}\n` + `**Private Keys:** ${data.private.join(', ')}\n\n` + `πŸ’‘ Check your logs for detailed timing information.`; } formatEndpointConfig(config) { let output = `βš™οΈ **Endpoint Configuration**\n\n`; if (config.url) { output += `**URL Configuration:**\n`; Object.entries(config.url).forEach(([key, value]) => { output += `β€’ ${key}: ${value}\n`; }); output += '\n'; } if (config.http) { output += `**HTTP:**\n`; output += `β€’ Port: ${config.http.port || 'default'}\n`; output += `β€’ IP: ${config.http.ip || 'all interfaces'}\n\n`; } if (config.https) { output += `**HTTPS:** βœ… Configured\n\n`; } output += `**Features:**\n`; output += `β€’ Server: ${config.server ? 'βœ…' : '❌'}\n`; output += `β€’ LiveView: ${config.live_view ? 'βœ…' : '❌'}\n`; output += `β€’ PubSub: ${config.pubsub_server || 'Not configured'}\n`; output += `β€’ Static Cache: ${config.cache_static_manifest ? 'βœ…' : '❌'}\n`; return output; } formatViewProfile(views) { if (!views || views.length === 0) { return `πŸ“Š **No Views Rendered**\n\nNo views were rendered during the profiling period.`; } let output = `⚑ **View Render Performance**\n\n`; let totalTime = 0; views.forEach((view, i) => { totalTime += view.total_time; output += `${i + 1}. **${view.view_template}**\n`; output += ` Renders: ${view.render_count}\n`; output += ` Avg: ${view.avg_time.toFixed(2)}ms\n`; output += ` Range: ${view.min_time.toFixed(2)}ms - ${view.max_time.toFixed(2)}ms\n`; output += ` Total: ${view.total_time.toFixed(2)}ms\n\n`; }); output += `**Summary:**\n`; output += `β€’ Total render time: ${totalTime.toFixed(2)}ms\n`; output += `β€’ Views rendered: ${views.length}\n`; const slowest = views[0]; if (slowest && slowest.avg_time > 50) { output += `\n⚠️ **Performance Warning:**\n`; output += `${slowest.view_template} is slow (${slowest.avg_time.toFixed(2)}ms avg)`; } return output; } formatRouterAnalysis(data, path) { let output = `πŸ—ΊοΈ **Router Analysis**\n\n`; output += `Total Routes: ${data.total_routes}\n\n`; if (path && data.path_analysis) { output += `**Path Analysis for ${path}:**\n`; output += `β€’ Plug: ${data.path_analysis.plug}\n`; output += `β€’ Action: ${data.path_analysis.plug_opts}\n\n`; } if (data.routes.length > 0) { output += `**Sample Routes:**\n`; data.routes.slice(0, 10).forEach((route) => { output += `β€’ ${route.verb} ${route.path}\n`; output += ` β†’ ${route.plug} ${route.helper ? `(${route.helper})` : ''}\n`; }); } if (data.helpers.length > 0) { output += `\n**Route Helpers:**\n`; data.helpers.forEach((helper) => { output += `β€’ ${helper}_path/url\n`; }); } return output; } formatTelemetryEvents(events) { if (!events || events.length === 0) { return `πŸ“Š **No Telemetry Events Captured**\n\nNo events were emitted during monitoring.`; } let output = `πŸ“‘ **Telemetry Events**\n\n`; output += `Captured ${events.length} events\n\n`; // Group by event type const grouped = {}; events.forEach(([name, measurements, metadata, timestamp]) => { const eventName = name.join('.'); if (!grouped[eventName]) grouped[eventName] = []; grouped[eventName].push({ measurements, metadata, timestamp }); }); Object.entries(grouped).forEach(([event, occurrences]) => { output += `**${event}** (${occurrences.length} times)\n`; // Show last occurrence const last = occurrences[occurrences.length - 1]; if (last.measurements.duration) { output += `β€’ Last duration: ${(last.measurements.duration / 1_000_000).toFixed(2)}ms\n`; } output += '\n'; }); return output; } formatSessionDebug(config, sessionKey) { let output = `πŸ” **Session Configuration**\n\n`; output += `**Storage:**\n`; output += `β€’ Store: ${config.store || 'cookie'}\n`; output += `β€’ Key: "${config.key || '_app_key'}"\n\n`; output += `**Security:**\n`; output += `β€’ Secure: ${config.secure ? 'βœ…' : '❌'} (HTTPS only)\n`; output += `β€’ HTTP Only: ${config.http_only ? 'βœ…' : '❌'}\n`; output += `β€’ Same Site: ${config.same_site || 'Not set'}\n\n`; output += `**Lifetime:**\n`; output += `β€’ Max Age: ${config.max_age ? `${config.max_age} seconds` : 'Session'}\n`; if (sessionKey) { output += `\nπŸ’‘ To inspect session value for key "${sessionKey}", add this to your controller:\n`; output += '```elixir\nIO.inspect(get_session(conn, "' + sessionKey + '"), label: "Session value")\n```'; } return output; } formatErrorHandlerTrace(data) { let output = `🚨 **Error Handler Trace**\n\n`; output += `Error Type: ${data.error_type}\n\n`; if (data.available_templates && data.available_templates.length > 0) { output += `**Available Error Templates:**\n`; data.available_templates.forEach((template) => { output += `β€’ ${template}\n`; }); output += '\n'; } if (data.error_helpers && data.error_helpers.length > 0) { output += `**Error Helper Functions:**\n`; data.error_helpers.forEach(([name, arity]) => { output += `β€’ ${name}/${arity}\n`; }); output += '\n'; } output += `βœ… ErrorView.render/2 is being traced.\n`; output += `πŸ’‘ Trigger a ${data.error_type} error to see the trace in your logs.`; return output; } } //# sourceMappingURL=phoenix-advanced-handler.js.map