From 24720e1a294c84fec213823e60dd81098af4ccb8 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Thu, 13 Aug 2026 23:11:04 +0200 Subject: [PATCH 01/33] Fix auto-injection packaging --- .github/workflows/build.yml | 67 +++++++++++++++++++ .github/workflows/release.yml | 61 +++++++++++++++++ CHANGELOG.md | 11 +++ .../InspectorService.cs | 40 +++++++++++ .../WpfVisualTreeMcp.Server.csproj | 41 ++++++------ 5 files changed, 198 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83a774c..2ce1286 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,14 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v1.3 + + - name: Build native bootstrapper + run: | + msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=x64 + msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=Win32 + - name: Restore dependencies run: dotnet restore WpfVisualTreeMcp.sln @@ -43,6 +51,65 @@ jobs: - name: Publish MCP Server run: dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --no-build --configuration Release --output ./publish + - name: Verify auto-injection payload + shell: pwsh + run: | + $netFxInspectorDlls = Get-ChildItem ./src/WpfVisualTreeMcp.Inspector/bin/Release/net48/*.dll + $requiredPayloadFiles = @( + './publish/native/x64/WpfInspectorBootstrapper.dll' + './publish/native/x86/WpfInspectorBootstrapper.dll' + './publish/native/x64/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json' + './publish/native/x86/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json' + foreach ($architecture in @('x64', 'x86')) { + foreach ($dll in $netFxInspectorDlls) { + "./publish/native/$architecture/$($dll.Name)" + } + } + ) + $missingPayloadFiles = @($requiredPayloadFiles | Where-Object { -not (Test-Path -LiteralPath $_) }) + if ($missingPayloadFiles.Count -gt 0) { + throw "Missing auto-injection payload files: $($missingPayloadFiles -join ', ')" + } + + - name: Pack NuGet package + run: dotnet pack src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --no-build --configuration Release --output ./artifacts + + - name: Verify NuGet auto-injection payload + shell: pwsh + run: | + $package = Get-ChildItem ./artifacts/*.nupkg | Select-Object -First 1 + if ($null -eq $package) { throw 'NuGet package was not created.' } + + $netFxInspectorDllNames = Get-ChildItem ./src/WpfVisualTreeMcp.Inspector/bin/Release/net48/*.dll | + Select-Object -ExpandProperty Name + $requiredEntrySuffixes = @( + foreach ($architecture in @('x64', 'x86')) { + "/native/$architecture/WpfInspectorBootstrapper.dll" + "/native/$architecture/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json" + foreach ($dllName in $netFxInspectorDllNames) { + "/native/$architecture/$dllName" + } + } + ) + + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [System.IO.Compression.ZipFile]::OpenRead($package.FullName) + try { + $entryNames = @($archive.Entries.FullName) + $missingEntrySuffixes = @($requiredEntrySuffixes | Where-Object { + $requiredSuffix = $_ + -not ($entryNames | Where-Object { + $_.EndsWith($requiredSuffix, [StringComparison]::OrdinalIgnoreCase) + }) + }) + if ($missingEntrySuffixes.Count -gt 0) { + throw "NuGet package is missing auto-injection payload entries: $($missingEntrySuffixes -join ', ')" + } + } + finally { + $archive.Dispose() + } + - name: Upload artifacts uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a68594..e6ec8a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,11 @@ jobs: - name: Setup MSBuild uses: microsoft/setup-msbuild@v1.3 + - name: Build native bootstrapper + run: | + msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=x64 + msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=Win32 + - name: Restore dependencies run: dotnet restore WpfVisualTreeMcp.sln @@ -44,6 +49,26 @@ jobs: - name: Publish MCP Server run: dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --no-build --configuration Release --output ./publish/server + - name: Verify auto-injection payload + shell: pwsh + run: | + $netFxInspectorDlls = Get-ChildItem ./src/WpfVisualTreeMcp.Inspector/bin/Release/net48/*.dll + $requiredPayloadFiles = @( + './publish/server/native/x64/WpfInspectorBootstrapper.dll' + './publish/server/native/x86/WpfInspectorBootstrapper.dll' + './publish/server/native/x64/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json' + './publish/server/native/x86/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json' + foreach ($architecture in @('x64', 'x86')) { + foreach ($dll in $netFxInspectorDlls) { + "./publish/server/native/$architecture/$($dll.Name)" + } + } + ) + $missingPayloadFiles = @($requiredPayloadFiles | Where-Object { -not (Test-Path -LiteralPath $_) }) + if ($missingPayloadFiles.Count -gt 0) { + throw "Missing auto-injection payload files: $($missingPayloadFiles -join ', ')" + } + - name: Publish Inspector DLL run: dotnet publish src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj --configuration Release --framework net8.0-windows --output ./publish/inspector @@ -54,6 +79,42 @@ jobs: - name: Pack NuGet package run: dotnet pack src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --configuration Release --output ./artifacts + - name: Verify NuGet auto-injection payload + shell: pwsh + run: | + $package = Get-ChildItem ./artifacts/*.nupkg | Select-Object -First 1 + if ($null -eq $package) { throw 'NuGet package was not created.' } + + $netFxInspectorDllNames = Get-ChildItem ./src/WpfVisualTreeMcp.Inspector/bin/Release/net48/*.dll | + Select-Object -ExpandProperty Name + $requiredEntrySuffixes = @( + foreach ($architecture in @('x64', 'x86')) { + "/native/$architecture/WpfInspectorBootstrapper.dll" + "/native/$architecture/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json" + foreach ($dllName in $netFxInspectorDllNames) { + "/native/$architecture/$dllName" + } + } + ) + + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [System.IO.Compression.ZipFile]::OpenRead($package.FullName) + try { + $entryNames = @($archive.Entries.FullName) + $missingEntrySuffixes = @($requiredEntrySuffixes | Where-Object { + $requiredSuffix = $_ + -not ($entryNames | Where-Object { + $_.EndsWith($requiredSuffix, [StringComparison]::OrdinalIgnoreCase) + }) + }) + if ($missingEntrySuffixes.Count -gt 0) { + throw "NuGet package is missing auto-injection payload entries: $($missingEntrySuffixes -join ', ')" + } + } + finally { + $archive.Dispose() + } + # Trusted publishing (OIDC): requires a policy at nuget.org/account/trustedpublishing # for repo faze79/WPFVisualTreeMcp, workflow release.yml, package WpfVisualTreeMcp, # plus a repo secret NUGET_USER = the nuget.org profile name. Skipped with a notice diff --git a/CHANGELOG.md b/CHANGELOG.md index f28ff8c..1351674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Build and package the x64 and x86 native bootstrappers so Auto-injection works from the + NuGet tool and release archive. +- Package the complete .NET Framework Inspector dependency closure and resolve its + co-located private assemblies without relying on the target application's binding redirects. +- Use the standard CoreCLR runtimeconfig filename so it remains accessible from the NuGet + tool's deeply nested installation directory. + ## [0.12.0] - 2026-07-24 ### Added diff --git a/src/WpfVisualTreeMcp.Inspector/InspectorService.cs b/src/WpfVisualTreeMcp.Inspector/InspectorService.cs index c6c85fe..be25111 100644 --- a/src/WpfVisualTreeMcp.Inspector/InspectorService.cs +++ b/src/WpfVisualTreeMcp.Inspector/InspectorService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Text.Json; using System.Threading.Tasks; using System.Windows; @@ -28,6 +29,21 @@ public class InspectorService : IDisposable private bool _disposed; private static readonly object _initLock = new(); +#if NET48 + private static readonly HashSet _privateDependencyNames = new(StringComparer.OrdinalIgnoreCase) + { + "Microsoft.Bcl.AsyncInterfaces", + "System.Buffers", + "System.Memory", + "System.Numerics.Vectors", + "System.Runtime.CompilerServices.Unsafe", + "System.Text.Encodings.Web", + "System.Text.Json", + "System.Threading.Tasks.Extensions", + "System.ValueTuple", + }; + private static bool _dependencyResolverRegistered; +#endif public static InspectorService? Instance { get; private set; } /// @@ -105,6 +121,9 @@ public static void Initialize(int processId) try { DebugLog($"Inspector.Initialize called for PID={processId}"); +#if NET48 + RegisterDependencyResolver(); +#endif Instance = new InspectorService(processId); DebugLog("Inspector instance created, calling Start()"); Instance.Start(); @@ -119,6 +138,27 @@ public static void Initialize(int processId) } } +#if NET48 + private static void RegisterDependencyResolver() + { + if (_dependencyResolverRegistered) return; + + var inspectorDirectory = Path.GetDirectoryName(typeof(InspectorService).Assembly.Location); + if (string.IsNullOrEmpty(inspectorDirectory)) return; + + AppDomain.CurrentDomain.AssemblyResolve += (_, args) => + { + var assemblyName = new AssemblyName(args.Name).Name; + if (string.IsNullOrEmpty(assemblyName) || !_privateDependencyNames.Contains(assemblyName)) + return null; + + var assemblyPath = Path.Combine(inspectorDirectory, assemblyName + ".dll"); + return File.Exists(assemblyPath) ? Assembly.LoadFrom(assemblyPath) : null; + }; + _dependencyResolverRegistered = true; + } +#endif + private InspectorService(int processId) { _treeWalker = new TreeWalker(); diff --git a/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj b/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj index 7e50c7d..3719476 100644 --- a/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj +++ b/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj @@ -72,16 +72,9 @@ PreserveNewest PreserveNewest - - - PreserveNewest - PreserveNewest - - + + PreserveNewest PreserveNewest @@ -92,16 +85,9 @@ PreserveNewest PreserveNewest - - - PreserveNewest - PreserveNewest - - + + PreserveNewest PreserveNewest @@ -152,7 +138,7 @@ PreserveNewest + Link="native\x64\coreclr\WpfVisualTreeMcp.Inspector.runtimeconfig.json"> PreserveNewest PreserveNewest @@ -170,7 +156,7 @@ PreserveNewest + Link="native\x86\coreclr\WpfVisualTreeMcp.Inspector.runtimeconfig.json"> PreserveNewest PreserveNewest @@ -182,4 +168,15 @@ + + + + + + + From 3d784de229ff5a0569f79c5e4a0e72cfdc5cead0 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 00:44:23 +0200 Subject: [PATCH 02/33] Document auto-injection limitations --- README.md | 33 ++++++- src/WpfVisualTreeMcp.Injector/README.md | 121 ++++++++++++++---------- 2 files changed, 102 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 5f02ff6..80d6eda 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ command list. Output is JSON on stdout; diagnostics go to stderr. | Interaction surface | click (UIA + physical, double/right), set-text w/ read-back, send-keys, **select-item (virtualized)** | click / type / select | usually invoke-only | | Wait for element conditions | ✅ `wpf_wait_for` | ✅ | ❌ | | Popup / dropdown / context-menu screenshots | ✅ screen mode | partial | screenshot only | -| Cross-architecture injection (x64 ⇄ x86) | ✅ | n/a | ❌ | +| Cross-architecture injection (x64 server → x86 target) | ✅ | n/a | ❌ | | Dual-mode: MCP server **and** one-shot CLI | ✅ | ❌ | ❌ | | Distribution | NuGet (`dnx`/tool) + official MCP registry | varies | varies | @@ -226,7 +226,36 @@ Add to your Cursor settings (`.cursor/mcp.json`): } ``` -### Self-Hosted Mode (Recommended) +### Auto-Injection Mode + +Auto-injection loads the Inspector into an already-running WPF process without +source changes. Set `auto_inject=true` when calling `wpf_attach`, or run: + +```powershell +wpfinspect attach --pid --auto-inject +``` + +Auto-injection has these constraints: + +- It requires permission to open the target process, write memory, and create a + remote thread. Elevated, protected, sandboxed, or security-hardened processes + may reject it, and endpoint security may block it as DLL injection. +- The matching x64 or x86 native bootstrapper and the complete Inspector + dependency set must be present. A 64-bit server additionally needs the + bundled x86 helper and the x86 .NET 8 runtime to inject into a 32-bit target. + Native ARM64 targets are not supported. +- Injection occurs after process startup, so it cannot recover earlier binding + errors or initialization activity. A restarted application must be injected + again under its new process ID. +- The target must have an initialized WPF `Application` and a responsive UI + dispatcher. The injected Inspector remains loaded until the target exits. +- Loading native and managed code into the target can conflict with its runtime, + assembly versions, or process-hardening policy. + +See the [injector documentation](src/WpfVisualTreeMcp.Injector/README.md) for the +implementation, runtime requirements, diagnostics, and detailed limitations. + +### Self-Hosted Mode For your WPF application to be inspectable, add a reference to the Inspector DLL and initialize it on startup: diff --git a/src/WpfVisualTreeMcp.Injector/README.md b/src/WpfVisualTreeMcp.Injector/README.md index cca4e56..4aa2878 100644 --- a/src/WpfVisualTreeMcp.Injector/README.md +++ b/src/WpfVisualTreeMcp.Injector/README.md @@ -1,16 +1,60 @@ # WpfVisualTreeMcp.Injector -This project handles injection of the Inspector DLL into target WPF processes. - -## Current Status - -The injector is currently a **stub implementation**. Full DLL injection into external .NET processes requires advanced techniques that are beyond the scope of the initial implementation. - -## Injection Approaches - -### Option 1: Self-Hosted Mode (Recommended for Development) - -For development and testing, the recommended approach is to have your WPF application directly reference the Inspector DLL: +This project injects the WPF Inspector into an already-running WPF process. It +is used by both the MCP server and the one-shot CLI when auto-injection is +requested. + +## How Auto-Injection Works + +1. `ProcessInjector` verifies that the target is managed and detects its + architecture. +2. It selects the matching x64 or x86 `WpfInspectorBootstrapper.dll`. +3. For a same-bitness target, it uses `OpenProcess`, `VirtualAllocEx`, + `WriteProcessMemory`, and `CreateRemoteThread` to call `LoadLibraryW` in the + target. A 64-bit server launches the bundled 32-bit `WpfInjectorHelper.exe` + for an x86 target. +4. The native bootstrapper detects the loaded CLR. It uses + `ExecuteInDefaultAppDomain` for .NET Framework and `hostfxr` for CoreCLR. +5. The bootstrapper loads `WpfVisualTreeMcp.Inspector.dll`, which starts the + named-pipe endpoint used by the server. + +Auto-injection does not require source changes to the target application. + +## Requirements and Limitations + +- **Windows and architecture:** Native bootstrappers are provided for x64 and + x86. Native ARM64 targets are not supported. Cross-bitness injection from the + normal 64-bit server into an x86 target requires `WpfInjectorHelper.exe` and + the x86 .NET 8 runtime. +- **Process access:** The injector needs process-query, remote-thread, and + virtual-memory access. Integrity-level differences, protected or sandboxed + processes, endpoint security, and process-hardening policies can deny these + operations. +- **Runtime compatibility:** .NET Framework targets load the `net48` Inspector + in the default AppDomain and require the .NET Framework 4.8 runtime. CoreCLR + targets load the `net8.0-windows` Inspector through `hostfxr` and require a + compatible Windows Desktop runtime. Custom CLR hosts and incompatible loaded + runtimes may reject the Inspector. +- **Complete payload:** The architecture directory must contain the matching + native bootstrapper, Inspector assembly, and managed dependency closure. + CoreCLR injection also requires the Inspector runtime configuration. + `LoadLibraryW` or managed initialization fails when the payload is incomplete. +- **Application state:** Injection starts the pipe server immediately, but WPF + operations require `Application.Current` and a responsive UI dispatcher. + Requests time out while the UI thread is blocked. +- **Timing and lifetime:** Auto-injection cannot observe binding errors or UI + initialization that occurred before attachment. A restarted process must be + injected again. Detaching the external client does not unload the Inspector; + it remains in the target until process exit. +- **Target stability:** Injection loads native and managed code, starts threads, + registers listeners, and resolves private dependencies inside the target. + Applications with conflicting assemblies or unusual hosting arrangements may + be destabilized. + +## Self-Hosted Alternative + +When the application source can be changed, self-hosting avoids remote-process +injection and gives the application control over startup and disposal: ```csharp // In your WPF application's App.xaml.cs @@ -25,49 +69,26 @@ protected override void OnStartup(StartupEventArgs e) This avoids the complexity of injection and provides the most reliable experience. -### Option 2: Native Injection (Future) - -For production scenarios where you need to attach to arbitrary WPF applications, the following approaches can be considered: - -1. **CreateRemoteThread with LoadLibrary** - - Classic DLL injection technique - - Requires a native C++ helper for bootstrapping managed code +Self-hosting is appropriate when policy blocks injection, diagnostics must start +with the application, or injection conflicts with the target's runtime. It +requires a target-framework-compatible Inspector reference. -2. **CLR Debugging APIs (ICorDebug)** - - Uses the .NET debugging infrastructure - - Can create threads and load assemblies in the target process +## Diagnostics -3. **EasyHook or Similar Libraries** - - Third-party libraries that simplify managed injection - - Handle the complexity of cross-process managed code loading +Run the smallest attachment attempt with verbose logging: -4. **AppDomain Injection via Profiling API** - - Uses the CLR profiling infrastructure - - Most invasive but most capable option - -## Why Injection is Complex - -Injecting managed code (.NET) into another managed process is significantly more complex than native DLL injection because: - -1. The CLR must be properly initialized in the target process -2. The injected assembly must be loaded into the correct AppDomain -3. The injected code must run on the correct thread (usually the UI thread for WPF) -4. .NET Core/5+ has different hosting requirements than .NET Framework - -## Recommended Development Workflow - -1. **During Development** - - Use self-hosted mode in your test applications - - Reference the Inspector DLL directly - -2. **For Testing with External Apps** - - Build a test harness that loads Inspector on startup - - Use this for integration testing +```powershell +wpfinspect attach --pid --auto-inject --verbose +``` -3. **Future Production** - - Implement proper injection using one of the approaches above - - Or consider a different architecture (e.g., a Visual Studio extension) +The native bootstrapper writes initialization details to +`%TEMP%\WpfInspectorBootstrapper.log`. When running as an MCP server, logs are +stored under `%LOCALAPPDATA%\WpfVisualTreeMcp\logs`. ## Files -- `ProcessInjector.cs` - Stub implementation with P/Invoke declarations for future use +- `ProcessInjector.cs` - Target discovery, architecture selection, and remote + `LoadLibraryW` injection +- `../WpfVisualTreeMcp.Bootstrapper/WpfInspectorBootstrapper.cpp` - Native CLR + bootstrapper +- `../WpfVisualTreeMcp.InjectorHelper/Program.cs` - x86 cross-bitness helper From 77d304d737552c22c3b63b2bd905b44544e275f5 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 00:53:21 +0200 Subject: [PATCH 03/33] Add .NET Framework 4.7.2 support --- README.md | 4 +++- .../WpfVisualTreeMcp.Inspector.csproj | 6 +++--- src/WpfVisualTreeMcp.Shared/WpfVisualTreeMcp.Shared.csproj | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5f02ff6..9b558f4 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,8 @@ public partial class App : Application ``` This enables the MCP server to connect to your application via named pipes for real-time inspection. +The Inspector multi-targets .NET Framework 4.7.2, .NET Framework 4.8, and +.NET 8 for Windows, so project references select a compatible build. ## Usage Examples @@ -469,7 +471,7 @@ WpfVisualTreeMcp/ │ │ ├── WpfTools.cs # 20 WPF tools (17 inspection + click/set-text/send-keys) │ │ ├── Cli/CliRunner.cs # One-shot CLI front-end (v0.4.0) │ │ └── Services/ # Process & IPC management -│ ├── WpfVisualTreeMcp.Inspector/ # Injected DLL (.NET Framework 4.8) +│ ├── WpfVisualTreeMcp.Inspector/ # Injected DLL (.NET Framework 4.7.2/4.8 and .NET 8) │ ├── WpfVisualTreeMcp.Injector/ # Managed injection logic (CreateRemoteThread; net48 + net8.0) │ ├── WpfVisualTreeMcp.InjectorHelper/# x86 .NET 8 helper exe for cross-arch injection (v0.6.0) │ ├── WpfVisualTreeMcp.Bootstrapper/ # Native C++ DLL for CLR hosting diff --git a/src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj b/src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj index 87ab961..8437e6c 100644 --- a/src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj +++ b/src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj @@ -1,7 +1,7 @@ - net48;net8.0-windows + net472;net48;net8.0-windows Library WpfVisualTreeMcp.Inspector WpfVisualTreeMcp.Inspector @@ -10,9 +10,9 @@ true - + - + diff --git a/src/WpfVisualTreeMcp.Shared/WpfVisualTreeMcp.Shared.csproj b/src/WpfVisualTreeMcp.Shared/WpfVisualTreeMcp.Shared.csproj index 36134d1..30b3e48 100644 --- a/src/WpfVisualTreeMcp.Shared/WpfVisualTreeMcp.Shared.csproj +++ b/src/WpfVisualTreeMcp.Shared/WpfVisualTreeMcp.Shared.csproj @@ -1,7 +1,7 @@ - net8.0;net48 + net8.0;net472;net48 enable enable 12.0 @@ -9,7 +9,7 @@ - + From 978e30f70af137c0c4d5c79859cac1f026919da5 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 02:22:03 +0200 Subject: [PATCH 04/33] Remove .NET Framework target from injector --- .../WpfVisualTreeMcp.Injector.csproj | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/WpfVisualTreeMcp.Injector/WpfVisualTreeMcp.Injector.csproj b/src/WpfVisualTreeMcp.Injector/WpfVisualTreeMcp.Injector.csproj index 68746ba..9c5b260 100644 --- a/src/WpfVisualTreeMcp.Injector/WpfVisualTreeMcp.Injector.csproj +++ b/src/WpfVisualTreeMcp.Injector/WpfVisualTreeMcp.Injector.csproj @@ -1,12 +1,7 @@ - - net48;net8.0 + net8.0 Library WpfVisualTreeMcp.Injector WpfVisualTreeMcp.Injector From 4aab3bbb19b2a1c073699d7f870d34bc4adaec6c Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 04:31:40 +0200 Subject: [PATCH 05/33] Multitarget shared tests --- WpfVisualTreeMcp.sln | 7 +++++ .../IpcSerializerTests.cs | 0 .../SharedModelsTests.cs | 0 .../WpfVisualTreeMcp.Shared.Tests.csproj | 30 +++++++++++++++++++ 4 files changed, 37 insertions(+) rename tests/{WpfVisualTreeMcp.Tests => WpfVisualTreeMcp.Shared.Tests}/IpcSerializerTests.cs (100%) rename tests/{WpfVisualTreeMcp.Tests => WpfVisualTreeMcp.Shared.Tests}/SharedModelsTests.cs (100%) create mode 100644 tests/WpfVisualTreeMcp.Shared.Tests/WpfVisualTreeMcp.Shared.Tests.csproj diff --git a/WpfVisualTreeMcp.sln b/WpfVisualTreeMcp.sln index f7d05a8..2f645e7 100644 --- a/WpfVisualTreeMcp.sln +++ b/WpfVisualTreeMcp.sln @@ -31,6 +31,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution mcp.json = mcp.json EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfVisualTreeMcp.Shared.Tests", "tests\WpfVisualTreeMcp.Shared.Tests\WpfVisualTreeMcp.Shared.Tests.csproj", "{17056590-7FA9-414F-9772-6C60C56C1A5D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -65,6 +67,10 @@ Global {F6A7B8C9-D0E1-2345-F012-456789012345}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6A7B8C9-D0E1-2345-F012-456789012345}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6A7B8C9-D0E1-2345-F012-456789012345}.Release|Any CPU.Build.0 = Release|Any CPU + {17056590-7FA9-414F-9772-6C60C56C1A5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17056590-7FA9-414F-9772-6C60C56C1A5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17056590-7FA9-414F-9772-6C60C56C1A5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17056590-7FA9-414F-9772-6C60C56C1A5D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -77,6 +83,7 @@ Global {B0A1C2D3-E4F5-6789-0123-456789ABCDEF} = {1A2B3C4D-5E6F-7890-1234-567890ABCDEF} {E5F6A7B8-C9D0-1234-EF01-345678901234} = {2B3C4D5E-6F78-9012-3456-7890ABCDEF01} {F6A7B8C9-D0E1-2345-F012-456789012345} = {3C4D5E6F-7890-1234-5678-90ABCDEF0123} + {17056590-7FA9-414F-9772-6C60C56C1A5D} = {3C4D5E6F-7890-1234-5678-90ABCDEF0123} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {12345678-90AB-CDEF-1234-567890ABCDEF} diff --git a/tests/WpfVisualTreeMcp.Tests/IpcSerializerTests.cs b/tests/WpfVisualTreeMcp.Shared.Tests/IpcSerializerTests.cs similarity index 100% rename from tests/WpfVisualTreeMcp.Tests/IpcSerializerTests.cs rename to tests/WpfVisualTreeMcp.Shared.Tests/IpcSerializerTests.cs diff --git a/tests/WpfVisualTreeMcp.Tests/SharedModelsTests.cs b/tests/WpfVisualTreeMcp.Shared.Tests/SharedModelsTests.cs similarity index 100% rename from tests/WpfVisualTreeMcp.Tests/SharedModelsTests.cs rename to tests/WpfVisualTreeMcp.Shared.Tests/SharedModelsTests.cs diff --git a/tests/WpfVisualTreeMcp.Shared.Tests/WpfVisualTreeMcp.Shared.Tests.csproj b/tests/WpfVisualTreeMcp.Shared.Tests/WpfVisualTreeMcp.Shared.Tests.csproj new file mode 100644 index 0000000..325fad8 --- /dev/null +++ b/tests/WpfVisualTreeMcp.Shared.Tests/WpfVisualTreeMcp.Shared.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0;net472;net48 + enable + enable + 12.0 + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + From 9130b850538e5244768f62fac0b3b8346e8bc45a Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 04:40:56 +0200 Subject: [PATCH 06/33] Move Inspector packaging dependency to Server --- .../ProcessInjector.cs | 18 ++++++++++++++++-- .../WpfVisualTreeMcp.Injector.csproj | 4 ---- .../WpfVisualTreeMcp.Server.csproj | 5 +++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/WpfVisualTreeMcp.Injector/ProcessInjector.cs b/src/WpfVisualTreeMcp.Injector/ProcessInjector.cs index e203f97..53a1fda 100644 --- a/src/WpfVisualTreeMcp.Injector/ProcessInjector.cs +++ b/src/WpfVisualTreeMcp.Injector/ProcessInjector.cs @@ -363,8 +363,22 @@ public bool IsInspectorLoaded(Process process) public string GetInspectorDllPath() { var assemblyLocation = typeof(ProcessInjector).Assembly.Location; - var directory = Path.GetDirectoryName(assemblyLocation); - return Path.Combine(directory!, "WpfVisualTreeMcp.Inspector.dll"); + var directory = Path.GetDirectoryName(assemblyLocation)!; + var fileName = "WpfVisualTreeMcp.Inspector.dll"; + var candidates = new[] + { + Path.Combine(directory, fileName), + Path.Combine(directory, "native", "x64", fileName), + Path.Combine(directory, "native", "x86", fileName), + }; + + foreach (var candidate in candidates) + { + if (File.Exists(candidate)) + return candidate; + } + + return candidates[1]; } /// diff --git a/src/WpfVisualTreeMcp.Injector/WpfVisualTreeMcp.Injector.csproj b/src/WpfVisualTreeMcp.Injector/WpfVisualTreeMcp.Injector.csproj index 9c5b260..60e69b3 100644 --- a/src/WpfVisualTreeMcp.Injector/WpfVisualTreeMcp.Injector.csproj +++ b/src/WpfVisualTreeMcp.Injector/WpfVisualTreeMcp.Injector.csproj @@ -10,8 +10,4 @@ true - - - - diff --git a/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj b/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj index 3719476..b402e9e 100644 --- a/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj +++ b/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj @@ -50,6 +50,11 @@ + + $(NoWarn);NU1903 From 980ab6d594c8758a4c881696db8c579f141eac10 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 06:39:29 +0200 Subject: [PATCH 09/33] Avoid reinjecting a loaded Inspector --- .../Services/ProcessManager.cs | 3 ++- .../InspectionModeMatrixTests.cs | 20 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs b/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs index 607ffcb..4c9e8c6 100644 --- a/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs +++ b/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs @@ -129,7 +129,8 @@ public async Task AttachToProcessAsync(int? processId, string targetProcess.Id, targetProcess.ProcessName); // Check if Inspector is already loaded (self-hosted mode) - var inspectorLoaded = IsInspectorLoaded(targetProcess); + var inspectorLoaded = IsInspectorLoaded(targetProcess) || + await WaitForInspectorPipeAsync(targetProcess.Id, TimeSpan.FromMilliseconds(700)); if (inspectorLoaded) { _logger.LogInformation("Inspector DLL already loaded in target process (self-hosted mode)"); diff --git a/tests/WpfVisualTreeMcp.IntegrationTests/InspectionModeMatrixTests.cs b/tests/WpfVisualTreeMcp.IntegrationTests/InspectionModeMatrixTests.cs index d19601d..355bc9a 100644 --- a/tests/WpfVisualTreeMcp.IntegrationTests/InspectionModeMatrixTests.cs +++ b/tests/WpfVisualTreeMcp.IntegrationTests/InspectionModeMatrixTests.cs @@ -90,11 +90,21 @@ public async Task Cli_inspects_sample_for_target_architecture_and_mode( { attachJson.RootElement.GetProperty("success").GetBoolean().Should().BeTrue(); attachJson.RootElement.GetProperty("processId").GetInt32().Should().Be(sample.Id); - if (autoInject) - { - attachJson.RootElement.GetProperty("inspectorStatus").GetString() - .Should().Be("Loaded (injected)"); - } + attachJson.RootElement.GetProperty("inspectorStatus").GetString() + .Should().Be(autoInject ? "Loaded (injected)" : "Loaded (self-hosted)"); + } + + if (autoInject) + { + var secondAttach = await RunCliAsync(attachArguments, TimeSpan.FromSeconds(20)); + secondAttach.ExitCode.Should().Be(0, FormatCommandFailure("second attach", secondAttach)); + + using var secondAttachJson = JsonDocument.Parse(secondAttach.StandardOutput); + secondAttachJson.RootElement.GetProperty("success").GetBoolean().Should().BeTrue(); + secondAttachJson.RootElement.GetProperty("processId").GetInt32().Should().Be(sample.Id); + secondAttachJson.RootElement.GetProperty("inspectorStatus").GetString() + .Should().Be("Loaded (self-hosted)", + "the second attach should reuse the loaded Inspector without reinjecting it"); } var find = await FindSubmitButtonAsync(sample.Id, TimeSpan.FromSeconds(15)); From 0e43d1197d773e625e07bb4c9ffb3fc4313d1a9b Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 07:05:38 +0200 Subject: [PATCH 10/33] Run the WPF inspection matrix in CI --- .github/workflows/build.yml | 46 +++++++++++++++------- .github/workflows/publish-mcp-registry.yml | 2 +- .github/workflows/release.yml | 8 ++-- tests/run-integration-tests.ps1 | 4 ++ 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ce1286..6104205 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,15 +17,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.3 + uses: microsoft/setup-msbuild@v3 - name: Build native bootstrapper run: | @@ -41,8 +41,23 @@ jobs: - name: Test run: dotnet test WpfVisualTreeMcp.sln --no-build --configuration Release --verbosity normal --logger "trx;LogFileName=test-results.trx" + - name: Install x86 .NET runtime + shell: pwsh + run: | + $dotnetRoot = Join-Path $env:RUNNER_TEMP 'dotnet-x86' + $installScript = Join-Path $env:RUNNER_TEMP 'dotnet-install.ps1' + Invoke-WebRequest https://dot.net/v1/dotnet-install.ps1 -OutFile $installScript + & $installScript -Channel 8.0 -Runtime dotnet -Architecture x86 -InstallDir $dotnetRoot -NoPath + & $installScript -Channel 8.0 -Runtime windowsdesktop -Architecture x86 -InstallDir $dotnetRoot -NoPath + if (-not (Test-Path -LiteralPath (Join-Path $dotnetRoot 'dotnet.exe'))) { throw 'x86 .NET runtime installation failed.' } + "DOTNET_ROOT_X86=$dotnetRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Test WPF inspection matrix + shell: pwsh + run: ./tests/run-integration-tests.ps1 -SkipNativeBuild + - name: Upload test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: test-results @@ -111,7 +126,7 @@ jobs: } - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wpf-visual-tree-mcp path: ./publish @@ -121,14 +136,14 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: ${{ env.DOTNET_VERSION }} - # Server transitively references the WPF (net8.0-windows) Inspector, so a + # Server has a build-only reference to the WPF Inspector payload, so a # restore/format on Linux needs Windows targeting enabled. - name: Restore dependencies run: dotnet restore src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj -p:EnableWindowsTargeting=true @@ -143,21 +158,22 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.3 + uses: microsoft/setup-msbuild@v3 - name: Restore NuGet packages run: dotnet restore WpfVisualTreeMcp.sln - - name: Build .NET Framework projects + - name: Build .NET Framework targets run: | - dotnet build src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj --configuration Release - dotnet build src/WpfVisualTreeMcp.Injector/WpfVisualTreeMcp.Injector.csproj --configuration Release - dotnet build samples/SampleWpfApp/SampleWpfApp.csproj --configuration Release + dotnet build src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj --configuration Release --framework net472 + dotnet build src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj --configuration Release --framework net48 + dotnet build samples/SampleWpfApp/SampleWpfApp.csproj --configuration Release --framework net472 + dotnet build samples/SampleWpfApp/SampleWpfApp.csproj --configuration Release --framework net48 diff --git a/.github/workflows/publish-mcp-registry.yml b/.github/workflows/publish-mcp-registry.yml index 94de03c..19c02c8 100644 --- a/.github/workflows/publish-mcp-registry.yml +++ b/.github/workflows/publish-mcp-registry.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Download mcp-publisher run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e6ec8a7..2465b0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,15 +22,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.3 + uses: microsoft/setup-msbuild@v3 - name: Build native bootstrapper run: | @@ -155,7 +155,7 @@ jobs: } - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v3 with: body_path: release-notes.md files: | diff --git a/tests/run-integration-tests.ps1 b/tests/run-integration-tests.ps1 index 5072013..354bc4c 100644 --- a/tests/run-integration-tests.ps1 +++ b/tests/run-integration-tests.ps1 @@ -52,6 +52,7 @@ function Assert-X86DotNetRuntime { if ([string]::IsNullOrWhiteSpace($dotnetRoot)) { $dotnetRoot = Join-Path ${env:ProgramFiles(x86)} 'dotnet' } + $env:DOTNET_ROOT_X86 = $dotnetRoot $dotnet = Join-Path $dotnetRoot 'dotnet.exe' if (-not (Test-Path -LiteralPath $dotnet)) { @@ -140,6 +141,9 @@ foreach ($targetFramework in @('net472', 'net48', 'net8.0-windows')) { } } +# Leave the shared intermediate output in its normal, non-RID-specific state. +Invoke-ExternalCommand dotnet @('restore', $sample) + Invoke-ExternalCommand dotnet @('build', $integrationTests, '--configuration', 'Release') $previousServer = $env:WPF_VISUAL_TREE_MCP_INTEGRATION_SERVER From 8b73714a099333bf4c256c08042a61ace78267a4 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 07:05:38 +0200 Subject: [PATCH 11/33] Refresh framework and test documentation --- CLAUDE.md | 13 +++++++++---- README.md | 29 +++++++++++++++++++++-------- docs/ARCHITECTURE.md | 21 ++++++++++----------- docs/GETTING_STARTED.md | 9 ++++++--- docs/TOOLS_REFERENCE.md | 4 +++- 5 files changed, 49 insertions(+), 27 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 783f9d4..7bcd3e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,8 +18,11 @@ dotnet build -c Release WpfVisualTreeMcp.sln # Run tests dotnet test WpfVisualTreeMcp.sln +# Run the full self-hosted/auto-injection matrix (Windows PowerShell) +./tests/run-integration-tests.ps1 + # Run sample WPF app for testing -dotnet run --project samples/SampleWpfApp +dotnet run --project samples/SampleWpfApp --framework net8.0-windows # Publish MCP Server executable (same exe also runs as the CLI) dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj -c Release -o ./publish @@ -129,6 +132,8 @@ The Inspector strips UTF-8 BOM (0xEF 0xBB 0xBF) before JSON parsing to prevent d dropdowns, context menus and tooltips — requires the window visible and unobstructed) - DPI-aware via `PresentationSource.FromVisual` - Downscales if exceeding `max_width`/`max_height` (default 1920x1080) +- Captures the element's current arranged bounds; it does not scroll and stitch + off-screen content, and virtualized items that have not been realized are absent - Returns MCP `ImageContentBlock` (base64 PNG) — Claude sees the image directly ### Logging @@ -140,9 +145,9 @@ The Inspector strips UTF-8 BOM (0xEF 0xBB 0xBF) before JSON parsing to prevent d ## WPF App Inspection Modes ### Auto-Injection Mode -Use `wpf_attach(process_id=, auto_inject=true)` to inject the Inspector into any running .NET Framework WPF app. Requires: +Use `wpf_attach(process_id=, auto_inject=true)` to inject the Inspector into a running .NET Framework or .NET 8 WPF app. Requires: - Native bootstrapper DLL in `publish/native/x64/` (or x86) -- Target process must be .NET Framework (CLR hosting) +- A supported CLR and matching Inspector payload (`net48` for .NET Framework or `net8.0-windows` for CoreCLR) - Architecture detection is automatic (x64 vs x86) ### Self-Hosted Mode @@ -177,7 +182,7 @@ The server uses the official Microsoft/Anthropic MCP SDK. Configure in `.mcp.jso The server executable doubles as a command-line tool. `WpfVisualTreeMcp.Server.exe` with **no arguments** runs the MCP stdio server; with **any recognised subcommand** it runs a single one-shot CLI command instead (`Program.cs` checks `args[0]` via -`CliRunner.IsCliCommand`). This gives the same 21 capabilities without an MCP +`CliRunner.IsCliCommand`). This gives the same 28 capabilities without an MCP connection — useful when the MCP server is not connected, for scripting, or for verifying the pipeline manually. diff --git a/README.md b/README.md index 9b558f4..3e252a7 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ In short: UIA-based tools see what accessibility exposes, and computer-use agent - Windows 10/11 - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or later - A WPF application to inspect +- Visual Studio 2022 C++ build tools when building the auto-injection payload from source +- x86 .NET 8 Windows Desktop runtime when running the cross-bitness integration tests ### Installation @@ -359,7 +361,13 @@ For detailed architecture documentation, see [docs/ARCHITECTURE.md](docs/ARCHITE | `wpf_get_layout_info` | Get layout information | | `wpf_export_tree` | Export visual tree to XAML or JSON | -For complete tool documentation, see [docs/TOOLS_REFERENCE.md](docs/TOOLS_REFERENCE.md). +Screenshot capture covers the element's current arranged bounds. It does not +scroll and stitch off-screen content, and virtualized items that have not been +realized are not available to capture. + +For detailed examples of the original inspection tools, see +[docs/TOOLS_REFERENCE.md](docs/TOOLS_REFERENCE.md); run `wpfinspect help` for +complete CLI documentation. ## Roadmap @@ -452,13 +460,16 @@ Contributions are welcome! Please feel free to submit a Pull Request. 3. Build and run tests: ```bash - dotnet build - dotnet test + dotnet build WpfVisualTreeMcp.sln -c Release + dotnet test WpfVisualTreeMcp.sln -c Release + + # Full 12-case self-hosted/auto-injection matrix (Windows PowerShell) + ./tests/run-integration-tests.ps1 ``` 4. Run the sample WPF app for testing: ```bash - dotnet run --project samples/SampleWpfApp + dotnet run --project samples/SampleWpfApp --framework net8.0-windows ``` ### Project Structure @@ -468,18 +479,20 @@ WpfVisualTreeMcp/ ├── src/ │ ├── WpfVisualTreeMcp.Server/ # MCP Server (.NET 8) - Uses official MCP SDK │ │ ├── Program.cs # Server initialization with MCP SDK -│ │ ├── WpfTools.cs # 20 WPF tools (17 inspection + click/set-text/send-keys) +│ │ ├── WpfTools.cs # 28 WPF tools (22 inspection + 6 state-changing) │ │ ├── Cli/CliRunner.cs # One-shot CLI front-end (v0.4.0) │ │ └── Services/ # Process & IPC management │ ├── WpfVisualTreeMcp.Inspector/ # Injected DLL (.NET Framework 4.7.2/4.8 and .NET 8) -│ ├── WpfVisualTreeMcp.Injector/ # Managed injection logic (CreateRemoteThread; net48 + net8.0) +│ ├── WpfVisualTreeMcp.Injector/ # Managed injection logic (CreateRemoteThread; .NET 8) │ ├── WpfVisualTreeMcp.InjectorHelper/# x86 .NET 8 helper exe for cross-arch injection (v0.6.0) │ ├── WpfVisualTreeMcp.Bootstrapper/ # Native C++ DLL for CLR hosting │ └── WpfVisualTreeMcp.Shared/ # Shared models & IPC contracts ├── samples/ │ └── SampleWpfApp/ # Test application ├── tests/ -│ └── WpfVisualTreeMcp.Tests/ # Unit tests (48 tests) +│ ├── WpfVisualTreeMcp.Tests/ # Server, CLI and injector unit tests +│ ├── WpfVisualTreeMcp.Shared.Tests/ # Shared contract tests across all target frameworks +│ └── WpfVisualTreeMcp.IntegrationTests/ # 3 frameworks × 2 architectures × 2 modes ├── publish/ # Published server + native DLLs │ └── native/{x64,x86}/ # Architecture-specific bootstrapper └── docs/ # Documentation @@ -489,7 +502,7 @@ WpfVisualTreeMcp/ - **MCP SDK**: Built with the [official C# MCP SDK](https://github.com/modelcontextprotocol/csharp-sdk) from Microsoft/Anthropic - **Protocol**: JSON-RPC 2.0 over stdio transport -- **Target Framework**: .NET 8.0 (Server) / .NET Framework 4.8 + .NET 8.0-windows (Inspector, dual-target) +- **Target Framework**: .NET 8.0 (Server/Injector) / .NET Framework 4.7.2, 4.8 + .NET 8.0-windows (Inspector) - **IPC**: Named Pipes for server-to-application communication - **Tools**: 28 tools auto-discovered via `[McpServerTool]` attributes (22 read-only inspection incl. `wpf_wait_for`, `wpf_snapshot`, `wpf_diff`, `wpf_evaluate_binding`, `wpf_explain_triggers` + 6 state-changing: `wpf_click_element`, `wpf_select_item`, `wpf_set_text`, `wpf_send_keys`, `wpf_set_property`, `wpf_revert_property`) - **CLI**: same executable runs as one-shot CLI when given a subcommand (`Program.cs` routes via `CliRunner.IsCliCommand`) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 088fdb8..56a9553 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -27,9 +27,9 @@ The MCP Server is the main entry point that communicates with AI agents via the ### 2. Inspector DLL (`WpfVisualTreeMcp.Inspector`) -The Inspector is a .NET Framework library that gets loaded into target WPF applications to perform inspection operations. +The Inspector is loaded into target WPF applications to perform inspection and interaction operations. -**Technology:** .NET Framework 4.8 (for maximum compatibility with WPF apps) +**Technology:** .NET Framework 4.7.2, .NET Framework 4.8, and .NET 8 for Windows **Responsibilities:** - Access `VisualTreeHelper` and `LogicalTreeHelper` @@ -48,7 +48,7 @@ The Inspector is a .NET Framework library that gets loaded into target WPF appli The Injector is responsible for loading the Inspector DLL into target WPF processes. -**Technology:** C++/CLI or managed code +**Technology:** .NET 8 managed injection logic, native C++ bootstrapper, and an x86 .NET 8 helper **Responsibilities:** - Enumerate running WPF processes @@ -170,18 +170,17 @@ public async Task GetVisualTreeAsync(ElementHandle root) ### Process Isolation - The MCP Server runs in a separate process from target applications -- Inspector DLL has read-only access to the visual tree -- No modification of application state is possible through inspection +- The Inspector runs in-process and can inspect the visual tree +- Interaction and live-property tools intentionally modify application state ### Named Pipe Security -- Pipes are created with appropriate ACLs -- Only the MCP Server process can connect -- Pipe names include process IDs to prevent collisions +- Pipes are local to the machine and their names include process IDs +- The protocol does not authenticate the connecting client ### Injection Safety -- Only .NET Framework WPF applications can be inspected -- Injection uses safe managed code techniques -- Target application stability is preserved +- .NET Framework and .NET 8 WPF applications are supported +- Auto-injection loads native and managed code by creating a remote thread +- Process permissions, endpoint security, runtime conflicts, or blocked UI threads can prevent injection or affect the target ## Error Handling diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index e28b781..1071baa 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -31,7 +31,7 @@ Before you begin, ensure you have: src/WpfVisualTreeMcp.Server/bin/Debug/net8.0/WpfVisualTreeMcp.Server.exe ``` -### Option 2: .NET Tool Installation (Coming Soon) +### Option 2: .NET Tool Installation ```bash dotnet tool install -g WpfVisualTreeMcp @@ -134,6 +134,9 @@ Add a reference to `WpfVisualTreeMcp.Inspector` in your WPF project: ``` +The Inspector supports WPF applications targeting .NET Framework 4.7.2, +.NET Framework 4.8, and .NET 8 for Windows. + ### Step 2: Initialize the Inspector In your `App.xaml.cs`, initialize the inspector on startup: @@ -175,7 +178,7 @@ Either start your own WPF application (with the Inspector set up as above) or us ```bash cd WpfVisualTreeMcp -dotnet run --project samples/SampleWpfApp +dotnet run --project samples/SampleWpfApp --framework net8.0-windows ``` The sample app already has the Inspector configured. @@ -255,7 +258,7 @@ What styles are defined in this application? ### "No WPF applications found" - Ensure the target application is running -- Check that it's a .NET Framework WPF application +- Check that it is a supported .NET Framework or .NET 8 WPF application - The application must have a main window visible ### "Failed to attach to process" or "Inspector not loaded" diff --git a/docs/TOOLS_REFERENCE.md b/docs/TOOLS_REFERENCE.md index 288e5b1..3a8147a 100644 --- a/docs/TOOLS_REFERENCE.md +++ b/docs/TOOLS_REFERENCE.md @@ -1,6 +1,8 @@ # Tools Reference -Complete reference for all MCP tools provided by WpfVisualTreeMcp. +Detailed reference for the original inspection tools provided by +WpfVisualTreeMcp. For the current list of all 28 tools, see the +[README](../README.md#available-tools) or run `wpfinspect help`. ## Process Management From fa91eecab9db280300196310eca1ef37867279d1 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 07:45:33 +0200 Subject: [PATCH 12/33] Fix clean CI validation --- .github/workflows/build.yml | 2 ++ tests/WpfVisualTreeMcp.Tests/ProcessInjectorTests.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6104205..5baecac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -150,6 +150,8 @@ jobs: - name: Check formatting run: dotnet format src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --verify-no-changes --verbosity diagnostic + env: + EnableWindowsTargeting: true continue-on-error: true # Build verification for .NET Framework projects (Windows only) diff --git a/tests/WpfVisualTreeMcp.Tests/ProcessInjectorTests.cs b/tests/WpfVisualTreeMcp.Tests/ProcessInjectorTests.cs index 438b437..0f82292 100644 --- a/tests/WpfVisualTreeMcp.Tests/ProcessInjectorTests.cs +++ b/tests/WpfVisualTreeMcp.Tests/ProcessInjectorTests.cs @@ -68,7 +68,7 @@ public void InjectIntoProcess_WithInvalidProcessId_ThrowsInvalidOperationExcepti { // Arrange var invalidProcessId = int.MaxValue - 1; - var dllPath = _injector.GetInspectorDllPath(); + var dllPath = typeof(ProcessInjector).Assembly.Location; // Act & Assert var act = () => _injector.InjectIntoProcess(invalidProcessId, dllPath); From 09b43fdd49fffc2ee8c0e7b07c2621e5b81376c3 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 08:03:21 +0200 Subject: [PATCH 13/33] Skip payload reference during packaging --- src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj b/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj index b402e9e..003a277 100644 --- a/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj +++ b/src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj @@ -52,6 +52,7 @@ From 35c1177c9df6c6960f96b8d063e62d5b4e84b72a Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 08:35:14 +0200 Subject: [PATCH 14/33] Address consolidated review feedback --- src/WpfVisualTreeMcp.Injector/README.md | 2 +- .../InspectorService.cs | 8 +++-- .../Services/ProcessManager.cs | 31 ++++++++++++++++--- .../InspectionModeMatrixTests.cs | 4 +-- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/WpfVisualTreeMcp.Injector/README.md b/src/WpfVisualTreeMcp.Injector/README.md index 4aa2878..2779be2 100644 --- a/src/WpfVisualTreeMcp.Injector/README.md +++ b/src/WpfVisualTreeMcp.Injector/README.md @@ -63,7 +63,7 @@ protected override void OnStartup(StartupEventArgs e) base.OnStartup(e); // Initialize the inspector - WpfVisualTreeMcp.Inspector.InspectorService.Initialize(Process.GetCurrentProcess().Id); + WpfVisualTreeMcp.Inspector.InspectorService.Initialize(System.Diagnostics.Process.GetCurrentProcess().Id); } ``` diff --git a/src/WpfVisualTreeMcp.Inspector/InspectorService.cs b/src/WpfVisualTreeMcp.Inspector/InspectorService.cs index be25111..3d5e606 100644 --- a/src/WpfVisualTreeMcp.Inspector/InspectorService.cs +++ b/src/WpfVisualTreeMcp.Inspector/InspectorService.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +#if NETFRAMEWORK using System.Reflection; +#endif using System.Text.Json; using System.Threading.Tasks; using System.Windows; @@ -29,7 +31,7 @@ public class InspectorService : IDisposable private bool _disposed; private static readonly object _initLock = new(); -#if NET48 +#if NETFRAMEWORK private static readonly HashSet _privateDependencyNames = new(StringComparer.OrdinalIgnoreCase) { "Microsoft.Bcl.AsyncInterfaces", @@ -121,7 +123,7 @@ public static void Initialize(int processId) try { DebugLog($"Inspector.Initialize called for PID={processId}"); -#if NET48 +#if NETFRAMEWORK RegisterDependencyResolver(); #endif Instance = new InspectorService(processId); @@ -138,7 +140,7 @@ public static void Initialize(int processId) } } -#if NET48 +#if NETFRAMEWORK private static void RegisterDependencyResolver() { if (_dependencyResolverRegistered) return; diff --git a/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs b/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs index 4c9e8c6..c249bc0 100644 --- a/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs +++ b/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs @@ -128,13 +128,13 @@ public async Task AttachToProcessAsync(int? processId, string _logger.LogInformation("Attached to process {ProcessId} ({ProcessName})", targetProcess.Id, targetProcess.ProcessName); - // Check if Inspector is already loaded (self-hosted mode) + // Check if Inspector is already running, whether self-hosted or previously injected var inspectorLoaded = IsInspectorLoaded(targetProcess) || - await WaitForInspectorPipeAsync(targetProcess.Id, TimeSpan.FromMilliseconds(700)); + await IsInspectorPipeAvailableAsync(targetProcess.Id); if (inspectorLoaded) { - _logger.LogInformation("Inspector DLL already loaded in target process (self-hosted mode)"); - session.InspectorStatus = "Loaded (self-hosted)"; + _logger.LogInformation("Inspector already running in target process"); + session.InspectorStatus = "Loaded (existing)"; } else if (autoInject) { @@ -188,6 +188,29 @@ public async Task AttachToProcessAsync(int? processId, string return session; } + /// + /// Quickly checks whether an existing Inspector pipe accepts connections. + /// + private async Task IsInspectorPipeAvailableAsync(int processId) + { + var pipeName = $"wpf_inspector_{processId}"; + + try + { + using var pipeClient = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + await pipeClient.ConnectAsync(100); + return true; + } + catch (TimeoutException) + { + return false; + } + catch (IOException) + { + return false; + } + } + /// /// Waits for the Inspector's named pipe to become available. /// diff --git a/tests/WpfVisualTreeMcp.IntegrationTests/InspectionModeMatrixTests.cs b/tests/WpfVisualTreeMcp.IntegrationTests/InspectionModeMatrixTests.cs index 355bc9a..c2083d0 100644 --- a/tests/WpfVisualTreeMcp.IntegrationTests/InspectionModeMatrixTests.cs +++ b/tests/WpfVisualTreeMcp.IntegrationTests/InspectionModeMatrixTests.cs @@ -91,7 +91,7 @@ public async Task Cli_inspects_sample_for_target_architecture_and_mode( attachJson.RootElement.GetProperty("success").GetBoolean().Should().BeTrue(); attachJson.RootElement.GetProperty("processId").GetInt32().Should().Be(sample.Id); attachJson.RootElement.GetProperty("inspectorStatus").GetString() - .Should().Be(autoInject ? "Loaded (injected)" : "Loaded (self-hosted)"); + .Should().Be(autoInject ? "Loaded (injected)" : "Loaded (existing)"); } if (autoInject) @@ -103,7 +103,7 @@ public async Task Cli_inspects_sample_for_target_architecture_and_mode( secondAttachJson.RootElement.GetProperty("success").GetBoolean().Should().BeTrue(); secondAttachJson.RootElement.GetProperty("processId").GetInt32().Should().Be(sample.Id); secondAttachJson.RootElement.GetProperty("inspectorStatus").GetString() - .Should().Be("Loaded (self-hosted)", + .Should().Be("Loaded (existing)", "the second attach should reuse the loaded Inspector without reinjecting it"); } From 285930a02e0cf3763dce9620546bcb322b07481e Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 08:54:20 +0200 Subject: [PATCH 15/33] Select Inspector payload by target architecture --- .../ProcessInjector.cs | 32 +++++++++++-------- .../Services/ProcessManager.cs | 2 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/WpfVisualTreeMcp.Injector/ProcessInjector.cs b/src/WpfVisualTreeMcp.Injector/ProcessInjector.cs index 53a1fda..bee15b3 100644 --- a/src/WpfVisualTreeMcp.Injector/ProcessInjector.cs +++ b/src/WpfVisualTreeMcp.Injector/ProcessInjector.cs @@ -361,24 +361,30 @@ public bool IsInspectorLoaded(Process process) /// Gets the path where the Inspector DLL should be located. /// public string GetInspectorDllPath() + { + return GetInspectorDllPath(Environment.Is64BitProcess); + } + + /// + /// Gets the path where the Inspector DLL should be located for the target process. + /// + public string GetInspectorDllPath(Process targetProcess) + { + ArgumentNullException.ThrowIfNull(targetProcess); + return GetInspectorDllPath(IsProcess64Bit(targetProcess)); + } + + private string GetInspectorDllPath(bool targetIs64Bit) { var assemblyLocation = typeof(ProcessInjector).Assembly.Location; var directory = Path.GetDirectoryName(assemblyLocation)!; var fileName = "WpfVisualTreeMcp.Inspector.dll"; - var candidates = new[] - { - Path.Combine(directory, fileName), - Path.Combine(directory, "native", "x64", fileName), - Path.Combine(directory, "native", "x86", fileName), - }; - - foreach (var candidate in candidates) - { - if (File.Exists(candidate)) - return candidate; - } + var sameDirPath = Path.Combine(directory, fileName); + if (File.Exists(sameDirPath)) + return sameDirPath; - return candidates[1]; + var arch = targetIs64Bit ? "x64" : "x86"; + return Path.Combine(directory, "native", arch, fileName); } /// diff --git a/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs b/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs index c249bc0..03add79 100644 --- a/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs +++ b/src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs @@ -142,7 +142,7 @@ public async Task AttachToProcessAsync(int? processId, string _logger.LogInformation("Inspector not loaded, attempting injection..."); try { - var inspectorPath = _injector.GetInspectorDllPath(); + var inspectorPath = _injector.GetInspectorDllPath(targetProcess); var result = _injector.InjectIntoProcess(targetProcess.Id, inspectorPath); if (result) From 61b73b1b2c41385149657c44571844987f99eb91 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 09:14:22 +0200 Subject: [PATCH 16/33] Add repository WPF CLI skill --- .agents/skills/wpf-visual-tree-cli/SKILL.md | 127 ++++++++++++++ .../wpf-visual-tree-cli/agents/openai.yaml | 4 + .../references/cli-reference.md | 165 ++++++++++++++++++ AGENTS.md | 3 + CLAUDE.md | 39 +++-- 5 files changed, 326 insertions(+), 12 deletions(-) create mode 100644 .agents/skills/wpf-visual-tree-cli/SKILL.md create mode 100644 .agents/skills/wpf-visual-tree-cli/agents/openai.yaml create mode 100644 .agents/skills/wpf-visual-tree-cli/references/cli-reference.md create mode 100644 AGENTS.md diff --git a/.agents/skills/wpf-visual-tree-cli/SKILL.md b/.agents/skills/wpf-visual-tree-cli/SKILL.md new file mode 100644 index 0000000..7faa15a --- /dev/null +++ b/.agents/skills/wpf-visual-tree-cli/SKILL.md @@ -0,0 +1,127 @@ +--- +name: wpf-visual-tree-cli +description: Operate WpfVisualTreeMcp through the `wpfinspect` or `WpfVisualTreeMcp.Server.exe` command-line interface to discover, attach to, inspect, diagnose, screenshot, and intentionally drive running Windows WPF applications. Use for WPF visual-tree, dependency-property, binding, DataContext, resource, style, layout, screenshot, interaction, wait, snapshot, diff, and live-property experiments from PowerShell or another shell. Choose self-hosting when it is safer or more reliable than runtime auto-injection. +--- + +# WPF Visual Tree CLI + +Use the CLI for quick diagnostics, repeatable shell automation, and environments without a configured MCP client. Keep inspection read-only unless the user explicitly requests interaction or live modification. + +## Establish the executable + +Prefer the installed .NET tool command: + +```powershell +Get-Command wpfinspect -ErrorAction Stop +wpfinspect help +``` + +If it is unavailable, check for an extracted `WpfVisualTreeMcp.Server.exe`. Install or update the global tool only when authorized: + +```powershell +dotnet tool install --global WpfVisualTreeMcp +dotnet tool update --global WpfVisualTreeMcp +``` + +Run `wpfinspect help` and `wpfinspect --help` before relying on bundled syntax when the installed version differs from v0.12.0. Read [references/cli-reference.md](references/cli-reference.md) for the v0.12.0 command map and examples. + +Distinguish command compatibility from artifact contents. The published v0.12.0 artifacts have a known Auto-injection packaging defect: they omit the native bootstrapper and the complete .NET Framework Inspector dependency closure. The current repository source builds both bootstrapper architectures, packages the dependency closure under both architecture directories, selects the Inspector payload for the target process architecture, resolves co-located .NET Framework dependencies, and validates publish/pack payloads. Do not assume an installed package contains that repair until its release notes or package contents confirm it. + +Do not treat `dotnet build` alone as a complete Auto-injection source build because it skips the native `.vcxproj`. Read [references/cli-reference.md](references/cli-reference.md) for the native x64/Win32 MSBuild commands before publishing a source build for injection. + +## Decide whether to suggest an update + +Suggest `dotnet tool update --global WpfVisualTreeMcp` only when the installation is the global .NET tool, a newer stable package exists, and the observed limitation is plausibly version-specific. Check rather than assume: + +```powershell +Get-Command wpfinspect -ErrorAction SilentlyContinue +dotnet tool list --global | Select-String '^wpfvisualtreemcp\s' +dotnet tool search WpfVisualTreeMcp --detail +``` + +Consult the newer release notes or changelog before claiming that an update fixes a problem. Recommend updating when a newer version adds the missing command or option, fixes the encountered error, restores packaging artifacts such as an architecture helper, or brings an older command schema in line with the current reference. + +For failed Auto-injection from v0.12.0, specifically check whether a newer stable release includes the native-bootstrapper and dependency-closure repair. If no such release exists, explain that current source contains the fix and offer a source build or self-hosted mode; do not imply that reinstalling the same package repairs it. + +Do not recommend an update for limitations that remain architectural: injection blocked by policy or privilege, the need to capture startup diagnostics, or incompatible target frameworks for self-hosting. Route those cases to self-hosted mode as described below. + +Do not execute the update merely to check availability. Explain the evidence and ask before changing a working global tool, especially when scripts may depend on a pinned version. If the package is absent, suggest `dotnet tool install --global WpfVisualTreeMcp`; if the user runs an extracted release EXE, direct them to update that release instead because the global-tool command will not replace it. + +After an approved update, verify the installed version and live syntax, then retry the smallest safe failing operation: + +```powershell +dotnet tool update --global WpfVisualTreeMcp +dotnet tool list --global | Select-String '^wpfvisualtreemcp\s' +wpfinspect help +``` + +## Follow the operating workflow + +1. List candidate processes and parse stdout as JSON: + + ```powershell + $wpfProcessList = wpfinspect list --compact | ConvertFrom-Json + $wpfProcessList.processes | Format-Table processId, processName, mainWindowTitle, runtimeType, dotNetVersion + ``` + + Do not assign to `$pid`; PowerShell treats `$PID` case-insensitively as a read-only automatic variable. Use `$targetProcessId`. + +2. Select an exact process ID. Prefer PID over process name when multiple instances exist. Re-run `list` after an application restart because its PID changes. + +3. Probe attachment without injection first: + + ```powershell + $attachResult = wpfinspect attach --pid $targetProcessId --compact | ConvertFrom-Json + $attachResult.inspectorStatus + ``` + + Continue if the Inspector is already loaded. If it is not loaded, choose auto-injection or self-hosting using the rules below. Auto-inject once; the Inspector remains in that target process for later one-shot commands: + + ```powershell + wpfinspect attach --pid $targetProcessId --auto-inject + ``` + +4. Inspect from broad to narrow: `tree` or `find`, then `props`, `bindings`, `data-context`, `styles`, `layout`, `evaluate-binding`, or `explain-triggers`. Use returned `elem_...` handles only while the target process and element remain alive. + +5. Prefer `wait-for` over sleep-and-retry loops. Use `select-item` instead of clicking virtualized ComboBox/ListBox/ListView/TabControl entries. + +6. Keep stdout machine-readable. Add `--compact` for parsing and `--verbose` only when diagnostics on stderr are needed. + +## Protect target state + +Treat these commands as state-changing and run them only when the user requests the corresponding action: `click`, `select-item`, `set-text`, `send-keys`, `set-property`, `revert-property`, and `clear-binding-errors`. `highlight` visibly alters the target temporarily. + +Prefer UI Automation behavior. Use `--physical` only when necessary because it moves the real cursor, focuses or raises the window, and can affect whichever desktop is active. Double-click and right-click are physical operations. + +Before `set-property`, take a labeled snapshot when useful. Revert experimental edits after measurement unless the user asks to leave them applied. + +## Choose auto-injection or self-hosting + +Use auto-injection when the application cannot be modified, its security policy permits runtime DLL injection, and the installed artifact contains the complete injection payload. Current source builds package the x64 and x86 bootstrappers, the `net48` Inspector dependency closure, the x86 helper, and the CoreCLR runtime configuration; inherent injection constraints still apply. + +Recommend self-hosted mode instead when it solves a concrete auto-injection limitation: + +- Security policy, endpoint protection, process hardening, or organizational rules block `CreateRemoteThread`/`LoadLibrary` injection. +- Privilege or user-session boundaries prevent the server from opening and modifying the target process. +- Cross-bitness injection fails because the architecture-matching helper or required x86 .NET 8 runtime is missing. +- Diagnostics must start with application startup so early binding errors or UI initialization behavior are not missed. +- Repeated application launches need a deterministic Inspector endpoint without re-injecting each new PID. +- Injection destabilizes this particular application or its custom CLR hosting environment. + +Self-hosting requires source changes: reference the Inspector target matching the application and call `InspectorService.Initialize(...)` during WPF startup. The current repository Inspector targets `net472`, `net48`, and `net8.0-windows`, so .NET Framework 4.7.2 applications can self-host directly without retargeting to 4.8. Match a modern WPF application to `net8.0-windows`, not plain `net8.0`. + +Do not confuse self-hosting support with the injected payload. Auto-injection into .NET Framework processes deliberately uses the `net48` Inspector and requires the .NET Framework 4.8 runtime. Adding `net472` enables source-integrated self-hosting; it does not add a `net472` Auto-injection payload. + +Do not suggest self-hosting when target source cannot be changed. Do not suggest MCP as a remedy for blocked injection; MCP uses the same Inspector deployment choices. + +## Recover from common failures + +- No processes: verify the WPF app has a visible main window, match privilege level, and run `list` again. +- Inspector not loaded: inspect `inspectorStatus`; then explicitly auto-inject or explain the self-hosted alternative. +- Target restarted or handle failed: re-list, use the new PID, attach, and reacquire element handles. +- x64 server to x86 target fails: verify the bundled x86 helper, x86 bootstrapper, and x86 .NET 8 runtime. If they are absent from v0.12.0, recommend a newer release containing the packaging repair or a current source build; prefer self-hosting if installing them is unsuitable. +- Native bootstrapper or managed dependency is missing: treat this as the known v0.12.0 packaging defect when applicable. Do not keep retrying injection; update to a release containing the repair, use a verified current source build, or self-host. +- ARM64 target: explain that native ARM64 Auto-injection is unsupported; use a supported x64/x86 target or self-host when the application architecture and Inspector reference permit it. +- Popup/menu missing from screenshot: use `screenshot --mode screen` while the window is visible and unobstructed. Use default `render` mode for covered windows and ordinary controls. +- Command fails opaquely: rerun that command with `--verbose`, keeping stderr separate from stdout JSON. +- Missing command, option, helper, or known fixed behavior: check installed versus current package versions and release notes; suggest a global-tool update only when the evidence connects the limitation to version drift. diff --git a/.agents/skills/wpf-visual-tree-cli/agents/openai.yaml b/.agents/skills/wpf-visual-tree-cli/agents/openai.yaml new file mode 100644 index 0000000..fac48cd --- /dev/null +++ b/.agents/skills/wpf-visual-tree-cli/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "WPF Visual Tree CLI" + short_description: "Inspect and drive WPF apps from the CLI" + default_prompt: "Use $wpf-visual-tree-cli to inspect a running WPF app safely from the command line." diff --git a/.agents/skills/wpf-visual-tree-cli/references/cli-reference.md b/.agents/skills/wpf-visual-tree-cli/references/cli-reference.md new file mode 100644 index 0000000..d6d8191 --- /dev/null +++ b/.agents/skills/wpf-visual-tree-cli/references/cli-reference.md @@ -0,0 +1,165 @@ +# WpfVisualTreeMcp CLI reference + +The command map in this reference matches WpfVisualTreeMcp v0.12.0. Treat the installed command's `help` output as authoritative for other versions. Packaging and target-framework notes describe the current repository state; verify which later release first contains them. + +## Installation and mode selection + +```powershell +dotnet tool install --global WpfVisualTreeMcp +wpfinspect help +``` + +For an existing global-tool installation, inspect version drift without changing it: + +```powershell +dotnet tool list --global | Select-String '^wpfvisualtreemcp\s' +dotnet tool search WpfVisualTreeMcp --detail +``` + +When a newer stable release specifically addresses the observed limitation and the user approves the change: + +```powershell +dotnet tool update --global WpfVisualTreeMcp +wpfinspect help +``` + +This update command does not replace a manually extracted `WpfVisualTreeMcp.Server.exe`. Update that installation from GitHub Releases instead. + +The published v0.12.0 package and ZIP omit required Auto-injection files. Reinstalling v0.12.0 does not fix that defect. Prefer a later release whose notes include the native-bootstrapper and .NET Framework dependency-closure repair, or use a verified current source build or self-hosted mode. + +For an Auto-injection source build, build both native bootstrapper platforms with Visual Studio MSBuild before publishing the server; `dotnet build` alone skips the `.vcxproj`: + +```powershell +msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=x64 +msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=Win32 +dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --configuration Release --output ./publish +``` + +The release ZIP exposes the same CLI as `WpfVisualTreeMcp.Server.exe`. Starting the executable with no arguments runs the MCP stdio server; a recognized subcommand runs one CLI operation. + +Every command except `list` accepts `--pid ` or `--process `. Prefer `--pid`. Global options are `--compact`, `--verbose`, and `--help`/`-h`. + +## Command map + +### Discover and attach + +```text +list +attach --pid|--process [--auto-inject] +``` + +`attach` without `--auto-inject` connects only when the Inspector is already hosted or was injected earlier. `--auto-inject` loads it once into the current target process. + +### Find and inspect + +```text +tree --pid [--root H] [--depth N] +props --pid --handle H +find --pid [--type T] [--name N] [--text S] [--visible-only] [--root H] [--max N] [--filter JSON] +find-deep --pid (--type T | --name N | --text S) [--visible-only] [--root H] [--filter JSON] +bindings --pid --handle H +binding-errors --pid +data-context --pid --handle H +resources --pid [--scope application|element] [--handle H] +styles --pid --handle H +layout --pid --handle H +evaluate-binding --pid --handle H --property P +explain-triggers --pid --handle H [--property P] +``` + +`find` limits results to 50 by default. Filters combine with AND. `find-deep` requires at least one of type, name, or text so the search is bounded semantically. + +### Observe and compare + +```text +watch-property --pid --handle H --property P +wait-for --pid (--type T | --name N | --text S) [--condition visible|exists|enabled|hidden] [--timeout MS] [--poll MS] +snapshot --pid [--handle H] [--label L] [--depth N] +diff --pid --before L1 --after L2 +``` + +`watch-property` registers the watch, but the one-shot CLI cannot stream change events; re-read properties. `wait-for` defaults to a 10-second timeout and 250 ms poll interval, with a 25-second maximum timeout. + +### Capture and export + +```text +highlight --pid --handle H [--duration MS] +export --pid [--handle H] [--format json|xaml] [--out FILE] +screenshot --pid [--handle H] [--out FILE] [--max-width N] [--max-height N] [--mode render|screen] +``` + +`render` is the screenshot default and works if the window is covered, but it omits popup windows. `screen` captures visible popups, dropdowns, context menus, and tooltips but requires an unobstructed visible window. Neither mode scrolls and stitches off-screen content; virtualized items that have not been realized are absent. + +### Change application state + +```text +clear-binding-errors --pid +click --pid --handle H [--physical] [--click-type single|double|right] +select-item --pid --handle H (--item-text S | --index N) +set-text --pid --handle H --text VALUE [--physical] +send-keys --pid --keys COMBO [--handle H] +set-property --pid --handle H --property P --value V +revert-property --pid (--all | [--handle H] [--property P]) +``` + +`set-property` replaces a binding with a local value when applied to a bound dependency property. `revert-property` restores the prior binding, local value, or default. + +## Read-only diagnostic example + +```powershell +$wpfProcessList = wpfinspect list --compact | ConvertFrom-Json +$targetProcessId = ($wpfProcessList.processes | Where-Object processName -eq 'MyApp' | Select-Object -First 1).processId + +$attachResult = wpfinspect attach --pid $targetProcessId --compact | ConvertFrom-Json +if ($attachResult.inspectorStatus -like 'Not loaded*') { + wpfinspect attach --pid $targetProcessId --auto-inject +} + +$buttons = wpfinspect find --pid $targetProcessId --type Button --text Save --visible-only --compact | ConvertFrom-Json +wpfinspect binding-errors --pid $targetProcessId +``` + +Check for multiple matching processes before using `Select-Object -First 1`; do not silently choose one in an ambiguous live environment. + +## Reversible experiment example + +```powershell +wpfinspect snapshot --pid $targetProcessId --label before +wpfinspect set-property --pid $targetProcessId --handle elem_00000052 --property Width --value 300 +wpfinspect snapshot --pid $targetProcessId --label after +wpfinspect diff --pid $targetProcessId --before before --after after +wpfinspect revert-property --pid $targetProcessId --handle elem_00000052 --property Width +``` + +## Self-hosted startup shape + +Use only with an Inspector build compatible with the target application's framework. Current source provides `net472`, `net48`, and `net8.0-windows` Inspector targets: + +```csharp +using System.Diagnostics; +using System.Windows; +using WpfVisualTreeMcp.Inspector; + +public partial class App : Application +{ + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + InspectorService.Initialize(Process.GetCurrentProcess().Id); + } + + protected override void OnExit(ExitEventArgs e) + { + InspectorService.Instance?.Dispose(); + base.OnExit(e); + } +} +``` + +After self-hosting, run `attach` without `--auto-inject`. + +## Sources + +- Repository: +- NuGet tool: +- CLI implementation for v0.12.0: diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3cdc603 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +# AGENTS.md + +Read and follow [CLAUDE.md](CLAUDE.md) as the repository's authoritative agent guidance. diff --git a/CLAUDE.md b/CLAUDE.md index 7bcd3e0..271b218 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,12 +9,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Build Commands ```bash -# Build solution +# Build managed solution dotnet build WpfVisualTreeMcp.sln -# Build for Release +# Build managed solution for Release dotnet build -c Release WpfVisualTreeMcp.sln +# Build native auto-injection bootstrapper (Visual Studio MSBuild) +msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=x64 +msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=Win32 + # Run tests dotnet test WpfVisualTreeMcp.sln @@ -42,8 +46,8 @@ MCP Server (.NET 8.0) ├─ ProcessManager (discovers WPF processes) └─ NamedPipeBridge (IPC) ↓ [Named Pipes: wpf_inspector_{pid}] -Target WPF Application (.NET Framework) - └─ Inspector DLL (.NET 4.8) +Target WPF Application (.NET Framework 4.7.2/4.8 or .NET 8) + └─ Inspector DLL (compatible framework payload) ├─ TreeWalker (visual tree navigation + adorner/popup traversal) ├─ ScreenshotCapture (RenderTargetBitmap element capture) ├─ PropertyReader (dependency properties) @@ -52,7 +56,7 @@ Target WPF Application (.NET Framework) └─ IpcServer (named pipe communication) ``` -**Key Design:** Multi-process architecture for safety. The server runs separately and communicates via named pipes. All operations are read-only **except `wpf_click_element`, `wpf_select_item`, `wpf_set_text`, `wpf_send_keys`, `wpf_set_property`, and `wpf_revert_property`**, which drive controls or edit property values and change application state. `wpf_set_property` is reversible via `wpf_revert_property` (per-session undo stack that restores the prior binding, local value, or default). +**Key Design:** Multi-process architecture for safety. The server runs separately and communicates via named pipes. The six interaction/property commands — `wpf_click_element`, `wpf_select_item`, `wpf_set_text`, `wpf_send_keys`, `wpf_set_property`, and `wpf_revert_property` — drive controls or edit property values and change application state. `wpf_set_property` is reversible via `wpf_revert_property` (per-session undo stack that restores the prior binding, local value, or default). `wpf_highlight_element` temporarily changes the UI, while `wpf_clear_binding_errors` clears Inspector-held diagnostics. ## Key Source Locations @@ -68,7 +72,7 @@ Target WPF Application (.NET Framework) | Injector Helper | `src/WpfVisualTreeMcp.InjectorHelper/Program.cs` | 32-bit .NET 8 helper exe spawned by `ProcessInjector` for cross-arch injection (v0.6.0) | | IPC Bridge | `src/WpfVisualTreeMcp.Server/Services/NamedPipeBridge.cs` | Named pipe communication to Inspector | | Process Manager | `src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs` | WPF process discovery and attachment | -| Inspector Entry | `src/WpfVisualTreeMcp.Inspector/InspectorService.cs` | Injected DLL main entry point | +| Inspector Entry | `src/WpfVisualTreeMcp.Inspector/InspectorService.cs` | Injected or self-hosted DLL main entry point | | Screenshot Capture | `src/WpfVisualTreeMcp.Inspector/ScreenshotCapture.cs` | RenderTargetBitmap element/window capture | | IPC Server | `src/WpfVisualTreeMcp.Inspector/IpcServer.cs` | Named pipe server in target process | | IPC Messages | `src/WpfVisualTreeMcp.Shared/Ipc/IpcMessages.cs` | Request/response contracts | @@ -148,7 +152,7 @@ The Inspector strips UTF-8 BOM (0xEF 0xBB 0xBF) before JSON parsing to prevent d Use `wpf_attach(process_id=, auto_inject=true)` to inject the Inspector into a running .NET Framework or .NET 8 WPF app. Requires: - Native bootstrapper DLL in `publish/native/x64/` (or x86) - A supported CLR and matching Inspector payload (`net48` for .NET Framework or `net8.0-windows` for CoreCLR) -- Architecture detection is automatic (x64 vs x86) +- Architecture detection and payload selection use the target process (x64 vs x86) ### Self-Hosted Mode For your own WPF application, add a reference to the Inspector and initialize on startup: @@ -162,6 +166,10 @@ protected override void OnStartup(StartupEventArgs e) } ``` +The Inspector multi-targets `net472`, `net48`, and `net8.0-windows`, so a project +reference selects the build matching the WPF application's target framework. +Self-hosting avoids runtime injection but requires modifying the application. + ## MCP Server Configuration The server uses the official Microsoft/Anthropic MCP SDK. Configure in `.mcp.json`: @@ -199,7 +207,9 @@ shell call. No MCP handshake required; `--help` is self-documenting. - **Targeting:** every command except `list` takes `--pid ` or `--process `. - **screenshot** writes a PNG file and prints its path (Claude reads it with Read); **export** writes to `--out` if given, otherwise prints content inline. -- **click / select-item / set-text / send-keys** are the four state-changing commands. +- **click / select-item / set-text / send-keys / set-property / revert-property** + are the six commands that change application state. `highlight` changes the UI + temporarily, and `clear-binding-errors` clears Inspector-held diagnostics. - `click` — UI Automation invoke by default; `--physical` for OS mouse click (auto-scrolls into view); `--click-type double|right` for double/right clicks (always physical; right opens context menus — capture with `screenshot --mode screen`). @@ -211,7 +221,7 @@ shell call. No MCP handshake required; `--help` is self-documenting. The response reports the value read back after the write. - `send-keys` — OS-level keyboard input; modifiers `Ctrl/Shift/Alt/Win` plus letters, digits, F1-F12, and named keys. - - All four live in `ControlInteractor`. + - The first four live in `ControlInteractor`; property edits use `PropertyWriter`. ### Typical CLI workflow ```bash @@ -231,8 +241,9 @@ Run `WpfVisualTreeMcp.Server.exe help` for the full command list, or (registry manifest — must match the NuGet package version exactly) 3. Update `CHANGELOG.md` and `RELEASE_NOTES.md` 4. Commit, tag `vX.Y.Z`, push tag → `release.yml` builds the zip, creates the GitHub - release, packs the NuGet package and pushes it to nuget.org (needs the - `NUGET_API_KEY` repo secret; skipped with a notice if absent) + release, packs the NuGet package and pushes it to nuget.org using trusted + publishing (requires the nuget.org policy and `NUGET_USER` repo secret; + skipped with a notice if the secret is absent) 5. Once the package is live on nuget.org, run the manual **"Publish to MCP Registry"** workflow (GitHub OIDC, no secrets) to update registry.modelcontextprotocol.io @@ -241,4 +252,8 @@ Run `WpfVisualTreeMcp.Server.exe help` for the full command list, or - **xUnit** - Test runner - **Moq** - Mocking - **FluentAssertions** - Assertions -- Tests located in `tests/WpfVisualTreeMcp.Tests/` +- Server and Injector tests: `tests/WpfVisualTreeMcp.Tests/` (`net8.0`) +- Shared IPC/model tests: `tests/WpfVisualTreeMcp.Shared.Tests/` + (`net472`, `net48`, and `net8.0`) +- Live framework/architecture/mode matrix: `tests/WpfVisualTreeMcp.IntegrationTests/` + (run through `tests/run-integration-tests.ps1`) From d886e13bf7aec6e06d78b31a9aa379e4d8e9d6b7 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Fri, 14 Aug 2026 09:50:36 +0200 Subject: [PATCH 17/33] Add full-content ScrollViewer screenshots --- .agents/skills/wpf-visual-tree-cli/SKILL.md | 3 +- .../references/cli-reference.md | 8 +- CHANGELOG.md | 6 + CLAUDE.md | 7 +- README.md | 16 +- samples/SampleWpfApp/MainViewModel.cs | 13 +- samples/SampleWpfApp/MainWindow.xaml | 20 +- .../InspectorService.cs | 16 +- .../ScreenshotCapture.cs | 422 ++++++++++++++++-- src/WpfVisualTreeMcp.Server/Cli/CliRunner.cs | 29 +- .../Services/IIpcBridge.cs | 4 +- .../Services/NamedPipeBridge.cs | 5 +- src/WpfVisualTreeMcp.Server/WpfTools.cs | 15 +- .../Ipc/IpcMessages.cs | 6 + .../InspectionModeMatrixTests.cs | 124 ++++- .../IpcSerializerTests.cs | 5 +- .../WpfVisualTreeMcp.Tests/McpServerTests.cs | 45 ++ 17 files changed, 658 insertions(+), 86 deletions(-) diff --git a/.agents/skills/wpf-visual-tree-cli/SKILL.md b/.agents/skills/wpf-visual-tree-cli/SKILL.md index 7faa15a..7417e8a 100644 --- a/.agents/skills/wpf-visual-tree-cli/SKILL.md +++ b/.agents/skills/wpf-visual-tree-cli/SKILL.md @@ -23,7 +23,7 @@ dotnet tool install --global WpfVisualTreeMcp dotnet tool update --global WpfVisualTreeMcp ``` -Run `wpfinspect help` and `wpfinspect --help` before relying on bundled syntax when the installed version differs from v0.12.0. Read [references/cli-reference.md](references/cli-reference.md) for the v0.12.0 command map and examples. +Run `wpfinspect help` and `wpfinspect --help` before relying on bundled syntax when the installed version differs from the current repository. Read [references/cli-reference.md](references/cli-reference.md) for the current command map and examples. Distinguish command compatibility from artifact contents. The published v0.12.0 artifacts have a known Auto-injection packaging defect: they omit the native bootstrapper and the complete .NET Framework Inspector dependency closure. The current repository source builds both bootstrapper architectures, packages the dependency closure under both architecture directories, selects the Inspector payload for the target process architecture, resolves co-located .NET Framework dependencies, and validates publish/pack payloads. Do not assume an installed package contains that repair until its release notes or package contents confirm it. @@ -123,5 +123,6 @@ Do not suggest self-hosting when target source cannot be changed. Do not suggest - Native bootstrapper or managed dependency is missing: treat this as the known v0.12.0 packaging defect when applicable. Do not keep retrying injection; update to a release containing the repair, use a verified current source build, or self-host. - ARM64 target: explain that native ARM64 Auto-injection is unsupported; use a supported x64/x86 target or self-host when the application architecture and Inspector reference permit it. - Popup/menu missing from screenshot: use `screenshot --mode screen` while the window is visible and unobstructed. Use default `render` mode for covered windows and ordinary controls. +- Unscrolled ScrollViewer content is missing: when installed help lists it, pass the precise ScrollViewer or owning control handle to `screenshot --full-content`; use a larger `--max-height` for long output. This render-mode option restores the original scroll position after paging virtualized content. It cannot be combined with `--mode screen`, and logically scrolling virtualized controls with horizontal overflow are unsupported. - Command fails opaquely: rerun that command with `--verbose`, keeping stderr separate from stdout JSON. - Missing command, option, helper, or known fixed behavior: check installed versus current package versions and release notes; suggest a global-tool update only when the evidence connects the limitation to version drift. diff --git a/.agents/skills/wpf-visual-tree-cli/references/cli-reference.md b/.agents/skills/wpf-visual-tree-cli/references/cli-reference.md index d6d8191..0ecefec 100644 --- a/.agents/skills/wpf-visual-tree-cli/references/cli-reference.md +++ b/.agents/skills/wpf-visual-tree-cli/references/cli-reference.md @@ -1,6 +1,6 @@ # WpfVisualTreeMcp CLI reference -The command map in this reference matches WpfVisualTreeMcp v0.12.0. Treat the installed command's `help` output as authoritative for other versions. Packaging and target-framework notes describe the current repository state; verify which later release first contains them. +The command map in this reference matches the current WpfVisualTreeMcp repository. Treat the installed command's `help` output as authoritative; v0.12.0 does not contain every current option. Packaging and target-framework notes describe the current repository state; verify which later release first contains them. ## Installation and mode selection @@ -85,10 +85,12 @@ diff --pid --before L1 --after L2 ```text highlight --pid --handle H [--duration MS] export --pid [--handle H] [--format json|xaml] [--out FILE] -screenshot --pid [--handle H] [--out FILE] [--max-width N] [--max-height N] [--mode render|screen] +screenshot --pid [--handle H] [--out FILE] [--max-width N] [--max-height N] [--mode render|screen] [--full-content] ``` -`render` is the screenshot default and works if the window is covered, but it omits popup windows. `screen` captures visible popups, dropdowns, context menus, and tooltips but requires an unobstructed visible window. Neither mode scrolls and stitches off-screen content; virtualized items that have not been realized are absent. +`render` is the screenshot default and works if the window is covered, but it omits popup windows. `screen` captures visible popups, dropdowns, context menus, and tooltips but requires an unobstructed visible window. + +Add `--full-content` in render mode to capture all content in a `ScrollViewer`. Pass the most precise element handle because a control template or subtree can contain more than one ScrollViewer. Ordinary content is rendered directly; virtualized content is paged and stitched, with the original scroll offsets restored. Increase `--max-height` when the default limit would make a long image unreadably small. Full-content capture cannot be combined with `--mode screen`, and logically scrolling virtualized controls with horizontal overflow are unsupported. ### Change application state diff --git a/CHANGELOG.md b/CHANGELOG.md index 1351674..82ec4de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Capture complete `ScrollViewer` content with `wpf_capture_screenshot(full_content=true)` + or `screenshot --full-content`, including vertically virtualized items, while restoring + the original scroll position after capture. + ### Fixed - Build and package the x64 and x86 native bootstrappers so Auto-injection works from the diff --git a/CLAUDE.md b/CLAUDE.md index 271b218..184d676 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -136,8 +136,11 @@ The Inspector strips UTF-8 BOM (0xEF 0xBB 0xBF) before JSON parsing to prevent d dropdowns, context menus and tooltips — requires the window visible and unobstructed) - DPI-aware via `PresentationSource.FromVisual` - Downscales if exceeding `max_width`/`max_height` (default 1920x1080) -- Captures the element's current arranged bounds; it does not scroll and stitch - off-screen content, and virtualized items that have not been realized are absent +- Captures the element's current arranged bounds by default; `full_content=true` + (render mode only) locates the target's largest ScrollViewer, renders ordinary + content directly, or pages and stitches virtualized content before restoring offsets +- Full-content capture supports physical scrolling in both dimensions and logical + virtualized scrolling vertically; logical virtualized horizontal overflow is rejected - Returns MCP `ImageContentBlock` (base64 PNG) — Claude sees the image directly ### Logging diff --git a/README.md b/README.md index 3fb3d4a..1d945c1 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Debugging WPF UI issues traditionally requires manual inspection with specialize - **Property Watching** - Monitor property changes in real-time ### Interaction & Export -- **Screenshot Capture** - Capture window/element screenshots visible to AI agents +- **Screenshot Capture** - Capture window/element screenshots, including complete ScrollViewer content and virtualized items - **Element Highlighting** - Visually highlight elements in the running app - **Control Click** *(v0.4.0)* - Click elements via UI Automation (`Invoke`/`Toggle`/`Select`/`ExpandCollapse`) or a real OS mouse click - **Set Text** *(new in v0.5.0)* - Fill a TextBox/ComboBox/PasswordBox via UI Automation `IValueProvider.SetValue`, with a `TextBox.Text` / `PasswordBox.Password` / reflected-`Text` fallback, or `physical=true` to type via OS keyboard input (full Unicode BMP) @@ -95,7 +95,7 @@ command list. Output is JSON on stdout; diagnostics go to stderr. | Find controls by visible text / properties | ✅ | manual | partial (UIA) | pixel guessing | | Click / type / select / shortcuts | ✅ | ❌ | ✅ | ✅ (blind) | | Wait for UI conditions (no sleep loops) | ✅ | ❌ | ✅ | ❌ | -| Element screenshots + popup-aware screen capture | ✅ | ❌ | partial | full screen only | +| Element/full-scroll screenshots + popup-aware screen capture | ✅ | ❌ | partial | full screen only | | Works without target source changes | ✅ (auto-injection) | ✅ | ✅ | ✅ | **vs. other WPF MCP servers.** Most WPF MCP servers are built on **UI Automation** (FlaUI): they read the *accessibility* tree, and can only reach WPF internals — bindings, DataContext, ViewModel state — if you **install their in-process probe into the app you want to inspect**. This server takes the Snoop route instead: it **injects at runtime**, so it reads the *real* visual tree and diagnoses binding errors and DataContext **with zero changes to the target app** — nothing to add to your build, nothing to ship into production. @@ -367,7 +367,7 @@ For detailed architecture documentation, see [docs/ARCHITECTURE.md](docs/ARCHITE | `wpf_get_element_properties` | Get all dependency properties of an element | | `wpf_find_elements` | Query elements by type, x:Name, **visible text**, property values and visibility; results include text, automation id, enabled/visible state and screen bounds | | `wpf_find_elements_deep` | Same query filters without result limit, across all windows including adorners/popups | -| `wpf_capture_screenshot` | Capture a screenshot of the window or element (returns image); `mode='screen'` captures real on-screen pixels including open popups, dropdowns and context menus | +| `wpf_capture_screenshot` | Capture a screenshot of the window or element (returns image); `full_content=true` captures complete ScrollViewer content; `mode='screen'` captures real on-screen pixels including open popups, dropdowns and context menus | | `wpf_get_bindings` | Get data bindings for an element (includes MultiBinding, converter, StringFormat) | | `wpf_get_binding_errors` | List all captured binding errors | | `wpf_clear_binding_errors` | Clear the captured binding errors list | @@ -390,9 +390,13 @@ For detailed architecture documentation, see [docs/ARCHITECTURE.md](docs/ARCHITE | `wpf_get_layout_info` | Get layout information | | `wpf_export_tree` | Export visual tree to XAML or JSON | -Screenshot capture covers the element's current arranged bounds. It does not -scroll and stitch off-screen content, and virtualized items that have not been -realized are not available to capture. +Screenshot capture covers the element's current arranged bounds by default. Set +`full_content=true` with render mode to capture all content in a `ScrollViewer`. +Non-virtualized content is rendered directly; virtualized content is paged and +stitched, then the original scroll position is restored. Logically scrolling, +virtualized controls with horizontal overflow are not currently supported by +full-content capture. Increase `max_height` when a long image would otherwise be +downscaled too far. For detailed examples of the original inspection tools, see [docs/TOOLS_REFERENCE.md](docs/TOOLS_REFERENCE.md); run `wpfinspect help` for diff --git a/samples/SampleWpfApp/MainViewModel.cs b/samples/SampleWpfApp/MainViewModel.cs index 6ccdc46..39857f9 100644 --- a/samples/SampleWpfApp/MainViewModel.cs +++ b/samples/SampleWpfApp/MainViewModel.cs @@ -22,12 +22,15 @@ public class MainViewModel : INotifyPropertyChanged public MainViewModel() { - Items = new ObservableCollection + Items = new ObservableCollection(); + for (var i = 1; i <= 40; i++) { - new ItemModel { Name = "Item 1", Description = "First sample item" }, - new ItemModel { Name = "Item 2", Description = "Second sample item" }, - new ItemModel { Name = "Item 3", Description = "Third sample item" } - }; + Items.Add(new ItemModel + { + Name = $"Item {i}", + Description = $"Sample item {i}" + }); + } SubmitCommand = new RelayCommand(Submit, CanSubmit); ClearCommand = new RelayCommand(Clear); diff --git a/samples/SampleWpfApp/MainWindow.xaml b/samples/SampleWpfApp/MainWindow.xaml index 2a0982a..353f191 100644 --- a/samples/SampleWpfApp/MainWindow.xaml +++ b/samples/SampleWpfApp/MainWindow.xaml @@ -86,6 +86,7 @@ + @@ -114,7 +115,24 @@ - + + + + + + + + + + + + + + +