You change a small part of a desktop application. The project builds, the unit tests pass, and then comes the familiar routine: launch the app, navigate to the right screen, enter some data, click through the workflow, and check that everything still behaves as expected.
Now do it again with a different input. And once more because someone needs a screenshot of the result.
There is nothing wrong with checking your work manually. The frustration comes when the same useful sequence lives only in your memory, a ticket comment, or a collection of screenshots. It is difficult to repeat, difficult to hand to someone else, and surprisingly easy to get slightly wrong.
I recently open sourced CLIF, a Windows desktop automation toolkit with a command-line interface and a local Model Context Protocol server. It gives you a way to inspect an application, operate its controls, describe repeatable workflows, and collect information about what happened.
That combination interests me because it serves several kinds of work: a PowerShell script that saves a few minutes every morning, a regression check around a difficult screen, or an AI agent that needs to see and validate the application it is helping you change.
Let’s walk through where CLIF fits, try a small WPF workflow, and look at how the same foundation becomes useful to an agent.
The work around the automation#
In Modern WPF Testing with FlaUI and AI, I covered using FlaUI directly from a .NET test project. That remains a good approach when you want strongly typed page objects, custom assertions, and close integration with your test framework.
But sometimes you need to answer a much smaller question: What does this application expose, and can I operate it without writing another test harness first?
Even with a capable automation library, there is supporting work to do. Find the correct process. Inspect its controls. Choose a selector. Set a value. Allow the application to respond. Read the result. Save enough evidence to explain a failure later.
CLIF packages those recurring tasks into interfaces you can use from a terminal or an agent host. Its CLI guide covers process discovery, tree inspection, individual actions, interactive sessions, and ordered JSON scripts.
For example, the intent behind an operation can be expressed as “set this checkbox to true” or “select this item.” Your script does not have to calculate where the control happens to be on the screen.
What CLIF builds on#
The foundation is Windows UI Automation, accessed through FlaUI and its UIA3 backend. UI Automation exposes a tree of elements with properties such as names, control types, and automation IDs. Controls can also expose patterns for operations such as invoking a button, selecting an item, or changing a value.
For WPF developers, this is the accessibility and automation view of the application. It is not a dump of the complete XAML visual tree. WPF automation peers determine what controls expose; a decorative layout element and an interactive text box have different responsibilities here. Microsoft’s custom-control automation guidance explains that relationship.
flowchart TD
Shell["Developer or PowerShell script"] --> CLI["CLIF CLI
Selectors and JSON workflows"]
Agent["AI agent in a local MCP host"] --> MCP["CLIF MCP server
Inspection and interaction tools"]
CLI --> UIA["FlaUI and Windows UI Automation"]
MCP --> UIA
UIA <--> App["Windows desktop application"]
CLI --> Evidence["Session logs and screenshots"]
MCP --> Feedback["Tool results, snapshots, and images"]
The two interfaces share this foundation, while their contracts differ. CLI scripts use selectors and files. MCP interactions use registered windows and temporary element references returned by tools.
CLIF also acknowledges Scott Hanselman’s FlaUI-MCP as an inspiration for its MCP work. The project notices preserve that credit. There is useful work in this space to learn from and build on.
The current automation backend requires Windows 10 or 11 and an unlocked, interactive desktop. WPF, WinForms, WinUI, and other applications are candidates when they expose usable UI Automation support; compatibility ultimately depends on the controls. The repository’s portable Avalonia fixture does not make CLIF a macOS or Linux automation tool. See the support boundary.
Start with a small, visible workflow#
This walkthrough follows CLIF v0.1.0, an early release. Windows x64 and Arm64 archives are available for the CLI and MCP server. For the example below, building from source also gives us the included TestWpfApp fixture.
Use a Windows machine with Git and the exact .NET SDK 8.0.424 selected by the repository’s global.json. The SDK pin disables roll-forward, so having a newer SDK alone is insufficient.
From PowerShell:
| |
We retain the PID of the process we launched so another open copy cannot accidentally become our target. Keep the fixture visible and run CLIF in the same user session and at the same elevation as the application.
Inspect before choosing a selector#
| |
list-processes lists desktop processes with accessible main windows. tree lets you inspect or narrow the automation view. Notice the two syntaxes: id:TestTextBox is a tree search expression; id=TestTextBox is an action selector.
Try a few operations against controls in the fixture:
| |
The first command appends text to the fixture’s existing value; our repeatable script below clears it first. The last command clicks the fixture’s button named ToggleButton, which changes its label and status text. Its other button, TestButton, opens a modal message box. The fixture’s handlers make these behaviors explicit.
One detail to understand before using CLI clicks in a larger workflow: v0.1.0 also attempts to dismiss common dialogs after a click. That helper searches across the desktop, so an explicit PID does not isolate every resulting interaction. Use a dedicated test desktop with unrelated applications closed, and do not rely on a confirmation dialog remaining open for review. Our JSON example uses text and slider operations without a click step.
When you own the application, give important controls deliberate automation IDs. For example, in your own WPF view:
| |
That gives your automation a name that can survive a wording change in the UI. Microsoft documents that AutomationId is unique among siblings, not necessarily across the entire tree. Keep IDs unambiguous within your search scope and inspect what the running app actually exposes, particularly around repeated rows and custom controls.
Turn the sequence into a check you can keep#
The next step is to give the workflow a home in source control.
Save the following as clif-blog-workflow.json in the CLIF checkout, or download the example. It fills a text box, checks its value, changes a slider, and verifies the status text produced by the application’s event handler.
| |
Setting the slider to 0 before 42 ensures there is a value change on repeated runs. Otherwise, setting an already-correct value might leave status text from a different interaction on screen. Small details like that make repeatable automation easier to reason about.
Check the script’s structure with the repository validator, then execute it against our explicit PID:
| |
There are two kinds of validation here. The PowerShell validator checks the document’s supported shape; it cannot prove a control exists. A validate step reads an element from the running app and compares its value with the expected string. In this version, that read uses the UIA Value pattern when available and otherwise the element’s Name property. It retries briefly for delayed UI updates. These behaviors are implemented in ScriptService and AutomationService.
The text-box assertion checks that the input arrived. The status assertion checks an observable response from application code. In your application, the equivalent might be a validation message or a completion label. If the real requirement is that a record was persisted, add an appropriate API or database check in your test harness as well.
The script command returns 0 for success and 1 for failure, so a shell or build job can consume the result. The checks you put in the workflow determine what that success actually means.
Let timing serve the assertion#
A pause can help make a demo readable or allow a screen to settle. It cannot tell you that a save completed.
CLIF’s current validate step performs up to ten value reads with short delays. It is useful for a WPF dispatcher update, but it is not a general wait-until engine for long-running business operations. Put longer waits and application-specific retry policies in the surrounding harness when needed. The current executor also does not interpolate the script model’s variables; generate concrete JSON externally if you need data-driven cases. The scripting contract is worth keeping nearby as you expand an example.
Keep evidence that helps explain the result#
A failed check is much easier to investigate when you can see the expected value, the actual value, and the screen around the failure.
CLI automation creates session directories containing a session.log and a screenshots folder. For the checkout workflow above, look under sessions/, or use the session path printed by CLIF. The sample requests a screenshot before its final assertion so that a failed assertion does not prevent that capture step from being reached.
There are a few practical details in the capture implementation. The CLI script path starts capture without a target window, so its screenshots can include the desktop. Capture failures are logged and do not necessarily fail the workflow. If an image is a required deliverable, your harness should also check that the expected artifact exists.
I would keep the script, application build identifier, console result, session log, and relevant images together when investigating a regression. The log explains what was attempted and compared; the image provides visual context. That makes a much more useful bug report than “it failed on my machine.”
Use sample data and review artifacts before sharing them. CLI logs can contain entered values, and desktop images can include unrelated windows. MCP has a separate capture path: clif_screenshot can target a registered window or element, while full-screen capture requires an explicit grant. The MCP screenshot tool returns an image to the host; it is not the CLI’s session-file recorder.
You can also see the project in action in the repository’s recorded CLI and WPF demonstration.
Give an agent a way to observe its work#
An agent helping with a desktop application can benefit from the same feedback we need: what controls are present, what state they expose, and what changed after an action.
Model Context Protocol provides the host/client/server structure for discovering and calling tools. CLIF supplies desktop-specific tools through a local stdio server. The model and its host decide which tools to call; CLIF performs the supported operations and returns results. The CLI examples above do not require a model at all.
For a concrete connection, here is a VS Code .vscode/mcp.json configuration. Replace every C:\\src\\clif prefix with your checkout’s absolute path; the Release build earlier produces both executables:
| |
This configuration lets the server launch the fixture and send input. It leaves broader window enumeration, window closing, and full-screen screenshots disabled. The server reads these settings at startup, so restart it after changing them. Follow the current VS Code configuration reference to enable the server. CLIF’s MCP guide also contains configurations for other stdio hosts.
Start with a bounded request like this, substituting your fixture path:
Launch the WPF fixture at the allowed executable path. Inspect the window and locate TestTextBox, TestSlider, and StatusTextBlock by their automation IDs. Replace the text with “Hello from MCP” and read it back. Set the slider to 0, then 42. Read the status text and check that it equals “Status: Slider value: 42”. Capture that window and report the observed values and any failures. Use only references returned by the tools, and stop if a target is ambiguous or unavailable.
Here is how that request maps to the available tools:
| Stage | Tools and purpose |
|---|---|
| Open the fixture | clif_launch returns a registered window handle. |
| Inspect the controls | clif_snapshot shows the automation tree; clif_search_elements can find an automationId within that handle. |
| Set text | clif_fill replaces the value using a returned element ref. |
| Change the slider | clif_interact accepts its ref, controlType: "slider", action: "set", and a string value. |
| Check the outcome | clif_get_text reads the text box and status element for comparison by the host or agent. |
| Capture visual context | clif_screenshot takes the registered window handle. |
These are tool names and argument descriptions, not a transcript of a recorded agent run. For controls exposing the Value pattern, such as this text box, clif_type appends text and clif_fill replaces it. Without that pattern, the tools fall back to keyboard input, where focus and selection matter. Choose deliberately when rerunning a workflow.
Temporary references need fresh observations#
CLI selectors such as id=TestTextBox describe how to find a control again. MCP references such as w1e12 identify elements registered during the current server session.
A new snapshot invalidates the previous element references for that window. The element registry keeps increasing the reference counter rather than assigning an old reference to a different control. After refreshing a snapshot, use its new refs. A stale-ref error is a reason to inspect again, not guess the next identifier.
For short, known sequences, clif_batch supports click, type, fill, wait, and snapshot, with a maximum of 25 actions and a 30-second cooperative time limit. A stuck native UI Automation call can outlast that limit and require host-level recovery. Batches do not accept the whole CLI script language. Likewise, clif_validate_script checks inline JSON but does not execute it. The CLI remains the execution path for JSON files. Those distinctions are documented in the MCP tool workflow.
One useful development pattern is to let an agent help discover a workflow, then turn the understood sequence into a reviewed script with explicit assertions. You gain repeatability while keeping the agent available for exploration and diagnosis.
Keep the first session focused#
The MCP server is intended for trusted local development. Its permissions are application policy, not a sandbox for an untrusted agent. It also does not make the surrounding AI host local: text and screenshots returned to that host may be sent to its model provider. Use a test application and sample data while you establish the workflow, and read CLIF’s security policy before widening access.
That is also a useful engineering constraint. A small task with a named application, known controls, and explicit success criteria is easier to validate than an open-ended instruction to “check the app.”
Where I would put this to work#
The most useful first automation is usually a workflow you already understand and repeat often. A few possibilities:
| Your recurring task | How CLIF can help | What you still supply |
|---|---|---|
| Smoke-check a release | Execute the same short UI sequence and assert visible results. | A known starting state and the expected behavior. |
| Reproduce a bug | Keep a script and captured evidence with the issue. | Representative data and application/version details. |
| Check an agent’s UI change | Inspect controls, exercise the changed interaction, and collect a screenshot. | A specific acceptance criterion and review of the result. |
| Prepare a demo or support walkthrough | Set up a repeatable screen state and capture it. | Explanatory context and suitable sample data. |
| Repeat work in an app without a suitable API | Operate exposed controls from a shell workflow. | Recovery rules and checks for the actual business outcome. |
These are ways to compose the existing capabilities. CLIF does not automatically generate bug reports, perform visual regression analysis, or turn a recorded session into a complete test suite.
For CI, plan around the desktop session as carefully as the test code. A Windows service or locked session is not equivalent to the interactive environment used here. The project’s testing guide distinguishes unit, integration, WPF UI, and MCP UI checks. Keep fast application tests alongside a small set of UI workflows; a desktop check should earn its maintenance cost.
If an application already has an API that directly covers the operation, I would usually start there. If I need custom control behavior or richer assertions inside a .NET suite, I would reach for FlaUI directly. CLIF is especially useful when the task lives at the desktop boundary and I want an inspectable command or workflow without building the surrounding plumbing again.
Try one workflow you already know#
Pick a screen you can explain to a colleague in a few sentences. Inspect its automation tree, identify two or three stable controls, and script one meaningful result. Keep the evidence from a failed run as well as a successful one. Once that feels dependable, try giving an agent the same bounded task.
That is what I hope CLIF makes easier: taking the desktop work we already do and making it easier to repeat, inspect, and share.
The project is available under the MIT license at cmalpass/clif. If you try it with a control or workflow that does not behave as expected, a small reproduction and a sanitized tree or session log would be a useful contribution. I would love to hear which part of your desktop workflow you want to make less repetitive.