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