Im September 2026 habe ich CardsToLoop geschrieben, eine kleine Demo einer kartengesteuerten Entwicklungsschleife für eine Anwendung mit Windows Presentation Foundation (WPF). Eine Kanban-Karte beschreibt eine Fehlerbehebung oder eine neue Funktion, und Claude Code setzt sie um. Bei einer Desktop-Anwendung beweist ein erfolgreicher Build nichts. Der Agent muss die Anwendung starten, bedienen und das Ergebnis selbst prüfen.
Der Artikel zeigt, wie sich Claude Code mit einem kleinen Server für das Model Context Protocol (MCP) und Windows UI Automation sowie einem PowerShell-Runner verbinden lässt. Der Runner übergibt dem Agenten eine Karte, und der Agent arbeitet daran, bis er die Änderung committet. Es ist dasselbe Muster wie im agentischen UI-Testsystem für den Peakboard Designer, hier in etwa 700 Zeilen.
Die Anwendung und die Karten
Die Demo besteht aus vier Teilen. Die InvoiceApp ist ein kleiner WPF-Rechnungseditor mit drei absichtlichen Lücken, der UiMcpServer ein MCP-Server mit acht Tools für Windows UI Automation. Dazu kommen drei Karten mit je einer Markdown-Datei sowie run.ps1, der Runner zum Start von Claude Code für eine Karte.
Die InvoiceApp ist ein vereinfachtes Beispiel mit einer schlichten Benutzeroberfläche und zeigt nur das Prinzip der Technik. Jedes Steuerelement der InvoiceApp hat eine AutomationId, einen festen Namen, den UI Automation an andere Programme meldet. Der Agent findet darüber das Textfeld „Discount %“ oder die Gesamtsumme ohne Screenshot. Das folgende XAML zeigt das Textfeld „Discount %“:
<TextBox x:Name="DiscountTextBox"
AutomationProperties.AutomationId="DiscountTextBox"
Width="60" Height="28" VerticalContentAlignment="Center" Text="0"
TextChanged="DiscountTextBox_TextChanged"/>
Die AutomationId wird über das Attribut AutomationProperties.AutomationId gesetzt und vom Agenten an die Tools des MCP-Servers übergeben, um das Textfeld anzusprechen.
Eine Karte ist eine Markdown-Datei mit einem Titel, einem Status, einer Beschreibung und Abnahmekriterien. Abnahmekriterien sind prüfbare Bedingungen, die das Ergebnis erfüllen muss, bevor die Karte erledigt ist. Der Status wechselt von „New“ zu „In Work“ und anschließend zu „Done“ oder „Failed“. Ein Beispiel steht im Abschnitt „Ein Lauf mit einer Karte zur Fehlerbehebung“ weiter unten.
Der MCP-Server
Das Model Context Protocol legt fest, wie ein Agent die Tools eines anderen Prozesses aufruft. UiMcpServer ist eine .NET-8-Konsolenanwendung mit dem Software Development Kit (SDK) für MCP, dem Paket ModelContextProtocol in Version 2.2.0. Der Server bietet die folgenden Tools:
- ui_list_windows für die sichtbaren Top-Level-Fenster
- ui_find_elements für alle Elemente eines Fensters mit einer AutomationId oder einem Namen
- ui_click für einen Klick auf ein Element
- ui_set_text für das Schreiben des Texts eines Textfelds
- ui_get_text für das Lesen des Texts eines Elements
- ui_select_row für die Auswahl einer Zeile in einer Liste oder einem Grid
- ui_wait_for für das Warten, bis ein Element existiert
- ui_screenshot für einen PNG-Screenshot des Fensters
In jedem Tool wird ein Fenster über einen Teil seines Titels und ein Element über seine AutomationId angesprochen. Hat kein Element diese AutomationId, wird nach dem Namen gesucht. Damit bleiben die Buttons eines Systemdialogs erreichbar, da sie eine Beschriftung, aber keine AutomationId haben.
Der folgende Code zeigt das Tool, das den Text eines Textfelds setzt:
[McpServerTool(Name = "ui_set_text")]
[Description("Replaces the text of a text box via the Value pattern.")]
public static string SetText(
[Description("Title fragment of the window")] string windowTitle,
[Description("AutomationId or Name of the element")] string target,
[Description("The new text")] string text)
{
var element = FindTarget(FindWindow(windowTitle), target);
if (!element.TryGetCurrentPattern(ValuePattern.Pattern, out var value))
{
throw new McpException($"'{target}' does not support the Value pattern.");
}
((ValuePattern)value).SetValue(text);
return Ok($"set '{target}' to \"{text}\"");
}
Über das Attribut McpServerTool wird die Methode unter dem Namen ui_set_text registriert. Die Description-Attribute sind Teil des Prompts und werden von Claude Code an das Modell gesendet. Anhand dieser Texte entscheidet das Modell, welches Tool mit welchen Werten aufgerufen wird. Das Value-Pattern ist die Schnittstelle von UI Automation für Steuerelemente mit einem Textwert. Im Klick-Tool werden zunächst die Patterns Invoke, SelectionItem und Toggle versucht und erst als letzter Schritt ein echter Mausklick gesendet.
Eine Datei namens .mcp.json im Repository teilt Claude Code mit, wie der Server gestartet wird:
{
"mcpServers": {
"ui": {
"command": "src/UiMcpServer/bin/Debug/net8.0-windows/UiMcpServer.exe",
"args": []
}
}
}
Der Server wird von Claude Code als Kindprozess gestartet und kommuniziert über die Standardein- und -ausgabe mit einer Zeile JSON-RPC je Nachricht. Die folgenden zwei Zeilen zeigen einen Aufruf von ui_set_text und die Antwort des Servers:
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ui_set_text","arguments":{"windowTitle":"Invoice Editor","target":"DiscountTextBox","text":"10"}}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"ok\":true,\"message\":\"set 'DiscountTextBox' to \\\"10\\\"\"}"}]}}
Vor dem ersten Aufruf werden initialize mit der Protokollversion und anschließend tools/list gesendet. Die Antwort auf tools/list enthält für jedes Tool den Namen, die Beschreibung und ein JSON-Schema der Parameter. Das Schema wird vom SDK aus der Methode und ihren Attributen erzeugt.
Der Runner
Der Runner setzt die Karte auf „In Work“, baut den Prompt und startet Claude Code im nicht interaktiven Modus. Der Prompt enthält die Karte, die Pfade des Projekts und fünf Schritte. Der folgende Text zeigt die Vorlage, in die der Runner die Karte an der Stelle $cardText einsetzt:
You are working one card of a card-driven development loop in this repository.
THE CARD
--------
$cardText
THE PRODUCT
-----------
- WPF app: src/InvoiceApp (project src/InvoiceApp/InvoiceApp.csproj)
- Built exe: src/InvoiceApp/bin/Debug/net8.0-windows/InvoiceApp.exe
- Every control has an AutomationId; discover them with ui_find_elements.
HOW TO WORK
-----------
1. Start the app (Start-Process the exe), reproduce or inspect the behavior the
card describes using the ui_* MCP tools. Take a screenshot as evidence.
2. Implement the change in src/InvoiceApp. Keep it minimal and in the style of
the existing code.
3. Stop the app (taskkill /IM InvoiceApp.exe /F) BEFORE rebuilding - the build
fails while the exe runs. Rebuild with: dotnet build src/InvoiceApp/InvoiceApp.csproj
4. Start the app again and verify every acceptance criterion of the card live
through the ui_* tools: change values, click, read the results back.
Take an after screenshot. A build that compiles proves nothing.
5. Stop the app. Commit ONLY the source changes (git add the changed files
under src/, never git add -A) with a message whose first line is:
card: <the card title>
and whose body says in two or three sentences what was changed and how it
was verified live.
RULES
-----
- Do not touch board/, README.md or anything outside src/ and the commit.
- If an acceptance criterion cannot be met, do not commit half work.
THE LAST LINE of your final message must be exactly one of:
RESULT: done
RESULT: failed: <one-line reason>
Der vierte Schritt enthält den Kernsatz des Prompts: „A build that compiles proves nothing.“ Die Regeln halten den Agenten vom Board fern und verhindern den Commit halbfertiger Arbeit. Der folgende Code zeigt den Aufruf und die Auswertung:
$output = claude -p $prompt --mcp-config .mcp.json --strict-mcp-config 2>&1
$output | Set-Content $logPath -Encoding UTF8
$resultLine = @($output | Where-Object { $_ -match "^RESULT:" }) | Select-Object -Last 1
if ($resultLine -match "^RESULT: done")
{
Set-CardState "Done"
}
else
{
Set-CardState "Failed"
}
Mit dem Flag -p wird Claude Code ohne Terminaloberfläche ausgeführt und die letzte Nachricht ausgegeben. Mit –strict-mcp-config wird nur der Server aus .mcp.json geladen und keiner aus den Benutzereinstellungen. Die letzte Zeile der Antwort muss RESULT: done oder RESULT: failed mit einer Begründung sein. Abhängig von dieser Zeile wird die Karte vom Runner verschoben und die Ausgabe in eine Logdatei geschrieben.
Da Claude Code im nicht interaktiven Modus nicht um Erlaubnis fragen kann, erlaubt die Datei .claude/settings.json im Repository die Datei-Tools, Bash und die UI-Tools. Sie wird erst gelesen, wenn der Ordner als vertrauenswürdig eingestuft ist. Der erste Lauf ist daran gescheitert. Claude Code hat die Berechtigungen ignoriert und eine Warnung auf stderr ausgegeben, woraufhin Windows PowerShell das Skript abgebrochen hat. Bash ohne Einschränkung ist auf einem Entwicklungsrechner vertretbar, auf einem gemeinsam genutzten Rechner nicht.
Ein Lauf mit einer Karte zur Fehlerbehebung
Eine der drei Karten beschreibt einen Fehler im Feld „Discount %“ der InvoiceApp, das von der Gesamtsumme ignoriert wird. Mit einer Position über 100.00 und einem Rabatt von 10 bleibt die Gesamtsumme bei 100,00 (im Screenshot unten rot markiert).

