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
113 changes: 99 additions & 14 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,21 @@ 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@v3

- 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 @@ -33,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
Expand All @@ -43,8 +66,67 @@ 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
uses: actions/upload-artifact@v7
with:
name: wpf-visual-tree-mcp
path: ./publish
Expand All @@ -54,20 +136,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 }}

# 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

- 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)
Expand All @@ -76,21 +160,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
2 changes: 1 addition & 1 deletion .github/workflows/publish-mcp-registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
69 changes: 65 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@ 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: |
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 Expand Up @@ -94,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: |
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
13 changes: 9 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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=<pid>, auto_inject=true)` to inject the Inspector into any running .NET Framework WPF app. Requires:
Use `wpf_attach(process_id=<pid>, 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
Expand Down Expand Up @@ -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.

Expand Down
Loading