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