Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Comment on lines +27 to +34

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded by consolidated PR #22, whose description explicitly presents and explains the complete code, workflow, packaging, documentation, and test scope.

- name: Restore dependencies
run: dotnet restore WpfVisualTreeMcp.sln

Expand All @@ -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:
Expand Down
61 changes: 61 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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 <process-id> --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:

Expand Down Expand Up @@ -258,6 +287,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

Expand Down Expand Up @@ -469,7 +500,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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already corrected in consolidated PR #22: the root README now describes Injector as .NET 8 only. #22 supersedes this stacked PR.

│ ├── WpfVisualTreeMcp.InjectorHelper/# x86 .NET 8 helper exe for cross-arch injection (v0.6.0)
│ ├── WpfVisualTreeMcp.Bootstrapper/ # Native C++ DLL for CLR hosting
Expand Down
7 changes: 7 additions & 0 deletions WpfVisualTreeMcp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Loading