Skip to main content

avalonia_mcp_tools/
debugging_assistant_tool.rs

1//! Debugging Assistant tool - Debugging guidance and patterns
2use avalonia_mcp_core::error::AvaloniaMcpError;
3use avalonia_mcp_core::markdown::MarkdownOutputBuilder;
4use rmcp::model::{CallToolResult, Content};
5use rmcp::tool;
6use serde::{Deserialize, Serialize};
7use schemars::JsonSchema;
8
9#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
10pub struct DebuggingAssistantParams {
11    pub issue_type: Option<String>,
12    pub include_solutions: Option<bool>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
16pub struct DebugUtilitiesParams {
17    pub utility_type: Option<String>,
18    pub include_devtools: Option<bool>,
19    pub include_telemetry: Option<bool>,
20}
21
22#[derive(Debug, Clone, Default)]
23pub struct DebuggingAssistantTool;
24
25impl DebuggingAssistantTool {
26    pub fn new() -> Self { Self }
27
28    #[tool(description = "Provide debugging assistance for AvaloniaUI applications. Covers common issues, debugging tools, breakpoints, and troubleshooting strategies.")]
29    pub async fn provide_debugging_assistance(
30        &self,
31        params: DebuggingAssistantParams,
32    ) -> Result<CallToolResult, AvaloniaMcpError> {
33        let include_solutions = params.include_solutions.unwrap_or(true);
34        let issue_type = params.issue_type.as_deref().unwrap_or("common");
35
36        let output = match issue_type {
37            "binding" => self.debug_binding_issues(include_solutions),
38            "layout" => self.debug_layout_issues(include_solutions),
39            "performance" => self.debug_performance_issues(include_solutions),
40            "memory" => self.debug_memory_issues(include_solutions),
41            _ => self.debug_common_issues(include_solutions),
42        };
43
44        Ok(CallToolResult::success(vec![Content::text(output)]))
45    }
46
47    #[tool(description = "Generates debug utilities and logging helpers for AvaloniaUI applications")]
48    pub async fn generate_debug_utilities(
49        &self,
50        params: DebugUtilitiesParams,
51    ) -> Result<CallToolResult, AvaloniaMcpError> {
52        let utility_type = params.utility_type.as_deref().unwrap_or("logger").to_lowercase();
53        let include_devtools = params.include_devtools.unwrap_or(true);
54        let include_telemetry = params.include_telemetry.unwrap_or(false);
55
56        let utility_code = match utility_type.as_str() {
57            "logger" => r#"public static class DebugLogger
58{
59    [Conditional("DEBUG")]
60    public static void Log(string message, [CallerMemberName] string member = "")
61    {
62        Console.WriteLine($"[{member}] {message}");
63    }
64
65    [Conditional("DEBUG")]
66    public static void LogBinding(object source, string property, object value)
67    {
68        Log($"Binding: {source.GetType().Name}.{property} = {value}");
69    }
70}"#,
71            "visualtree" => r#"public static class VisualTreeHelper
72{
73    public static void PrintVisualTree(Visual visual, int indent = 0)
74    {
75        var padding = new string(' ', indent * 2);
76        Console.WriteLine($"{padding}{visual.GetType().Name}");
77        foreach (var child in visual.GetVisualChildren())
78        {
79            PrintVisualTree(child, indent + 1);
80        }
81    }
82}"#,
83            "binding" => r#"public static class BindingDiagnostics
84{
85    public static void EnableBindingDebugging()
86    {
87        BindingLog.Enabled = true;
88        BindingLog.Subscribe(log =>
89        {
90            Console.WriteLine($"Binding: {log.Message}");
91        });
92    }
93}"#,
94            _ => "// Unknown utility type",
95        };
96
97        let mut builder = MarkdownOutputBuilder::new()
98            .heading(1, &format!("Debug Utilities: {}", utility_type))
99            .heading(2, "Utility Implementation")
100            .code_block("csharp", utility_code);
101
102        if include_devtools {
103            builder = builder
104                .heading(2, "DevTools Integration")
105                .code_block(
106                    "csharp",
107                    r#"// Enable DevTools in debug builds
108#if DEBUG
109app.SetupWithDevTools(options);
110#endif
111
112// Programmatic DevTools access
113public static void ShowDevTools()
114{
115    if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
116    {
117        var window = desktop.Windows.FirstOrDefault(w => w is DevToolsWindow);
118        window?.Show();
119    }
120}"#,
121                );
122        }
123
124        if include_telemetry {
125            builder = builder
126                .heading(2, "Telemetry Integration")
127                .code_block(
128                    "csharp",
129                    r#"public class AppTelemetry
130{
131    private readonly ILogger _logger;
132    public AppTelemetry(ILogger logger) => _logger = logger;
133
134    public void TrackEvent(string name, Dictionary<string, object>? properties = null)
135    {
136        _logger.LogInformation("Event: {Name} {@Properties}", name, properties);
137    }
138}"#,
139                );
140        }
141
142        builder = builder
143            .heading(2, "Setup Instructions")
144            .list(vec![
145                "Add utility classes to your project",
146                "Register services in Program.cs",
147                "Enable DevTools in debug builds only",
148                "Use structured logging",
149            ])
150            .heading(2, "Usage Tips")
151            .list(vec![
152                "Enable DevTools in debug builds only",
153                "Use structured logging for better analysis",
154                "Monitor performance metrics in production",
155            ]);
156
157        Ok(CallToolResult::success(vec![Content::text(builder.build())]))
158    }
159
160    fn debug_common_issues(&self, include_solutions: bool) -> String {
161        let mut builder = MarkdownOutputBuilder::new()
162            .heading(1, "Debugging Common Issues")
163            .paragraph("Troubleshooting guide for common AvaloniaUI issues.")
164            .heading(2, "Debugging Tools")
165            .list(vec![
166                "Visual Studio / Rider debugger",
167                "Avalonia DevTools (F12)",
168                "Serilog with file output",
169                "dotnet-trace for profiling",
170            ]);
171
172        if include_solutions {
173            builder = builder
174                .heading(2, "Binding Issues")
175                .code_block("csharp", r#"// Enable binding debug output
176AvaloniaLogging.LogLevel = LogLevel.Debug;
177AvaloniaLogging.Sink = new ConsoleSink
178{
179    Area = LogArea.Binding
180};
181
182// Common binding errors:
183// 1. Property not found: Check property name and INotifyPropertyChanged
184// 2. Type mismatch: Ensure source and target types match
185// 3. DataContext null: Set DataContext before bindings evaluate
186// 4. Thread affinity: Update UI on UI thread
187
188// Fix: Implement INotifyPropertyChanged properly
189public class ViewModel : INotifyPropertyChanged
190{
191    private string _name;
192    public string Name
193    {
194        get => _name;
195        set
196        {
197            _name = value;
198            OnPropertyChanged();
199        }
200    }
201    
202    public event PropertyChangedEventHandler PropertyChanged;
203    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
204        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
205}"#)
206                .heading(2, "Layout Issues")
207                .code_block("csharp", r#"// Debug layout with DevTools
208// Press F12 in running app
209
210// Common layout issues:
211// 1. Control not visible: Check Width/Height, Visibility, Parent container
212// 2. Overlapping controls: Check Grid.Row/Column, ZIndex
213// 3. Not filling space: Check Horizontal/VerticalAlignment
214// 4. Infinite measure: Avoid SizeToContent in ScrollViewer
215
216// Fix: Use layout debugging
217AvaloniaLogging.LogLevel = LogLevel.Debug;
218AvaloniaLogging.Sink = new ConsoleSink
219{
220    Area = LogArea.Layout
221};"#)
222                .heading(2, "Memory Leaks")
223                .code_block("csharp", r#"// Common memory leak sources:
224// 1. Event handlers not unsubscribed
225// 2. Static collections holding references
226// 3. Timers not stopped
227// 4. Observables not disposed
228
229// Fix: Use WeakEventManager for events
230WeakEventManager<EventHandler>.AddHandler(
231    button, 
232    nameof(button.Click), 
233    OnClick);
234
235// Fix: Dispose subscriptions
236private CompositeDisposable _disposables = new();
237
238observable.Subscribe(x => /* ... */)
239    .DisposeWith(_disposables);
240
241// Dispose in OnClosed
242protected override void OnClosed(EventArgs e)
243{
244    _disposables.Dispose();
245    base.OnClosed(e);
246}"#)
247                .heading(2, "Performance Issues")
248                .code_block("csharp", r#"// Profile with dotnet-trace
249// dotnet-trace collect --process-id <PID>
250
251// Common performance issues:
252// 1. Too many visual elements: Use virtualization
253// 2. Expensive bindings: Use x:CompileBindings
254// 3. Layout thrashing: Avoid frequent size changes
255// 4. Blocking UI thread: Use async/await
256
257// Fix: Enable UI virtualization
258<ListBox VirtualizationMode=\"Simple\">
259    <ListBox.ItemsPanel>
260        <ItemsPanelTemplate>
261            <VirtualizingStackPanel/>
262        </ItemsPanelTemplate>
263    </ListBox.ItemsPanel>
264</ListBox>"#);
265        }
266
267        builder.heading(2, "Debugging Checklist")
268            .task_list(vec![
269                (true, "Enable Avalonia DevTools"),
270                (true, "Check binding errors in output"),
271                (true, "Profile memory with dotnet-gcdump"),
272                (true, "Use breakpoints in ViewModels"),
273                (false, "Add logging for async operations"),
274            ])
275            .build()
276    }
277
278    fn debug_binding_issues(&self, _include_solutions: bool) -> String {
279        MarkdownOutputBuilder::new()
280            .heading(1, "Debugging Binding Issues")
281            .paragraph("Troubleshoot data binding problems.")
282            .heading(2, "Common Binding Errors")
283            .list(vec![
284                "Property not found on DataContext",
285                "Type conversion failed",
286                "Null reference in binding path",
287                "Collection not notifying changes",
288            ])
289            .heading(2, "Solutions")
290            .code_block("csharp", r#"// 1. Enable binding debug
291AvaloniaLogging.LogLevel = LogLevel.Debug;
292
293// 2. Check DataContext
294Debug.WriteLine($\"DataContext: {DataContext?.GetType().Name}\");
295
296// 3. Use compiled bindings for performance
297<x:CompileBindings>true</x:CompileBindings>
298
299// 4. Implement ObservableCollection for collections
300public ObservableCollection<Item> Items { get; } = new();"#)
301            .build()
302    }
303
304    fn debug_layout_issues(&self, _include_solutions: bool) -> String {
305        MarkdownOutputBuilder::new()
306            .heading(1, "Debugging Layout Issues")
307            .paragraph("Troubleshoot layout and rendering problems.")
308            .heading(2, "Common Layout Errors")
309            .list(vec![
310                "Control not visible",
311                "Incorrect sizing",
312                "Overlapping elements",
313                "Infinite measure loop",
314            ])
315            .heading(2, "Solutions")
316            .code_block("csharp", r#"// 1. Use DevTools (F12) to inspect visual tree
317// 2. Check HorizontalAlignment and VerticalAlignment
318// 3. Verify Grid.Row and Grid.Column attached properties
319// 4. Avoid SizeToContent=\"WidthAndHeight\" with ScrollViewer
320
321// Debug layout passes
322AvaloniaLogging.Sink = new ConsoleSink { Area = LogArea.Layout };"#)
323            .build()
324    }
325
326    fn debug_performance_issues(&self, _include_solutions: bool) -> String {
327        MarkdownOutputBuilder::new()
328            .heading(1, "Debugging Performance Issues")
329            .paragraph("Identify and fix performance bottlenecks.")
330            .heading(2, "Profiling Tools")
331            .list(vec!["dotnet-trace", "dotnet-gcdump", "Avalonia DevTools", "Visual Studio Profiler"])
332            .heading(2, "Common Issues")
333            .list(vec!["UI thread blocking", "Excessive GC", "Layout thrashing", "No virtualization"])
334            .build()
335    }
336
337    fn debug_memory_issues(&self, _include_solutions: bool) -> String {
338        MarkdownOutputBuilder::new()
339            .heading(1, "Debugging Memory Issues")
340            .paragraph("Find and fix memory leaks.")
341            .heading(2, "Memory Profiling")
342            .code_block("bash", r#"# Collect memory dump
343dotnet-gcdump collect --process-id <PID>
344
345# Analyze
346dotnet-dump analyze <dump>
347> gcroot
348> gcstat"#)
349            .heading(2, "Common Leaks")
350            .list(vec!["Event handlers", "Static collections", "Timers", "Observables"])
351            .build()
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    #[tokio::test]
359    async fn test_provide_debugging_assistance() {
360        let tool = DebuggingAssistantTool::new();
361        let result = tool.provide_debugging_assistance(DebuggingAssistantParams { issue_type: None, include_solutions: Some(true) }).await.unwrap();
362        assert!(result.is_error.is_none() || result.is_error == Some(false));
363    }
364
365    #[tokio::test]
366    async fn test_generate_debug_utilities_logger() {
367        let tool = DebuggingAssistantTool::new();
368        let params = DebugUtilitiesParams {
369            utility_type: Some("logger".to_string()),
370            include_devtools: Some(true),
371            include_telemetry: Some(false),
372        };
373        let result = tool.generate_debug_utilities(params).await.unwrap();
374        assert!(result.is_error.is_none() || result.is_error == Some(false));
375        assert!(result.content.len() > 0);
376    }
377
378    #[tokio::test]
379    async fn test_generate_debug_utilities_visualtree() {
380        let tool = DebuggingAssistantTool::new();
381        let params = DebugUtilitiesParams {
382            utility_type: Some("visualtree".to_string()),
383            include_devtools: Some(false),
384            include_telemetry: Some(false),
385        };
386        let result = tool.generate_debug_utilities(params).await.unwrap();
387        assert!(result.is_error.is_none() || result.is_error == Some(false));
388    }
389
390    #[tokio::test]
391    async fn test_generate_debug_utilities_with_telemetry() {
392        let tool = DebuggingAssistantTool::new();
393        let params = DebugUtilitiesParams {
394            utility_type: Some("binding".to_string()),
395            include_devtools: Some(true),
396            include_telemetry: Some(true),
397        };
398        let result = tool.generate_debug_utilities(params).await.unwrap();
399        assert!(result.is_error.is_none() || result.is_error == Some(false));
400    }
401
402    #[tokio::test]
403    async fn test_generate_debug_utilities_unknown_type() {
404        let tool = DebuggingAssistantTool::new();
405        let params = DebugUtilitiesParams {
406            utility_type: Some("unknown".to_string()),
407            include_devtools: Some(false),
408            include_telemetry: Some(false),
409        };
410        let result = tool.generate_debug_utilities(params).await.unwrap();
411        assert!(result.is_error.is_none() || result.is_error == Some(false));
412    }
413
414    #[tokio::test]
415    async fn test_generate_debug_utilities_defaults() {
416        let tool = DebuggingAssistantTool::new();
417        let params = DebugUtilitiesParams {
418            utility_type: None,
419            include_devtools: None,
420            include_telemetry: None,
421        };
422        let result = tool.generate_debug_utilities(params).await.unwrap();
423        assert!(result.is_error.is_none() || result.is_error == Some(false));
424    }
425}