diff --git a/Changelog.md b/Changelog.md index 380c17d3..9ddeaa77 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,24 +2,14 @@ ## 6.1.0 (preview) -- ### Bug fixes - - - [#477](../../issues/477) - Binding Errors - Get error message - - [#483](../../issues/483) - Error when I try to GetBindingExpression from TextProperty - - [#484](../../issues/484) - Snoop fails to attach to application running on .NET Framework on ARM64 - -- ### Improvements +- ### New Features - - [#457](../../issues/457) - [Quality of Life] Synchronize Zoomer instance background slider value. - - [#482](../../issues/482) - Add "Auto-track on click" option (thanks @Koichi-Kobayashi) - - [#485](../../issues/485) - Snoop running on ARM hardware is using x64 emulation instead of the native Arm64 - - **MCP (Model Context Protocol) Server Integration** - AI assistant integration for automated WPF inspection - - Added MCP server with 7 tools: get_visual_tree, get_selected_element, get_element_properties, select_element, find_elements, get_bindings, get_element_preview - - Integrated official MCP SDK for .NET 8+ builds with fallback implementation for .NET 6/Framework + - **MCP (Model Context Protocol) Server Integration** - Added AI assistant integration for automated WPF inspection and debugging + - Enables AI assistants like Claude to interact with and inspect WPF applications in real-time + - 7 MCP tools: get_visual_tree, get_selected_element, get_element_properties, select_element, find_elements, get_bindings, get_element_preview + - Official MCP SDK support for .NET 8+ with fallback implementation for .NET 6/Framework - Server-Sent Events (SSE) transport on localhost:47700-47799 - - Dedicated MCP Server window for server control and configuration - - Visual element screenshot capability using RenderTargetBitmap - - Enables AI assistants like Claude to debug, inspect, and analyze WPF applications in real-time + - Dedicated MCP Server window for easy configuration and control ## 6.0.0 diff --git a/Directory.packages.props b/Directory.packages.props index ac3d0aac..ef29f8de 100644 --- a/Directory.packages.props +++ b/Directory.packages.props @@ -6,6 +6,7 @@ + diff --git a/README.md b/README.md index 676a2a21..e01e2975 100644 --- a/README.md +++ b/README.md @@ -69,95 +69,9 @@ The MCP server exposes 7 tools for AI assistants: The MCP server runs on `localhost:47700-47799` using Server-Sent Events (SSE) transport. -## Versions +## Changelog -You can read the [changelog](Changelog.md) for details on what changed in which version. - -### [6.0.0](../../releases/tag/v6.0.0) - -Breaking: - -- Dropped support for all .NET Framework versions prior to .NET 4.6.2 -- Dropped support for .NET 3.1 and NET 5 - -### [5.0.0](../../releases/tag/v5.0.0) - -Highlights: - -- Improved settings system that does not rely on `System.Configuration` - The new system allows sharing of settings between different snooped applications. - It also allows to define settings for whole directory trees. -- It's now possible to hide properties from Snoop in it's default view. - Just annotate your properties with `[System.ComponentModel.BrowsableAttribute(false)]`. -- Added the ability to show browser dev tools on browser controls. - `WebView2` and `CefSharp` are currently supported. -- Added dark theme - -Breaking: - -- Dropped support for all .NET Framework versions prior to .NET 4.5.2 -- Dropped support for .NET 3.0 -- Added support for .NET versions >= 6.0 (by not explicitly blocking versions greater than 6.0) - -### [4.0.0](../../releases/tag/v4.0.0) - -Highlights: - -- Support for .NET 6.0 -- Support for ARM/ARM64 -- New "Diagnostics" view -- Settings for highlighting -- Artifacts are digitally signed thanks to SignPath.io (MSI, Chocolatey NUPKG and zip) - -Breaking: - -- Dropped support for all .NET versions prior to .NET 4.5.1 - -### [3.0.0](../../releases/tag/v3.0.0) - -Highlights: - -- Support for .NET Core (3.0, 3.1 and 5.0) (including self contained and single file applications) -- Rewritten injector code -- You no longer have to have installed any Microsoft Visual C++ Redistributable(s) -- Snooping disabled controls when holding `CTRL + SHIFT` works now -- Snoop now filters uncommon properties by default -- Snoop is now able to show `MergedDictionaries` from `ResourceDictionary` -- Snoop now has two tracking modes. - - Holding `CTRL` tries to skip template parts => this is changed to `CTRL + ALT` in newer versions - - Holding `CTRL + SHIFT` does not skip template parts -- Drastically improved performance of `AppChooser.Refresh()` (thanks @mikel785) -- Usability improvements for process dropdown (thanks @mikel785) -- Support for displaying the logical tree and the tree of WPF automation peers -- Ability to inspect `Popup` without opening it -- `Snoop.exe` and the injector launcher now support commandline args -- Global hotkey support (just start snoop, focus a WPF application and hit `CTRL + WIN + ALT + F12`) - -Known issues: - -- Trying to snoop a trimmed single file application might not work as trimming might have removed things Snoop relies on - -### [2.11.0](../../releases/tag/2.11.0) - -Highlights: - -- Support for multiple app domains -- Auto elevation to enable spying of elevated processes without running Snoop as administrator -- Persistent settings for various settings -- Improved error dialog and issue reporting -- Rewritten window finder - -### [2.10.0](../../releases/tag/2.10.0) - -Was released on September 19th, 2018. -In this version we finally got rid of support for snooping WPF 3.5 applications. -This allowed us to move the Snoop projects forward to Visual Studio 2017 which should make it much easier to work with Snoop's source code. - -### [2.9.0](../../releases/tag/2.9.0) - -Was released on July 27th, 2018. -The big addition in this version was the inclusion of the triggers tab which was a useful feature of another WPF spying utility called WPF Inspector (written by [Christan Moser](https://github.com/ChristianMoser)). -It was ported to Snoop by Bastian Schmidt. +You can read the [changelog](Changelog.md) for details on what changed in each version. ## Documentation on how to use Snoop diff --git a/Snoop.Core/Infrastructure/MCP/McpServer.cs b/Snoop.Core/Infrastructure/MCP/McpServer.cs index 6dff19e3..f0a0d37a 100644 --- a/Snoop.Core/Infrastructure/MCP/McpServer.cs +++ b/Snoop.Core/Infrastructure/MCP/McpServer.cs @@ -26,8 +26,8 @@ namespace Snoop.Infrastructure.MCP; #else using System.Collections.Generic; using System.IO; -using System.Net; using System.Linq; +using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -146,9 +146,6 @@ public async Task StartAsync(int preferredPort = 0) builder.Logging.AddDebug(); builder.Logging.SetMinimumLevel(LogLevel.Warning); - // Configure the server URL - builder.WebHost.UseUrls($"http://localhost:{portToUse}"); - // Add MCP server with the Snoop tools builder.Services.AddSingleton(snoopContext); builder.Services @@ -158,6 +155,9 @@ public async Task StartAsync(int preferredPort = 0) this.webApplication = builder.Build(); + // Configure the server URL + this.webApplication.Urls.Add($"http://localhost:{portToUse}"); + // Map the MCP endpoints (/sse and /messages) this.webApplication.MapMcp(); @@ -166,7 +166,7 @@ public async Task StartAsync(int preferredPort = 0) { try { - await this.webApplication.RunAsync(this.cancellationTokenSource.Token); + await this.webApplication.RunAsync(); } catch (OperationCanceledException) { @@ -176,7 +176,7 @@ public async Task StartAsync(int preferredPort = 0) { System.Diagnostics.Debug.WriteLine($"MCP Server error: {ex.Message}"); } - }); + }, this.cancellationTokenSource!.Token); // Give the server a moment to start await Task.Delay(100); @@ -248,15 +248,15 @@ public void Dispose() #else // Fallback implementation for .NET Framework and .NET 6 - public async Task StartAsync(int preferredPort = 0) + public Task StartAsync(int preferredPort = 0) { if (this.IsRunning) { - return true; + return Task.FromResult(true); } this.cancellationTokenSource = new CancellationTokenSource(); - this.sessionId = Guid.NewGuid().ToString("N")[..8]; + this.sessionId = Guid.NewGuid().ToString("N").Substring(0, 8); // Try to find an available port var portsToTry = preferredPort > 0 @@ -276,7 +276,7 @@ public async Task StartAsync(int preferredPort = 0) // Start listening for requests _ = Task.Run(() => this.ListenAsync(this.cancellationTokenSource.Token)); - return true; + return Task.FromResult(true); } catch (HttpListenerException) { @@ -285,7 +285,7 @@ public async Task StartAsync(int preferredPort = 0) } } - return false; + return Task.FromResult(false); } public void Stop() @@ -390,7 +390,7 @@ private async Task HandleSseConnectionAsync(HttpListenerContext context, Cancell response.Headers.Add("Cache-Control", "no-cache"); response.Headers.Add("Connection", "keep-alive"); - using var writer = new StreamWriter(response.OutputStream, Encoding.UTF8, leaveOpen: true); + using var writer = new StreamWriter(response.OutputStream, Encoding.UTF8, 1024, leaveOpen: true); // Send the endpoint event with the messages URL var endpointEvent = new @@ -505,7 +505,7 @@ private object HandleToolsList() { return new { - tools = new[] + tools = new object[] { new { @@ -649,7 +649,7 @@ private async Task HandleToolCallAsync(JsonElement root) var toolName = paramsElement.GetProperty("name").GetString(); var arguments = paramsElement.TryGetProperty("arguments", out var args) ? args : default; - return await this.dispatcher.InvokeAsync(() => + return await this.dispatcher.InvokeAsync(() => { try { @@ -665,7 +665,7 @@ private async Task HandleToolCallAsync(JsonElement root) _ => throw new InvalidOperationException($"Unknown tool: {toolName}") }; - return new + return (object)new { content = new[] { @@ -679,7 +679,7 @@ private async Task HandleToolCallAsync(JsonElement root) } catch (Exception ex) { - return new + return (object)new { content = new[] { @@ -721,7 +721,7 @@ private object ExecuteGetVisualTree(JsonElement arguments) return new { - tree = this.BuildTreeNode(root, 0, maxDepth, includeProperties, "") + tree = this.BuildTreeNode(root, 0, maxDepth, includeProperties, string.Empty) }; } @@ -792,12 +792,12 @@ private object BuildTreeNode(TreeItem item, int currentDepth, int maxDepth, bool if (depObj is System.Windows.Controls.TextBlock tb) { - props["Text"] = tb.Text?.Length > 100 ? tb.Text[..100] + "..." : tb.Text; + props["Text"] = tb.Text?.Length > 100 ? tb.Text.Substring(0, 100) + "..." : tb.Text; } if (depObj is System.Windows.Controls.TextBox textBox) { - props["Text"] = textBox.Text?.Length > 100 ? textBox.Text[..100] + "..." : textBox.Text; + props["Text"] = textBox.Text?.Length > 100 ? textBox.Text.Substring(0, 100) + "..." : textBox.Text; } return props; @@ -905,7 +905,7 @@ private string FormatValue(object? value) if (value is string s) { - return s.Length > 200 ? s[..200] + "..." : s; + return s.Length > 200 ? s.Substring(0, 200) + "..." : s; } if (value is Brush brush) @@ -971,7 +971,7 @@ private object ExecuteFindElements(JsonElement arguments) } var results = new List(); - this.SearchTree(root, typeName, elementName, "", results, maxResults); + this.SearchTree(root, typeName, elementName, string.Empty, results, maxResults); return new { @@ -1199,7 +1199,7 @@ private object ExecuteGetElementPreview(JsonElement arguments) return null; } - var indices = path.Split('/').Select(int.Parse).ToArray(); + var indices = path!.Split('/').Select(int.Parse).ToArray(); var current = root; foreach (var index in indices) @@ -1224,7 +1224,7 @@ private async Task SendJsonResponseAsync(HttpListenerResponse response, object d var buffer = Encoding.UTF8.GetBytes(json); response.ContentLength64 = buffer.Length; - await response.OutputStream.WriteAsync(buffer); + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); response.Close(); } #endif diff --git a/Snoop.Core/Infrastructure/MCP/SnoopMcpTools.cs b/Snoop.Core/Infrastructure/MCP/SnoopMcpTools.cs index b568e2b8..02083c5a 100644 --- a/Snoop.Core/Infrastructure/MCP/SnoopMcpTools.cs +++ b/Snoop.Core/Infrastructure/MCP/SnoopMcpTools.cs @@ -24,7 +24,9 @@ namespace Snoop.Infrastructure.MCP; public class SnoopContext { public required Func GetCurrentSelection { get; init; } + public required Func GetRootTreeItem { get; init; } + public required Action SelectItem { get; init; } } @@ -40,7 +42,8 @@ public sealed class SnoopMcpTools WriteIndented = true }; - [McpServerTool("get_visual_tree"), Description("Get the visual tree structure of the currently inspected WPF application. Returns a hierarchical view of all UI elements.")] + [McpServerTool] + [Description("get_visual_tree: Get the visual tree structure of the currently inspected WPF application. Returns a hierarchical view of all UI elements.")] public static string GetVisualTree( SnoopContext context, [Description("Maximum depth to traverse (default: 10, max: 50)")] int maxDepth = 10, @@ -54,11 +57,12 @@ public static string GetVisualTree( maxDepth = Math.Clamp(maxDepth, 1, 50); - var tree = BuildTreeNode(root, 0, maxDepth, includeProperties, ""); + var tree = BuildTreeNode(root, 0, maxDepth, includeProperties, string.Empty); return JsonSerializer.Serialize(new { tree }, JsonOptions); } - [McpServerTool("get_selected_element"), Description("Get detailed information about the currently selected element in the Snoop inspector, including its properties.")] + [McpServerTool] + [Description("get_selected_element: Get detailed information about the currently selected element in the Snoop inspector, including its properties.")] public static string GetSelectedElement(SnoopContext context) { var selection = context.GetCurrentSelection(); @@ -86,7 +90,8 @@ public static string GetSelectedElement(SnoopContext context) return JsonSerializer.Serialize(result, JsonOptions); } - [McpServerTool("get_element_properties"), Description("Get all properties of a specific element identified by its path in the visual tree.")] + [McpServerTool] + [Description("get_element_properties: Get all properties of a specific element identified by its path in the visual tree.")] public static string GetElementProperties( SnoopContext context, [Description("Path to the element (e.g., '0/2/1' for root's child 0, then child 2, then child 1)")] string path, @@ -112,7 +117,8 @@ public static string GetElementProperties( }, JsonOptions); } - [McpServerTool("select_element"), Description("Select an element in the visual tree by its path, making it the current selection in Snoop.")] + [McpServerTool] + [Description("select_element: Select an element in the visual tree by its path, making it the current selection in Snoop.")] public static string SelectElement( SnoopContext context, [Description("Path to the element (e.g., '0/2/1')")] string path) @@ -137,7 +143,8 @@ public static string SelectElement( }, JsonOptions); } - [McpServerTool("find_elements"), Description("Find elements in the visual tree by type name or element name.")] + [McpServerTool] + [Description("find_elements: Find elements in the visual tree by type name or element name.")] public static string FindElements( SnoopContext context, [Description("Type name to search for (e.g., 'Button', 'TextBox')")] string? typeName = null, @@ -156,7 +163,7 @@ public static string FindElements( } var results = new List(); - SearchTree(root, typeName, elementName, "", results, maxResults); + SearchTree(root, typeName, elementName, string.Empty, results, maxResults); return JsonSerializer.Serialize(new { @@ -165,7 +172,8 @@ public static string FindElements( }, JsonOptions); } - [McpServerTool("get_bindings"), Description("Get all data bindings on the currently selected element, including binding errors.")] + [McpServerTool] + [Description("get_bindings: Get all data bindings on the currently selected element, including binding errors.")] public static string GetBindings(SnoopContext context) { var selection = context.GetCurrentSelection(); @@ -227,7 +235,8 @@ public static string GetBindings(SnoopContext context) }, JsonOptions); } - [McpServerTool("get_element_preview"), Description("Capture a visual preview/screenshot of the currently selected element as a base64-encoded PNG image.")] + [McpServerTool] + [Description("get_element_preview: Capture a visual preview/screenshot of the currently selected element as a base64-encoded PNG image.")] public static string GetElementPreview( SnoopContext context, [Description("Maximum width of the preview image (default: 400)")] int maxWidth = 400, diff --git a/Snoop.Core/Snoop.Core.csproj b/Snoop.Core/Snoop.Core.csproj index 5e55e3ea..fe5fd4ef 100644 --- a/Snoop.Core/Snoop.Core.csproj +++ b/Snoop.Core/Snoop.Core.csproj @@ -7,6 +7,9 @@ true true true + + + 11 @@ -42,4 +45,9 @@ + + + + + \ No newline at end of file diff --git a/Snoop.Core/Windows/McpServerWindow.xaml b/Snoop.Core/Windows/McpServerWindow.xaml index dff992bf..6d443894 100644 --- a/Snoop.Core/Windows/McpServerWindow.xaml +++ b/Snoop.Core/Windows/McpServerWindow.xaml @@ -1,4 +1,4 @@ - - -]]> \ No newline at end of file + \ No newline at end of file