From 24720e1a294c84fec213823e60dd81098af4ccb8 Mon Sep 17 00:00:00 2001 From: Milosz Kukla Date: Thu, 13 Aug 2026 23:11:04 +0200 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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 @@