Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

52 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Marionette.NET

Your C# desktop app, talking fluent MCP. In about three lines.

👉 Read SHOWCASE.md for the 90-second pitch + live demo walkthrough.

Marionette.NET turns any C# desktop application into an MCP server that Claude (or any MCP-aware agent) can drive end-to-end. In-process, attribute-driven, cross-framework (WPF + Avalonia + WinUI 3 + MAUI + Windows Forms — five-framework roster, Uno investigated and dropped), AOT-friendly, and strippable to literal zero footprint in production builds.

[McpRoot]
public class TodoListViewModel : INotifyPropertyChanged
{
    [McpCallable("Add a new TODO with the given title.")]
    public void AddTodo(string title) { ... }

    [McpObservable("Total number of todos.", Watchable = true)]
    public int TotalCount => _items.Count;
}
Sample.Wpf.TodoApp.exe --mcp --headless

Claude can now call TodoListViewModel.AddTodo({"title": "buy milk"}) directly as an MCP tool, subscribe to marionette://TodoListViewModel/TotalCount, and screenshot the live window.

Demos

WPF TodoApp dogfood

Avalonia Dashboard dogfood

WinUI FormLab dogfood

What it does

  • Four MCP meta-tools out of the box: inspect_app_api, invoke_method, read_observable, capture_screenshot.
  • Per-method dynamic tools: every [McpCallable] shows up in tools/list as <Root>.<Method> with a rich JSON input schema, callable directly.
  • simulate_input + raise_event meta-tools: real input through each framework's input pipeline (WPF 8-kind matrix via Mouse.PrimaryDevice / Keyboard.PrimaryDevice + RoutedEventArgs; Avalonia click variants via routed events; WinUI via AutomationPeer.Invoke() first, InputInjector fallback).
  • Multi-window routing: when multiple instances of the same [McpRoot] are open, each gets a stable windowId (w1, w2, ...) and the runtime registers per-window dynamic-tool variants (<Root>.<Method>:<windowId>) alongside the bare-form tool.
  • Idempotent tool identity: stable SHA-256 over the canonical signature; description-only edits don't churn the tool ID, signature changes do.
  • tools/list_changed notifications when the manifest mutates (hot-plug roots, multi-window open/close — coalesced 100ms).
  • Watchable observables as MCP resources with resources/subscribe push notifications (INPC-driven, polling fallback).
  • [McpEvent] as MCP resources with sequence-numbered event delivery.
  • Channel push (Ai.Trigger("...")) - the app talks back to Claude as JSON-RPC notifications.
  • Loop protection (MARIONETTE_MAX_DEPTH, default 5; decay window via MARIONETTE_DECAY_SECONDS).
  • Marionette.NET.Testing in-process test harness: call inspect_app_api, invoke_method, and read_observable directly in unit tests without spawning a stdio MCP process.
  • Marionette.NET.Testing.Xunit / Marionette.NET.Testing.NUnit thin adapters for framework-specific test ergonomics and GUI-test gating.
  • DX analyzer hints (MAR013) for public [McpRoot] methods that look like possible [McpCallable] actions.
  • Compile-time stripping via EnableMcpAutomation=false MSBuild property - Release builds ship with literal zero MCP symbols (IL-verified across WPF, Avalonia, and WinUI samples).

Status