Die folgende Karte beschreibt den Fehler und das erwartete Ergebnis:
---
title: The total ignores the discount
state: New
category: Bug
---
The totals panel has a "Discount %" field. Typing a discount changes nothing:
the total always equals the subtotal.
**Acceptance criteria**
- With items worth 100.00 and a discount of 10, the total shows 90.00.
- An empty or non-numeric discount counts as 0, not as an error.
- The subtotal stays undiscounted.
Jedes Abnahmekriterium nennt einen Wert zur Eingabe und einen Wert zum Zurücklesen.
Zunächst wurde der Fehler vom Agenten mit den zwei Beispielpositionen reproduziert. Anschließend wurden der Code geändert, die Anwendung neu gebaut und die drei Kriterien geprüft. Der nächste Screenshot zeigt das Ergebnis mit denselben Werten und einer Gesamtsumme von jetzt 90,00 (rot markiert).

Der folgende Diff zeigt die Änderung in einer Methode namens RecalculateTotals:
SubtotalText.Text = subtotal.ToString("N2", CultureInfo.CurrentCulture);
-// NOTE: The discount is read but never applied on purpose - this is the bug the
-// first card describes, and the agent run fixes it.
-TotalText.Text = subtotal.ToString("N2", CultureInfo.CurrentCulture);
+// An empty or non-numeric discount counts as 0.
+var discountPercent = decimal.TryParse(DiscountTextBox.Text, NumberStyles.Number,
+ CultureInfo.CurrentCulture, out var d) ? d : 0m;
+var total = subtotal - subtotal * discountPercent / 100m;
+TotalText.Text = total.ToString("N2", CultureInfo.CurrentCulture);
Im neuen Code wird der Rabatt mit dem Zahlenformat des Rechners gelesen und ein leerer oder ungültiger Wert als 0 gezählt. Anschließend wird der Rabatt von der Zwischensumme abgezogen. Der Commit enthält nur die Datei MainWindow.xaml.cs, mit der folgenden Nachricht:
card: The total ignores the discount
RecalculateTotals now parses the Discount % box with the current culture and
subtracts that share from the subtotal; an empty or non-numeric value counts
as 0. The subtotal line is unchanged. Verified live through UI Automation:
with one item of 100.00 a discount of 10 shows total 90.00, an empty box and
"abc" both show 100.00 with no error dialog, and the subtotal stayed at
100.00 throughout.
Die Karte steht jetzt auf „Done“, und im Repository markiert das Tag „discount-card-done“ den Stand nach diesem Lauf. Der Branch main behält den Fehler, damit die Karte erneut laufen kann.
Der Lauf zeigt auch eine Grenze. Die InvoiceApp formatiert die Summen mit dem deutschen Zahlenformat des Rechners, das Grid jedoch mit englischen Zahlen. Statt der in der Karte verlangten 90.00 hat der Agent 90,00 gelesen und ohne Hinweis akzeptiert.
Auf MCP und UI Automation wird in diesem Artikel nicht im Detail eingegangen. Es gibt weitere Wege, einen Agenten eine Anwendung sehen zu lassen, z. B. einen Screenshot und ein Modell mit Bildeingabe ohne AutomationIds. Allerdings kostet jeder Schritt ein Bild, und Werte werden weniger genau gelesen.
Für Webanwendungen bietet Playwright einen MCP-Server mit derselben Art von Tools. UiMcpServer verwendet System.Windows.Automation, die verwaltete UI-Automation-API von .NET. Für Anwendungen mit WinForms- oder Win32-Steuerelementen ist die Bibliothek FlaUI eine Alternative. Sie verwendet die neuere, auf dem Component Object Model (COM) basierende Schnittstelle von UI Automation. Aufgezeichnete Skripte laufen schneller und ohne Modell, brechen jedoch bei Änderungen der Benutzeroberfläche und können ein Ergebnis nicht beurteilen.
In der Demo ist zwischen Karte und Commit kein Mensch beteiligt. Eine nächste Version könnte Folgendes ergänzen:
- einen Status „Questions“ für unklare Karten, mit den Fragen und den Antworten in der Karte
- einen Status „Review“ vor dem Commit, mit meinem Feedback für eine zweite Runde
- eine Prüfung der Karte ohne Schreibzugriff, vor jeder Codeänderung
- ein Memory für das Projekt, in dem der Agent festhält, was er für die nächste Karte gelernt hat
- ein Kanban-Board als kleiner Webdienst
Für Fragen und Review würde dieselbe Claude-Code-Session vom Runner mit dem Flag –resume fortgesetzt. Der Agent behält damit, was er bereits über die Anwendung weiß. Claude Code liest zu Beginn jedes Laufs die Datei CLAUDE.md im Repository. Ergänzt der Agent dort, was er gelernt hat (z. B. das deutsche Zahlenformat der Summen), startet die nächste Karte mit diesem Wissen. Diese Einträge müssten wie der Code geprüft werden.
Das Kanban-Board könnte je Status eine Spalte zeigen und den Runner für eine Karte starten. Für jeden Lauf würden das Log, der Diff und die Screenshots angezeigt und Felder für Antworten und Feedback angeboten. Die Karten blieben Markdown-Dateien im Repository und würden vom Board nur gelesen und geschrieben. Das Board könnte auch die Spalte „New“ Karte für Karte abarbeiten und eine Benachrichtigung senden, wenn eine Karte auf eine Antwort wartet.
Das Repository enthält diesen Artikel auch als PDF →, auf Englisch und in einer etwas längeren Fassung mit mehr Details.
Das war’s.
Bei Fragen gerne melden.
Links:
CardsToLoop auf GitHub →
Model Context Protocol →
MCP C# SDK →
Claude Code, programmatisch ausführen →
Claude Code, MCP →
Übersicht zu UI Automation →
FlaUI →