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