v0.1.0-preview — post-Phase-15 release candidate. What works today:

  • The four meta-tools, channel push, watchable resources, [McpEvent] (Phase 1.2 / 1.6).
  • Source generator emits AOT-clean dispatcher tables + per-method JSON schemas; 32/32 generator tests green.
  • Adapter.Wpf — Dispatcher marshalling, RenderTargetBitmap screenshots, name-resolved control lookup (Phase 1.3).
  • Adapter.Avalonia — Cross-platform (net10.0, NOT -windows), Avalonia 12.0.2, Dispatcher.UIThread + RenderTargetBitmap-with-PixelSize semantics + Phase-9.1 simulate_input(type_text) via direct Text-property setter (Phase 2.1 / 9.1).
  • Adapter.WinUI — Windows App SDK 2.x / WinUI 3 (net10.0-windows10.0.19041.0), unpackaged-first, AutomationPeer-first input strategy + Phase-9.3 startup probe for InputInjector availability (Phase 3.2 / 9.3).
  • Adapter.Maui — .NET MAUI 10.x Windows head with MAUI dispatcher / handler integration + Phase-9.2 Application.Windows-lifecycle multi-window tracking (Phase 4.1 / 9.2).
  • Marionette.NET.Testing — framework-neutral in-process harness, structured-error assertions, and xUnit/NUnit helper adapters (Phase 6).
  • Local NuGet packaging — 11 preview packages, including the Marionette.NET meta-package, build locally to artifacts/nuget (Phase 7).
  • simulate_input + raise_event — real input through each framework's input pipeline; WPF 8 kinds, Avalonia click + type_text variants, WinUI full matrix via AutomationPeer.Invoke() first / InputInjector fallback (Phase 3.1 / 3.2 / 9.x).
  • Multi-window routing — per-window dynamic-tool variants (<Root>.<Method>:<windowId>), inspect_app_api advertises windowIds when >1 windows open, tools/list_changed coalesced 100ms (Phase 3.3, MAUI added in Phase 9.2).
  • AOT JSON source generator — typed JsonTypeInfo<T> for [McpEvent] args / [McpObservable] values / [McpCallable] returns + parameters across primitives, user records, Nullable<T>, enums, T[], List<T>, every standard IEnumerable/IList/ICollection/ISet/Stack/Queue, Dictionary / IDictionary / IReadOnlyDictionary across STJ-supported key types, and (Phase 11) any user / concurrent type that implements one of those interfaces (Phase 8 / 8.5 / 11).
  • AOT-clean per-method dynamic tools via the MarionetteAIFunction subclass + McpServerTool.Create(AIFunction, …) overload — registration is reflection-free and verified end-to-end against AOT-published binaries on all four adapters (Phase 10).
  • MarionetteHost.RunAsyncSourceGenSafe — annotation-free entry point for adopters who do not call raise_event and stay within source-gen-eligible JSON shapes (Phase 11). RaiseEventAsync extracted into its own MarionetteRaiseEventTools class so the strict entry point can omit it cleanly.
  • Phase 12.2[McpRaisable] opt-in catalog: AOT-clean raise_event for WPF + Avalonia via assembly-level declarations + source-generated typed dispatcher.
  • Phase 13 — every source-generator shape gap closed: multi-dim arrays rank 3+4, tuple keys rank 4+5, in parameters, Stream params via base64, custom [JsonConverter] (type + property level), generic [McpRoot] via [assembly: McpClosedRoot], generic [McpCallable] via ClosedTypes. 49/49 source-gen tests.
  • Phase 14 — consolidated event layer: Win32 SendInput core for WPF/WinUI/MAUI Windows-head, Avalonia reflection-opt-in for raw input. Closes the last input/event caps.
  • Phase 15 — Windows Forms adapter (Marionette.NET.Adapter.WinForms). Fifth framework on the roster: net10.0-windows, Form.DrawToBitmap-based screenshots, Control.BeginInvoke UI-thread marshalling, Win32InputInjector reuse for simulate_input, CLR-event On<EventName> reflection for raise_event, Application.OpenForms multi-window tracking. Showcase: Sample.WinForms.OrderTracker. IL-strip verified, stdio handshake green.
  • Sample.Wpf.TodoApp + Sample.Avalonia.Dashboard + Sample.WinUI.FormLab + Sample.Maui.PocketPlanner + Sample.Wpf.NeonControlCenter + Sample.WinForms.OrderTracker — all compatible with the skill-pack v2 workflows. NeonControlCenter is the end-of-phase showcase: 13/13 PASS live-driven via MCP in GUI mode.
  • 7 end-to-end eval-cases (dotnet test) + 3 conditional GUI eval-cases (EC-8 simulate_input, EC-9 raise_event, EC-10 multi-window; gated on MARIONETTE_GUI_TESTS=1).
  • Local showcase publish + dogfood: WPF TodoApp, Avalonia Dashboard, and WinUI FormLab publish to artifacts/showcases and pass headless MCP handshake checks.
  • AOT-publish + AOT-runtime stdio handshake: 5 samples × stripped + full publish (10 publishes) all clean, 0 Marionette IL warnings; AOT-runtime handshake exercises explicit dynamic-tool invocation on all four UI-framework adapters (Phase 10 / 11).

What is not here: public NuGet push, GitHub Release tag. The repo is public; the NuGet push is the next milestone. The Uno adapter — originally on the roadmap as Phase 4, then deferred — was investigated in Phase 16 (see .phase16/spike-a-findings.md) and dropped: modern Uno's Skia-renderer default and hard NuGet version conflicts make a clean adapter a multi-week clone with uncertain runtime payoff. Adapter roster is final at five (WPF, Avalonia, WinUI, MAUI, WinForms). See MASTERPLAN.md for the full roadmap.

Quickstart

Prerequisites: .NET 10 SDK matching global.json. Running the WinUI showcase additionally requires Microsoft.WindowsAppRuntime 2.x (see docs/getting-started.md). AOT publish requires the Visual Studio C++ desktop workload.

git clone <this-repo> && cd nw.Automation
dotnet build Marionette.NET.sln -c Debug
pwsh .phase1/demo.ps1

The demo script builds the WPF TodoApp, spawns it under --mcp --headless, drives the MCP handshake, exercises every Phase-1 tool plus the per-method dynamic tools, and reports PASS/FAIL. Add -Gui to also capture a screenshot of the live window.

For the eval-suite that CI runs:

dotnet test tests/Marionette.NET.Integration/Marionette.NET.Integration.csproj

For the in-process testing toolkit:

dotnet test tests/Marionette.NET.Testing.Tests/Marionette.NET.Testing.Tests.csproj

For the local release candidate:

powershell -NoProfile -ExecutionPolicy Bypass -File .phase7\release-local.ps1

Documentation

License

MIT - see LICENSE.

About

AI-controllable, AI-testable C# desktop apps via MCP. Drop a NuGet, decorate with attributes, ship. WPF + Avalonia + WinUI 3 + MAUI. AOT-clean. Strippable to literal zero IL bytes in production.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages