VclRestAgent

An AI can’t see your UI. So let it ask.

TVclRestAgent makes a running Delphi / VCL application answer questions: which controls exist, what they contain, whether a button is enabled. To an AI client it is an MCP server — the model finds the tools by itself. To scripts and tests it is a REST API. Drop it on a data module, set Active := True, and UI testing stops being guesswork.

Twelve units, 15,500 lines, 835 tests. MCP and REST from one table. Nothing outside Indy.

Formerly CAWRestRemoteControl — same component, standalone name.

Eine KI sieht Ihre UI nicht. Also lassen Sie sie fragen.

TVclRestAgent lässt eine laufende Delphi-/VCL-Anwendung Auskunft geben: welche Steuerelemente es gibt, was darin steht, ob eine Schaltfläche bedienbar ist. Für einen KI-Client ist sie ein MCP-Server — das Modell findet die Werkzeuge selbst. Für Skripte und Tests ist sie eine REST-API. Auf ein Datenmodul legen, Active := True setzen — und UI-Tests hören auf, Raterei zu sein.

Zwölf Units, 15.500 Zeilen, 835 Tests. MCP und REST aus einer Tabelle. Nichts außer Indy.

Früher CAWRestRemoteControl — dieselbe Komponente, eigenständiger Name.

The problem it solves

Automating a desktop UI from the outside means working from pictures. That is fragile for a human, and it is worse for an AI.

The usual approach is a screenshot and a pair of coordinates. It works until the window moves, until the display scaling changes, until a caption gets one word longer — and then it fails silently, clicking whatever happens to sit at 530 / 342 now.

For an AI agent the gap is wider still. It can be handed a screenshot, but it cannot know whether the button it sees is enabled, whether the value in that field was actually committed, or whether the screen has finished rebuilding. It infers, and inference is exactly what a test must not do.

This project ran into that wall repeatedly: clicks on custom-drawn buttons that silently did nothing, a grid that would not take keyboard focus, scroll attempts that moved nothing. Every one of those cost time and produced a test result nobody could trust.

Das Problem

Eine Desktop-Oberfläche von außen zu automatisieren heißt, mit Bildern zu arbeiten. Das ist für Menschen brüchig — für eine KI erst recht.

Der übliche Weg ist ein Bildschirmfoto und ein Koordinatenpaar. Das funktioniert, bis das Fenster verschoben wird, bis sich die Skalierung ändert, bis eine Beschriftung ein Wort länger wird — und dann scheitert es lautlos und klickt, was jetzt zufällig bei 530 / 342 liegt.

Für eine KI ist die Lücke noch größer. Man kann ihr ein Bild geben, aber sie kann nicht wissen, ob die Schaltfläche darauf bedienbar ist, ob der Wert im Feld wirklich übernommen wurde, ob der Bildschirm fertig aufgebaut ist. Sie schließt daraus — und Schließen ist genau das, was ein Test nicht tun darf.

In diesem Projekt lief das wiederholt gegen die Wand: Klicks auf selbst gezeichnete Schaltflächen, die stillschweigend nichts taten; ein Grid, das den Tastaturfokus nicht annahm; Scrollversuche, die nichts bewegten. Jedes Mal kostete es Zeit und lieferte ein Ergebnis, dem niemand trauen konnte.

Ask instead of guess

The application knows everything about itself. It just had no way to say so.

Fragen statt raten

Die Anwendung weiß alles über sich selbst. Sie hatte nur keine Möglichkeit, es zu sagen.

Guessing pixel coordinates versus asking the application by name

A VCL application holds its own component tree, with names, classes, properties and state. The component opens that up over HTTP and hands it out as JSON — and takes commands the same way. Nothing is inferred from pixels; every answer comes from the object itself.

Eine VCL-Anwendung trägt ihren Komponentenbaum in sich, mit Namen, Klassen, Eigenschaften und Zustand. Die Komponente öffnet ihn über HTTP und gibt ihn als JSON heraus — und nimmt auf demselben Weg Befehle entgegen. Nichts wird aus Pixeln geschlossen; jede Antwort kommt vom Objekt selbst.

Two ways in — one application

An AI client connects over MCP and finds the tools by itself. A script calls the same endpoints over REST. Both get the same answers, through the same gate.

Zwei Wege hinein — eine Anwendung

Ein KI-Client verbindet sich per MCP und findet die Werkzeuge selbst. Ein Skript ruft dieselben Endpunkte per REST. Beide bekommen dieselben Antworten, durch dieselbe Prüfung.

For an AI client · MCP

Your application becomes an MCP server

At /mcp the running application speaks the Model Context Protocol. Connect Claude Code, Claude Desktop or any other MCP client, and the model sees some forty tools — list_objects, get_property, set_property, click, send_keys, wait, get_screenshot … — each with its parameters and what they mean. No readme to read, no curl to write.

Screenshots arrive as images the model can look at. What is closed in this installation is not offered, and the model is told so the moment it connects.

Für einen KI-Client · MCP

Die Anwendung wird zum MCP-Server

Unter /mcp spricht die laufende Anwendung das Model Context Protocol. Claude Code, Claude Desktop oder ein anderer MCP-Client verbindet sich, und das Modell sieht rund vierzig Werkzeuge — list_objects, get_property, set_property, click, send_keys, wait, get_screenshot … — jedes mit seinen Parametern und deren Bedeutung. Keine Readme zu lesen, kein curl zu schreiben.

Bildschirmfotos kommen als Bilder an, die das Modell ansehen kann. Was in dieser Installation zu ist, wird gar nicht angeboten, und das Modell erfährt es, sobald es sich verbindet.

claude mcp add --transport http myapp \
  http://127.0.0.1:8377/mcp \
  --header "Authorization: Bearer secret"

Then just ask: “Is Save enabled once an article number is entered?”

Dann einfach fragen: „Ist Speichern bedienbar, sobald eine Artikelnummer drinsteht?“

For scripts, tests and browsers · REST

And it is a plain REST API

Every endpoint at its own path, plain HTTP and JSON: a shell script, a test runner in any language, a browser page like the demo, a colleague with curl. Nothing about it assumes an AI on the other end.

/openapi.json describes exactly this installation — generated from the running state, not shipped alongside.

Für Skripte, Tests und Browser · REST

Und sie ist eine schlichte REST-API

Jeder Endpunkt unter eigenem Pfad, schlichtes HTTP und JSON: ein Shellskript, ein Testrunner in irgendeiner Sprache, eine Browserseite wie die Vorführung, ein Kollege mit curl. Nichts daran setzt eine KI am anderen Ende voraus.

/openapi.json beschreibt genau diese Installation — erzeugt aus dem laufenden Zustand, nicht mitgeliefert.

curl -H "Authorization: Bearer secret" \
  127.0.0.1:8377/property -G \
  -d name=btnSave -d prop=Enabled

{"ok":true,"data":{ … "value":true}}

The same question from any language — it is HTTP and JSON.

Dieselbe Frage aus jeder Sprache — es ist HTTP und JSON.

One table, not two implementations. Every MCP tool is generated from the same row as its REST endpoint — the same parameters, the same descriptions — and every tool call goes through the same authentication, the same permission areas and the same ReadOnly cap as the REST call behind it. What is closed for one is closed for the other.

Protocols decides which ways are open: both by default, or [prMcp] when only an AI client is meant to get in — its tools still reach every endpoint, while no script and no web page can call one directly.

The current MCP generation (2026-07-28, stateless, with server/discover) and the earlier ones back to 2025-03-26 alike, checked against the official Python and TypeScript SDKs. Requests from a foreign browser origin are refused, as the specification asks, against DNS rebinding — on every path, REST as well, and so is a Host header that gives a rebinding away.

Eine Tabelle, nicht zwei Umsetzungen. Jedes MCP-Werkzeug entsteht aus derselben Zeile wie sein REST-Endpunkt — dieselben Parameter, dieselben Beschreibungen —, und jeder Werkzeugaufruf geht durch dieselbe Anmeldung, dieselben Rechtebereiche und denselben ReadOnly-Deckel wie der REST-Aufruf dahinter. Was für den einen Weg zu ist, ist es auch für den anderen.

Protocols legt fest, welche Wege offen sind: beide in der Vorgabe, oder [prMcp], wenn nur ein KI-Client hinein soll — seine Werkzeuge erreichen weiter jeden Endpunkt, aber kein Skript und keine Webseite kann einen direkt aufrufen.

Die aktuelle MCP-Generation (2026-07-28, zustandslos, mit server/discover) und die früheren bis 2025-03-26 gleichermaßen, geprüft mit den offiziellen SDKs für Python und TypeScript. Anfragen mit fremder Browser-Herkunft werden abgewiesen, wie es die Spezifikation gegen DNS-Rebinding verlangt — auf jedem Pfad, auch bei REST, ebenso eine Host-Kopfzeile, die ein Rebinding verrät.

Twenty seconds to see it

A demo ships with the component: an ordinary VCL stock-list application, and a browser page that shows the same application from the inside. Start the exe — that is the whole setup.

In zwanzig Sekunden gesehen

Eine Vorführung liegt bei: eine ganz gewöhnliche VCL-Anwendung — eine Lagerverwaltung — und eine Browserseite, die dieselbe Anwendung von innen zeigt. Exe starten, fertig.

The inspector: component tree, the window with the selection framed, its properties and data
Tree on the left, the live window in the middle with a frame around the selection, its properties below, the dataset on the right.
Links der Baum, in der Mitte das lebende Fenster mit Rahmen um die Auswahl, darunter seine Eigenschaften, rechts die Datenmenge.
Download the demo  ·  1.9 MB Vorführung herunterladen  ·  1,9 MB

Unpack, start VclRestAgentDemo.exe, and the browser opens by itself. Four files in one folder, nothing to install, nothing to configure. Both ports listen on the loopback address only, so none of it is reachable from another machine. An AI client can connect to the same application at /mcp — the command is in the README of the package. The demo itself — application and page — was written entirely with Kai in RAD Studio and Claude. Auspacken, VclRestAgentDemo.exe starten, der Browser geht von selbst auf. Vier Dateien in einem Ordner, nichts zu installieren, nichts einzustellen. Beide Ports hören nur auf die Loopback-Adresse, von einer anderen Maschine ist also nichts davon erreichbar. Ein KI-Client kann sich unter /mcp mit derselben Anwendung verbinden — der Befehl steht in der README des Pakets. Die Vorführung selbst — Anwendung und Seite — ist vollständig mit Kai in RAD Studio und Claude entstanden.

The picture goes both ways

Pick something in the tree and the frame jumps to it. Click into the picture and the element underneath is selected — the smallest one containing that point, the way a browser’s developer tools do it. Switch to click, and the same click reaches the application as a real mouse click.

Das Bild geht in beide Richtungen

Im Baum wählen — der Rahmen springt hin. Ins Bild klicken — das Element darunter wird gewählt, und zwar das kleinste, das den Punkt enthält; so wie es die Entwicklerwerkzeuge eines Browsers machen. Auf click umschalten, und derselbe Klick geht als echter Mausklick an die Anwendung.

Change it and watch

Colours have a swatch that opens a colour picker. True and False are switches. Flip Visible on the header panel: it disappears from the running application, and the picture shows it in the same instant. Nothing rebuilt, nothing restarted.

Ändern und zusehen

Farben haben ein Kästchen, das den Farbwähler öffnet. True und False sind Schalter. Legen Sie Visible der Kopfzeile um: Sie verschwindet aus der laufenden Anwendung, und das Bild zeigt es im selben Augenblick. Nichts neu gebaut, nichts neu gestartet.

Even a modal dialog

About … opens one. Everything keeps answering: the tree shows the new window, the picture follows it, and a click on btnClose closes it again — without a hand on the mouse. The client is three files with no dependencies, and it talks to any agent, not just this one.

Auch ein modaler Dialog

About … öffnet einen. Alles antwortet weiter: Der Baum zeigt das neue Fenster, das Bild wechselt hinüber, und ein Klick auf btnClose schließt ihn wieder — ohne dass jemand die Maus anfasst. Der Client sind drei Dateien ohne Abhängigkeiten, und er spricht mit jedem Agenten, nicht nur mit diesem.

Demo 2: ask your own AI

The same exe is an MCP server as well — no second program, no other setting. Leave it running, give your AI client the address and the token, and let it find out what is going on inside.

Vorführung 2: Fragen Sie Ihre eigene KI

Dieselbe Exe ist auch ein MCP-Server — kein zweites Programm, keine andere Einstellung. Laufen lassen, dem KI-Client Adresse und Token geben, und er findet selbst heraus, was drinnen vor sich geht.

1 · Connect

Two things, nothing else

The address http://127.0.0.1:8080/mcp and the header Authorization: Bearer demo. Which tools exist, what their parameters mean and what is closed, the client learns when it connects. In Claude Code:

1 · Verbinden

Zwei Angaben, sonst nichts

Die Adresse http://127.0.0.1:8080/mcp und die Kopfzeile Authorization: Bearer demo. Welche Werkzeuge es gibt, was ihre Parameter bedeuten und was gesperrt ist, erfährt der Client beim Verbinden. In Claude Code:

claude mcp add --transport http stockdemo \
  http://127.0.0.1:8080/mcp \
  --header "Authorization: Bearer demo"

Open the Log page of the application beside it: every call the AI makes shows up there.

Die Log-Seite der Anwendung daneben öffnen: Jeder Aufruf der KI erscheint dort.

2 · Ask

Things no screenshot tells you

  • Which articles are below their minimum stock?
  • Which record is the article grid on, and which dataset is behind it?
  • What does the header of the main window look like?
  • Open “About …” and close it again.
  • Which actions are there, and which are disabled right now?
  • May you read files here?

Every answer comes from the running application — its datasets, properties and windows. Each of these questions was played through against the demo with the official MCP SDK.

2 · Fragen

Was kein Bildschirmfoto verrät

  • Welche Artikel liegen unter ihrem Mindestbestand?
  • Auf welchem Datensatz steht das Artikel-Grid, und welche Datenmenge steckt dahinter?
  • Wie sieht der Kopfbereich des Hauptfensters aus?
  • Öffne „About …“ und schließ es wieder.
  • Welche Aktionen gibt es, und welche sind gerade gesperrt?
  • Darfst du hier Dateien lesen?

Jede Antwort kommt aus der laufenden Anwendung — ihren Datenmengen, Eigenschaften und Fenstern. Jede dieser Fragen ist mit dem offiziellen MCP-SDK gegen die Vorführung durchgespielt.

Protected the way a real installation would be: loopback only, the token on every call, a web page from any other origin refused before the token is even looked at, and files and restarting closed — the AI is told so, and those tools are not offered. The questions are in English here and in the README of the package; ask in whatever language your client speaks.

Gesichert wie eine echte Installation: nur Loopback, das Token bei jedem Aufruf, eine Webseite fremder Herkunft wird abgewiesen, bevor das Token überhaupt geprüft wird, und Dateien und Neustart sind zu — die KI erfährt das, und die Werkzeuge dafür werden gar nicht angeboten. Fragen lässt sich in jeder Sprache, die der Client spricht.

Insight from the inside

The usual evidence about a running program is what it left behind. This one answers while it is still running.

Einblick von innen

Was man über ein laufendes Programm erfährt, sind sonst die Spuren, die es hinterlassen hat. Dieses antwortet, während es läuft.

When a desktop application misbehaves, the evidence is usually what it left behind: a crash dump, a stack trace, a log file — and the log holds only what somebody thought to write before the run. If the answer is not in it, the next step is to add more logging, rebuild, reproduce the problem and read again. Every round costs a build and a run, and reproducing is often the hard part.

Here the program is asked instead. The whole component tree, every dataset with the record it is sitting on, the state of every control, the exceptions it caught — as structured data, on request, at the moment it matters. Not reconstructed afterwards from what was left over.

And it does not stop at looking. A value can be set, an action triggered, a dialog answered, a dataset moved — and the result read back, in the same run, without a rebuild. Seeing in and reaching in. The loop shrinks from “add logging, rebuild, reproduce, read” to “ask”.

A debugger does part of this, and does it better for a single variable. What it does not do is answer “list every dataset in this application” in thirty milliseconds — and it stops the program to work, which changes the very timing behaviour you are often chasing. It also has to be driven, and driving a debugger is something an AI does badly.

Wenn sich eine Desktop-Anwendung falsch verhält, sind die Beweismittel üblicherweise das, was sie hinterlassen hat: ein Absturzprotokoll, ein Stacktrace, eine Logdatei — und im Log steht nur, was jemand vor dem Lauf hineinzuschreiben für nötig hielt. Steht die Antwort nicht darin, kommt als Nächstes: mehr Logausgaben einbauen, neu bauen, den Fehler wieder herbeiführen, erneut lesen. Jede Runde kostet einen Build und einen Lauf, und das Herbeiführen ist oft der schwierige Teil.

Hier wird das Programm stattdessen gefragt. Der ganze Komponentenbaum, jede Datenmenge mit dem Satz, auf dem sie gerade steht, der Zustand jedes Bedienelements, die aufgetretenen Ausnahmen — als strukturierte Daten, auf Anfrage, in dem Moment, in dem es darauf ankommt. Nicht hinterher aus den Überresten rekonstruiert.

Und es bleibt nicht beim Hinsehen. Ein Wert lässt sich setzen, eine Aktion auslösen, ein Dialog beantworten, eine Datenmenge weiterbewegen — und das Ergebnis zurücklesen, im selben Lauf, ohne neu zu bauen. Hineinsehen und hineingreifen. Aus „Logausgaben einbauen, neu bauen, reproduzieren, lesen“ wird „fragen“.

Ein Debugger kann einen Teil davon, und für eine einzelne Variable kann er es besser. Was er nicht kann: „nenn mir alle Datenmengen dieser Anwendung“ in dreißig Millisekunden beantworten — und er hält das Programm an, wodurch sich genau das Zeitverhalten ändert, hinter dem man oft her ist. Bedienen muss man ihn auch, und einen Debugger zu bedienen ist etwas, das eine KI schlecht kann.

And it works after the crash, too

An unhandled exception normally ends with a dump, a report and a process that is gone. HoldOnException stops the application where the exception left it instead — and everything on this page still works: the component tree, every dataset with the record it is sitting on, setting a property, triggering an action.

Somebody hanging in GET /wait?exception=1 is woken the moment it happens, and a URL can be called as well, so something outside finds out even when nobody is waiting. POST /resume lets it go, and the crash reporter carries on with whatever it was going to do.

Autopsy on a body that is still warm — and one that still answers questions. The clock only runs while nobody is asking: every request starts it over, so an investigation is never cut off in the middle.

The component cannot get at the call stack — the hold runs after the unwinding. Your crash reporter can, so it hands it over: ReportException(E, AIntf.GetBugReport) from a madExcept handler, and the client waiting on /wait receives both halves at once — the stack that says how it got there, and an application still standing there to be asked what state it is in.

So this does not replace madExcept or a crash dump; it takes what they produce and adds the half a dump reconstructs badly: the object graph, live and still operable.

Und es geht auch noch nach dem Absturz

Eine unbehandelte Exception endet normalerweise mit einem Dump, einem Protokoll und einem Prozess, den es nicht mehr gibt. HoldOnException hält die Anwendung stattdessen dort an, wo die Exception sie hinterlassen hat — und alles auf dieser Seite funktioniert weiter: der Komponentenbaum, jede Datenmenge mit dem Satz, auf dem sie steht, eine Eigenschaft setzen, eine Aktion auslösen.

Wer in GET /wait?exception=1 hängt, wird in dem Moment geweckt, in dem es passiert; zusätzlich lässt sich eine URL rufen, damit auch dann etwas davon erfährt, wenn gerade niemand wartet. POST /resume gibt sie frei, und der Absturzbericht nimmt seinen gewohnten Lauf.

Die Leiche sezieren, solange sie noch warm ist — und solange sie noch antwortet. Die Uhr läuft nur, wenn niemand fragt: Jede Anfrage setzt sie zurück, damit keine Untersuchung mittendrin abgeschnitten wird.

An den Aufrufstapel kommt die Komponente nicht heran — der Halt läuft nach dem Abwickeln. Der Absturzberichter kommt daran und gibt ihn weiter: ReportException(E, AIntf.GetBugReport) aus einem madExcept-Handler, und wer auf /wait wartet, bekommt beide Hälften auf einmal — den Stapel, der sagt, wie es dazu kam, und eine Anwendung, die noch dasteht und Auskunft gibt.

Das hier ersetzt also madExcept und den Speicherabzug nicht, sondern nimmt, was sie liefern, und legt die Hälfte dazu, die ein Dump schlecht rekonstruiert: den Objektgraphen, lebend und noch bedienbar.

Built so an AI can test

This is what it was actually made for. Remote-controlling a UI over MCP or REST is the mechanism; giving a machine a dependable grip on that UI is the point.

Gebaut, damit eine KI testen kann

Dafür ist es entstanden. Eine Oberfläche per MCP oder REST fernzusteuern ist der Mechanismus — einer Maschine verlässlichen Zugriff darauf zu geben, ist der Zweck.

A test step: trigger, wait, verify

Told what is closed

When an MCP client connects, the server says which areas are closed or read-only in this installation, and the tools behind them are simply not offered. A model does not spend its turns walking into refusals — it knows from the first message what it may try. Over REST, /openapi.json says the same.

Es sagt, was zu ist

Verbindet sich ein MCP-Client, sagt der Server, welche Bereiche in dieser Installation zu oder nur lesbar sind, und die Werkzeuge dahinter werden gar nicht erst angeboten. Ein Modell verbraucht seine Züge nicht an Absagen — es weiß ab der ersten Nachricht, was es versuchen darf. Über REST sagt /openapi.json dasselbe.

No timing guesswork

The usual fallback is sleep(3) and hope. Instead there is /wait: wait until a form appears, until a control becomes enabled, until it is gone, or until the main thread goes quiet. It answers 200 when the condition held and 408 when it never did — and reports how long it actually took.

Kein Raten über Zeiten

Der übliche Notbehelf ist sleep(3) und Hoffen. Stattdessen gibt es /wait: warten, bis ein Formular erscheint, bis ein Element bedienbar ist, bis es verschwunden ist, oder bis der Hauptthread zur Ruhe kommt. Antwort 200, wenn die Bedingung eintrat, 408, wenn nicht — samt der tatsächlich vergangenen Zeit.

Refusals instead of false positives

Triggering a disabled action returns 409, not a cheerful „done“. A test that asks for something impossible learns that it was impossible — the single most valuable property when nobody is watching the screen.

Absagen statt Scheinerfolge

Eine gesperrte Aktion auslösen ergibt 409, kein fröhliches „erledigt“. Ein Test, der Unmögliches verlangt, erfährt, dass es unmöglich war — die wertvollste Eigenschaft überhaupt, wenn niemand auf den Bildschirm sieht.

Modal dialogs don’t block

An action that opens a modal dialog would never return. With async=1 it is queued and answered immediately with 202 — checked first, so a disabled action is still refused. Message boxes can be answered in advance and are logged with their text, which is often worth more than the click.

Modale Dialoge blockieren nicht

Eine Aktion, die einen modalen Dialog öffnet, käme nie zurück. Mit async=1 wird sie vorgemerkt und sofort mit 202 beantwortet — vorher geprüft, eine gesperrte Aktion wird weiterhin abgelehnt. Meldungsdialoge lassen sich im Voraus beantworten und werden mit ihrem Text protokolliert, was oft mehr wert ist als der Klick.

Write, then read back

Setting a property answers with the value read back from the object, not with a confirmation. Some properties clamp or reformat what you give them, and that is precisely what the caller needs to know.

Schreiben und zurücklesen

Eine Eigenschaft zu setzen liefert den vom Objekt zurückgelesenen Wert, keine Bestätigung. Manche Eigenschaften begrenzen oder formatieren, was man ihnen gibt — und genau das muss der Aufrufer erfahren.

Nothing has to be known in advance

/objects lists every component the application holds — including the ones that never appear on screen: datasets, timers, action lists, whole data modules. Each with the path that addresses it unambiguously, even when two modules use the same name.

class=TDataSet&derived=1 narrows it to every dataset, whatever the application happens to use. Catalogue first, then drive — without ever having seen the source.

Man muss nichts vorher wissen

/objects listet jede Komponente der Anwendung — auch die, die nie auf dem Bildschirm auftauchen: Datenmengen, Timer, Aktionslisten, ganze Datenmodule. Jede mit dem Pfad, der sie eindeutig anspricht, auch wenn zwei Module denselben Namen benutzen.

class=TDataSet&derived=1 engt auf alle Datenmengen ein, gleich welche die Anwendung verwendet. Erst katalogisieren, dann steuern — ohne den Quelltext je gesehen zu haben.

Two endpoints teach you the third

The target of a call is always name. An upper limit for one request is always max. Every list answers with count and truncated. The method says what happens: GET reads, PUT replaces, POST appends, DELETE removes.

Nine rules, and they hold everywhere. Where one of them cannot — four places, all of them because the thing itself is shaped that way — it says so on the spot instead of quietly behaving differently.

Zwei Endpunkte erklären den dritten

Das Ziel eines Aufrufs heißt immer name. Die Obergrenze für eine Anfrage immer max. Jede Liste antwortet mit count und truncated. Die Methode sagt, was passiert: GET liest, PUT ersetzt, POST hängt an, DELETE löscht.

Neun Regeln, und sie gelten überall. Wo eine nicht kann — vier Stellen, alle vier, weil die Sache selbst so gebaut ist — steht es dort, statt sich still anders zu verhalten.

Data that survives the round trip

A dataset row comes back typed: numbers as numbers, booleans as true, dates as 2026-12-24, an empty field as null. Exactly what goes back in when you write it again — no conversion by hand, and no dependence on the Windows locale of whichever machine the test happens to run on.

Daten, die den Rundlauf überstehen

Eine Datensatzzeile kommt typisiert zurück: Zahlen als Zahl, Wahrheitswerte als true, Datumsangaben als 2026-12-24, ein leeres Feld als null. Genau das, was beim Zurückschreiben wieder hineingeht — ohne Umrechnen von Hand und ohne Abhängigkeit von den Ländereinstellungen der Maschine, auf der der Test gerade läuft.

Names, not coordinates

Controls are addressed by name or by path (frmMain.pnlLeft.btnSave). Clicking goes through the control’s own Click method, so it works while the window is covered or in the background — and hits the intended element every time.

Coordinates do come along, and then in all three frames of reference at once: in the parent, in the form, on the screen. Handing out one of them and calling it „left“ is how a caller ends up adding up parent positions by hand — which goes wrong at the first panel with a border and produces numbers that are almost right.

Namen statt Koordinaten

Steuerelemente werden über Namen oder Pfad angesprochen (frmMain.pnlLeft.btnSave). Der Klick geht über die Click-Methode des Elements und wirkt auch bei verdecktem oder im Hintergrund liegendem Fenster — und trifft immer das gemeinte.

Koordinaten kommen trotzdem mit, und zwar gleich in allen drei Bezugssystemen: im Elternelement, im Formular, auf dem Bildschirm. Nur eines davon herauszugeben und es „left“ zu nennen, führt dazu, dass der Aufrufer Elternpositionen von Hand addiert — was beim ersten Panel mit Rahmen schiefgeht und Zahlen liefert, die fast stimmen.

Typing that doesn’t need the window in front

/key?name=edtTotal&text=12,50 sends the keystrokes straight to that element — in the background, covered, whatever. And the answer only goes out once the control has processed them, so reading the value back is not a race.

Even Tab, Return and Escape — the keys a control never sees, because the VCL routes them through the form. They take that same road, so they work covered too. Nothing here goes through the keyboard of the system, and nothing needs the window in front.

Tippen, ohne das Fenster nach vorn zu holen

/key?name=edtTotal&text=12,50 schickt die Eingabe direkt an das Element — im Hintergrund, verdeckt, egal. Und die Antwort geht erst hinaus, wenn das Steuerelement sie verarbeitet hat: Zurücklesen ist damit kein Wettlauf.

Auch Tab, Return und Escape — die Tasten, die ein Element nie zu sehen bekommt, weil die VCL sie durch das Formular schickt. Sie nehmen denselben Weg und wirken deshalb auch bei verdecktem Fenster. Nichts davon läuft über die Tastatur des Systems, nichts braucht das Fenster im Vordergrund.

Screenshots when they help

For the genuinely visual checks — colours, layout, a rendered label — /screenshot has the window draw itself, so the image is right even when the window is covered, minimized or hidden. Nothing in it depends on what is in front. The picture complements the facts; it no longer replaces them.

And it does not have to be a whole window. /screenshot?name=lblTotal returns that label — even a TLabel, which has no window of its own and gets cut out of the one it sits in. A full screen costs an AI something like two thousand tokens of attention; a single control costs a fraction of that. Looking closely is cheaper than looking at everything.

Bildschirmfotos, wo sie helfen

Für wirklich Visuelles — Farben, Layout, ein gerendertes Etikett — lässt /screenshot das Fenster sich selbst zeichnen; das Bild stimmt auch verdeckt, minimiert oder unsichtbar. Nichts daran hängt davon ab, was davor liegt. Es ergänzt die Fakten, statt sie zu ersetzen.

Es muss auch kein ganzes Fenster sein: /screenshot?name=lblSumme liefert dieses eine Etikett — auch ein TLabel, das gar kein eigenes Fenster hat und deshalb aus dem ausgeschnitten wird, in dem es sitzt. Ein ganzer Bildschirm kostet eine KI gut zweitausend Token Aufmerksamkeit, ein einzelnes Element einen Bruchteil davon. Genau hinsehen ist billiger als auf alles zu schauen.

It works without an AI just as well

An agent was the reason it was built. It is not the condition for using it.

Everything behind the interface is plain HTTP and JSON: a shell script, a scheduled task, a test runner in any language, a colleague with curl — all of them can ask the same questions and trigger the same actions. Nothing about it assumes an AI on the other end.

What that opens up is automation for applications that were never built with it in mind:

  • a scripted regression run against a sealed-off test installation, driven by names instead of coordinates and therefore surviving the next layout change
  • a smoke test after deployment — does the application come up, does it reach the database, is the screen that matters actually there
  • a legacy application with no interface of its own, made scriptable from the outside without touching its logic
  • diagnosis of a running installation: which form is in front, what is in that field, what went wrong — without attaching a debugger and without asking the user to describe it

The permission areas matter most in exactly these cases. A smoke test needs nothing but information, so everything else stays closed; a scripted run that has to type needs Control and nothing more. And in every case it belongs in a sealed-off environment: the default binding is the loopback address, and it should stay there.

Es geht genauso gut ohne KI

Eine KI war der Anlass. Sie ist keine Voraussetzung.

Hinter der Schnittstelle steckt nichts als HTTP und JSON: ein Shellskript, eine geplante Aufgabe, ein Testrunner in irgendeiner Sprache, ein Kollege mit curl — alle können dieselben Fragen stellen und dieselben Aktionen auslösen. Nichts daran setzt eine KI am anderen Ende voraus.

Das eröffnet Automatisierung für Anwendungen, die nie dafür gebaut wurden:

  • ein Regressionslauf per Skript gegen eine abgeschottete Testinstallation, über Namen statt Koordinaten gesteuert und deshalb auch nach der nächsten Layoutänderung noch brauchbar
  • ein Rauchtest nach dem Ausrollen — kommt die Anwendung hoch, erreicht sie die Datenbank, ist der Bildschirm da, auf den es ankommt
  • eine Altanwendung ohne eigene Schnittstelle, die sich so von außen skripten lässt, ohne ihre Logik anzufassen
  • Diagnose einer laufenden Installation: welches Formular liegt vorn, was steht in dem Feld, was ist schiefgegangen — ohne Debugger und ohne den Anwender bitten zu müssen, es zu beschreiben

Gerade hier zahlen sich die Rechtebereiche aus. Ein Rauchtest braucht nur Auskunft, also bleibt alles andere zu; ein Skriptlauf, der tippen muss, braucht Bedienung und sonst nichts. Und in jedem Fall gehört es in eine abgeschottete Umgebung: Voreingestellt ist die Loopback-Adresse, und dabei sollte es bleiben.

How it works

Wie es arbeitet

An AI agent asks the running application over HTTP and gets JSON back

REST and MCP arrive at the same HTTP server and end in the same dispatcher: a tool call is unpacked into the REST call it stands for and runs through exactly the same code.

Indy serves each request on its own thread, but the VCL is not thread-safe. Every access to forms, controls, actions or datasets is therefore marshalled onto the main thread; exceptions raised there are caught and re-raised on the server thread, so they reach the caller as an HTTP 500 with a message instead of disappearing.

A side effect that is deliberate: if the application is busy, the request waits. The answer then reflects the real state rather than a convenient fiction.

REST und MCP kommen beim selben HTTP-Server an und landen im selben Verteiler: Ein Werkzeugaufruf wird in den REST-Aufruf ausgepackt, für den er steht, und läuft durch genau denselben Code.

Indy bedient jede Anfrage in einem eigenen Thread, die VCL ist aber nicht threadsicher. Jeder Zugriff auf Formulare, Steuerelemente, Aktionen oder Datenmengen läuft deshalb über den Hauptthread; Ausnahmen von dort werden eingefangen und im Serverthread erneut ausgelöst, damit sie als HTTP 500 mit Meldung ankommen, statt zu verschwinden.

Ein gewollter Nebeneffekt: Ist die Anwendung beschäftigt, wartet die Anfrage. Die Antwort spiegelt dann den tatsächlichen Zustand statt einer bequemen Fiktion.

The endpoints

Die Endpunkte

Is it there

Ist es da

/nopnothing but 200 — does the path and the login worknichts außer 200 — stimmen Pfad und Anmeldung
/pingapplication name, main form, form countAnwendungsname, Hauptformular, Anzahl Formulare
/timelocal and UTC, offset, time zone, tick countOrtszeit und UTC, Versatz, Zeitzone, Tickzähler
/machineCPU, memory, OS, monitors, network adaptersProzessor, Speicher, System, Bildschirme, Netzwerkkarten
/openapi.jsonwhat this installation can do, as OpenAPIwas diese Installation kann, als OpenAPI
/mcpall of the below as MCP tools, for an AI clientalles Folgende als MCP-Werkzeuge, für einen KI-Client

Looking

Ansehen

/componentsthe control tree, optionally with propertiesSteuerelementbaum, optional mit Eigenschaften
/componentone component, published and public properties — or one of its sub-objectseine Komponente, published und public Properties — oder eines ihrer Unterobjekte
/objectsevery component there is — datasets, timers, data modulesalle Komponenten — Datenmengen, Timer, Datenmodule
/actionsevery action with caption and statealle Aktionen mit Beschriftung und Zustand
/screenshota window or a single control of this applicationein Fenster oder ein einzelnes Element dieser Anwendung
/waitwait for a form, a control state, or idleauf Formular, Zustand oder Ruhe warten

Operating

Bedienen

/propertyread a value — or POST to set itWert lesen — oder per POST setzen
/focuswhere the focus is, and where it actually landedwo der Fokus steht — und wo er wirklich landete
/actionexecute one, optionally without waitingeine ausführen, auf Wunsch ohne Warten
/clicktrigger a control’s ClickClick eines Steuerelements auslösen
/keytype text or send a key into an elementText tippen oder eine Taste an ein Element
/mouseposition, or move and clickPosition, oder bewegen und klicken
/resumelet go of an application held after a crasheine nach einem Absturz angehaltene Anwendung freigeben
/dialogsanswer message boxes in advance, read the logMeldungen vorab beantworten, Protokoll lesen
/windowstate and bounds — and whether the app is in frontZustand und Lage — und ob die Anwendung vorn liegt

Data

Daten

/datasetfields and rows — or just the current recordFelder und Zeilen — oder nur der aktuelle Satz
/dataset POSTnavigate, jump to a row, locate, insert, edit, delete, refreshnavigieren, zu einer Zeile springen, suchen, anlegen, ändern, löschen

Files

Dateien

/fileGET reads, PUT replaces, POST appends, DELETE removesGET liest, PUT ersetzt, POST hängt an, DELETE löscht
/directorylist, create, remove — recursively on requestauflisten, anlegen, entfernen — auf Wunsch rekursiv
/renamerename or move a file or a directoryDatei oder Verzeichnis umbenennen oder verschieben
/copycopy a file — directories stay out, they have no ceiling hereDatei kopieren — Verzeichnisse nicht, dafür gibt es hier keine Obergrenze

Diagnostics and lifecycle

Diagnose und Lebenszyklus

/exceptionswhat went wrong, as a ring bufferwas schiefging, als Ringpuffer
/terminateshut the application downAnwendung beenden
/restartshut it down and start it againbeenden und neu starten

Where it ends, your application takes over

The built-in endpoints know nothing about any particular program. Everything that needs application knowledge goes through one event.

Wo es aufhört, übernimmt die Anwendung

Die eingebauten Endpunkte wissen nichts über ein bestimmtes Programm. Alles, wofür Anwendungswissen nötig ist, läuft über ein Ereignis.

Built-in endpoints answer generically; application-specific paths go through OnCustomRequest

Any path under /custom/ is handed to OnCustomRequest with method, path, parameters and body. The handler runs on the main thread already, so it may touch datasets and the UI directly, and answers with JSON and an HTTP status. Leave it unhandled and the component replies 404 by itself.

In the application this was built for, that event exposes a query through the existing database layer and the current connection state. Other obvious uses: seed a fixture before a test, reset to a known state afterwards, or read an internal counter that no control displays.

Jeder Pfad unter /custom/ geht an OnCustomRequest, mit Verfahren, Pfad, Parametern und Rumpf. Der Handler läuft bereits im Hauptthread, darf also direkt auf Datenmengen und Oberfläche zugreifen, und antwortet mit JSON und HTTP-Status. Bleibt er unbehandelt, antwortet die Komponente selbst mit 404.

In der Anwendung, für die das entstand, stellt dieses Ereignis eine Abfrage über die vorhandene Datenbankschicht bereit sowie den Verbindungszustand. Weitere naheliegende Fälle: vor einem Test einen Datenbestand herstellen, danach auf einen bekannten Stand zurücksetzen, oder einen internen Zähler auslesen, den kein Steuerelement anzeigt.

// Anything under /custom/ lands here. APath is already stripped
// of the prefix: "/custom/sql" arrives as "sql".
procedure TdmMain.RestCustomRequest(Sender: TObject;
  const AMethod, APath: string; AParams: TStrings;
  const ABody: string; var AResponse: string;
  var AStatusCode: Integer; var AHandled: Boolean);
begin
  if SameText(APath, 'sql') then
  begin
    AResponse := QueryToJson(AParams.Values['sql']);
    AHandled := True;
  end;
end;
// Alles unter /custom/ landet hier. APath ist bereits um das
// Praefix gekuerzt: "/custom/sql" kommt als "sql" an.
procedure TdmMain.RestCustomRequest(Sender: TObject;
  const AMethod, APath: string; AParams: TStrings;
  const ABody: string; var AResponse: string;
  var AStatusCode: Integer; var AHandled: Boolean);
begin
  if SameText(APath, 'sql') then
  begin
    AResponse := QueryToJson(AParams.Values['sql']);
    AHandled := True;
  end;
end;

There is also Execute, which runs a request straight through the same dispatcher without any network involved — for cases where the transport is somewhere else entirely, or for testing the endpoints themselves.

Daneben gibt es Execute: Es schickt eine Anfrage direkt durch denselben Verteiler, ganz ohne Netz — für Fälle, in denen der Transport woanders liegt, oder um die Endpunkte selbst zu prüfen.

Built in a day

Not a rewrite, not a framework — a tool that was missing, so it got built between two other tasks.

The commit history is blunt about it: first version at 01:19, the last of the round at 11:34 the same morning. In between: the component itself, a rename after the compiler pointed out that Dispatch already means something on TObject, the endpoints for waiting and writing, the dialog handling — and 59 tests.

That pace is only possible because the loop was closed. Build, run the tests, read the failure, fix, run again — without a human copying anything between windows. Several things were only found because the tests ran every single time: a /ping that crashed in an application without a main form, because IfThen evaluates both branches; a test of mine that asserted a guarantee the component never made.

Every endpoint was then exercised against the live application before it counted as done — including the one that took 46 ms to answer instead of blocking on a modal dialog, and the dialog watcher that closed and logged a real dialog in 62 ms.

What is in the repository now is the second round. A review of the whole unit turned up what a day of building had left behind: a screenshot parameter that wrote wherever it was pointed, methods that were named but never enforced, dataset values that came out as text and had to go back in as numbers. Four parallel tables became one, from which the dispatcher, the permission gate and the OpenAPI description are all fed — and the tests grew from 59 to 270, because most of those defects could have been caught by one.

An einem Tag entstanden

Keine Neuentwicklung, kein Framework — ein Werkzeug, das fehlte, also wurde es zwischen zwei anderen Aufgaben gebaut.

Die Commit-Historie ist da unmissverständlich: erste Fassung um 01:19, der letzte Commit der Runde um 11:34 desselben Vormittags. Dazwischen: die Komponente selbst, eine Umbenennung, nachdem der Compiler darauf hinwies, dass Dispatch bei TObject bereits etwas bedeutet, die Endpunkte zum Warten und Schreiben, die Dialogbehandlung — und 59 Tests.

Dieses Tempo geht nur, weil der Kreis geschlossen ist. Bauen, Tests laufen lassen, Fehlschlag lesen, reparieren, erneut laufen lassen — ohne dass jemand etwas zwischen Fenstern kopiert. Mehreres kam nur heraus, weil die Tests wirklich jedes Mal liefen: ein /ping, das in einer Anwendung ohne Hauptformular abstürzte, weil IfThen beide Zweige auswertet; ein Test von mir, der eine Zusage prüfte, die die Komponente nie gemacht hat.

Danach wurde jeder Endpunkt an der laufenden Anwendung ausprobiert, bevor er als fertig galt — auch der, der nach 46 ms antwortet, statt an einem modalen Dialog hängen zu bleiben, und die Dialogüberwachung, die einen echten Dialog in 62 ms geschlossen und protokolliert hat.

Im Repository steht inzwischen die zweite Runde. Ein Review der ganzen Unit förderte zutage, was ein Tag Bauen liegen gelassen hatte: ein Screenshot-Parameter, der schrieb, wohin man zeigte; Methoden, die genannt, aber nie durchgesetzt wurden; Feldwerte, die als Text herauskamen und als Zahl wieder hineinmussten. Aus vier parallelen Tabellen wurde eine, aus der sich Verteiler, Rechte-Riegel und OpenAPI-Beschreibung gleichermaßen speisen — und aus 59 Tests wurden 270, weil die meisten dieser Fehler sich mit einem hätten finden lassen.

Built with Kai

The assistant sat inside the IDE for all of this, and that shaped the result — so here is where it actually helped.

What changes when the AI sits inside RAD Studio is not that it writes code faster. It is that the loop closes: write, compile, read the real compiler message, fix, compile again — without a human carrying text between two windows. A twenty-second round trip instead of a context switch, and it stays that way at the two-hundredth repetition.

Three from this project, each found in one of those round trips: a } inside a { } comment, which ends it early and makes the error surface three lines further down looking like something else entirely; a type name left behind after renaming across 6,500 lines, which the compiler pointed at immediately; a unit removed from the uses as unused that turned out to hold four constants. Each of them is a minute with the loop closed, and a much worse afternoon without.

The same applies to the tests — 835 of them by now. They run after every change because running them costs next to nothing — and that is the only reason a review of this size was worth doing at all. Half the defects it turned up would have been invisible to reading alone.

One unit became twelve — and the tests vouched for it

The component started as a single unit and grew to 12,646 lines in it. Then it was split into twelve units, one per area — inspection, control, screenshots, datasets, files, exceptions, OpenAPI, MCP — and the split was done by an AI too: Claude, working from a script rather than by hand.

What made that safe was not care alone, but above all checks. The same 682 tests green before and after, the generated openapi.json identical byte for byte, the cross-check between description and source without a single deviation. For an application that uses it nothing changed: uses VclRestAgent is still all it takes.

Code is only half of it

Kai gives an AI a grip on the source: the editor, the compiler, the project. What it cannot give it is a grip on the running program — whether the button it just wired up is actually enabled, whether the value in that field was committed, whether the screen has finished rebuilding.

That is the gap this component fills. Kai writes the code; TVclRestAgent lets the same assistant check that the running application does what the code says — over MCP, as tools it finds by itself. Either half moves development along considerably on its own — together they are a different level altogether.

Asking has to be cheaper than guessing

An assistant cannot look at the screen. Where a developer glances at a form and knows, it has to ask — and its method is hypothesis, measurement, next hypothesis. That loop runs far more often than a human’s, because there is less intuition skipping the steps in between.

Which is why these numbers are not a detail:

  • 0.4 s — the component (15,500 lines) and a throwaway program to ask it something, compiled from scratch
  • 48 s — the test project built and all 835 tests run, screenshots and real sockets included
  • 3.9 s — a grown application with DevExpress, FastReport and a database started, until every one of its 21 forms answers
  • 0.03 s — one catalogue query against it

At those times you measure. At minutes, you start guessing — and a guess that sounds plausible is the most expensive thing in a debugging session. Three from this project, each of which only happened because asking was this cheap:

A hundred-line throwaway program settled a question about Delphi’s RTTI that reading the documentation had left open — and revealed that a redeclared property is in the list twice, which would otherwise have shipped as duplicate keys in every answer. An explanation the assistant was confident about — why one call took 113 seconds — was wrong, and two measurements showed it. And a bug across four endpoints that no test caught turned up because a real application starts in four seconds and answers in thirty milliseconds.

None of that is about typing speed. It is that the cost of a question drops far enough that measuring beats assuming — which is the difference between an assistant that knows what the application is doing and one that sounds like it does.

Mit Kai entstanden

Die KI saß bei alldem in der IDE, und das hat das Ergebnis geprägt — also: wo sie tatsächlich geholfen hat.

Was sich ändert, wenn die KI in RAD Studio sitzt, ist nicht, dass sie schneller Code schreibt. Es ist, dass sich der Kreis schließt: schreiben, übersetzen, die echte Compilermeldung lesen, reparieren, wieder übersetzen — ohne dass jemand Text zwischen zwei Fenstern hin und her trägt. Zwanzig Sekunden statt eines Kontextwechsels, und das bleibt auch beim zweihundertsten Mal so.

Drei Beispiele aus diesem Projekt, jedes in genau so einem Durchlauf gefunden: eine } in einem { }-Kommentar, die ihn vorzeitig beendet und den Fehler drei Zeilen weiter auftauchen lässt, wo er nach etwas ganz anderem aussieht; ein Typname, der nach dem Umbenennen quer durch 6.500 Zeilen liegen geblieben war und auf den der Compiler sofort zeigte; eine Unit, die als ungenutzt aus der uses geflogen war und dann doch vier Konstanten hielt. Jedes davon ist mit geschlossenem Kreis eine Minute — und ohne ihn ein deutlich schlechterer Nachmittag.

Für die Tests gilt dasselbe — inzwischen 835. Sie laufen nach jeder Änderung, weil sie laufen zu lassen fast nichts kostet — und nur deshalb hat sich ein Review dieser Größe überhaupt gelohnt. Die Hälfte der Befunde wäre beim bloßen Lesen unsichtbar geblieben.

Aus einer Unit wurden zwölf — abgesichert durch die Tests

Die Komponente begann als eine einzige Unit und wuchs darin auf 12.646 Zeilen. Dann wurde sie in zwölf Units zerlegt, eine je Bereich — Befragen, Bedienen, Bildschirmfotos, Datasets, Dateien, Exceptions, OpenAPI, MCP — und auch das hat eine KI gemacht: Claude, per Skript statt von Hand.

Sicher gemacht hat das nicht nur Sorgfalt, sondern besonders Prüfung. Dieselben 682 Tests grün vorher und nachher, die erzeugte openapi.json Byte für Byte gleich, der Abgleich zwischen Beschreibung und Quelltext ohne eine einzige Abweichung. Für eine Anwendung, die sie benutzt, ändert sich nichts: uses VclRestAgent genügt weiterhin.

Der Code ist nur die Hälfte

Kai gibt einer KI Zugriff auf den Quelltext: Editor, Compiler, Projekt. Was es ihr nicht geben kann, ist Zugriff auf das laufende Programm — ob die Schaltfläche, die sie gerade verdrahtet hat, wirklich bedienbar ist, ob der Wert im Feld übernommen wurde, ob der Bildschirm fertig aufgebaut ist.

Genau diese Lücke füllt diese Komponente. Kai schreibt den Code; TVclRestAgent lässt dieselbe KI nachsehen, ob die laufende Anwendung das tut, was im Code steht — per MCP, als Werkzeuge, die sie selbst findet. Jede der beiden Hälften bringt die Entwicklung für sich schon erheblich voran — zusammen sind sie noch einmal eine andere Liga.

Fragen muss billiger sein als raten

Eine KI kann nicht auf den Bildschirm sehen. Wo ein Entwickler kurz auf das Formular schaut und Bescheid weiß, muss sie fragen — und ihr Verfahren ist: Vermutung, Messung, nächste Vermutung. Diese Schleife läuft deutlich öfter als beim Menschen, weil weniger Intuition die Zwischenschritte überspringt.

Deshalb sind diese Zahlen keine Nebensache:

  • 0,4 s — die Komponente (15.500 Zeilen) und ein Wegwerfprogramm, das ihr etwas fragt, komplett neu übersetzt
  • 48 s — Testprojekt gebaut und alle 835 Tests gelaufen, Bildschirmfotos und echte Sockets eingeschlossen
  • 3,9 s — eine gewachsene Anwendung mit DevExpress, FastReport und Datenbank gestartet, bis alle 21 Formulare antworten
  • 0,03 s — eine Katalogabfrage daran

Bei diesen Zeiten misst man. Bei Minuten fängt man an zu raten — und eine Vermutung, die plausibel klingt, ist das Teuerste, was es bei der Fehlersuche gibt. Drei Beispiele aus diesem Projekt, die es nur gab, weil Fragen so billig war:

Ein Wegwerfprogramm von hundert Zeilen hat eine Frage zur Delphi-RTTI beantwortet, die das Lesen der Dokumentation offen gelassen hatte — und dabei gezeigt, dass eine neu deklarierte Property zweimal in der Liste steht, was sonst als doppelte Schlüssel in jeder Antwort gelandet wäre. Eine Erklärung, von der die KI überzeugt war — warum ein Aufruf 113 Sekunden dauert — war falsch, und zwei Messungen haben es gezeigt. Und ein Fehler in vier Endpunkten, den kein Test fand, kam ans Licht, weil eine echte Anwendung in vier Sekunden startet und in dreißig Millisekunden antwortet.

Das hat nichts mit Tippgeschwindigkeit zu tun. Es geht darum, dass eine Frage so wenig kostet, dass Messen sich gegen Annehmen durchsetzt — und das ist der Unterschied zwischen einer KI, die weiß, was die Anwendung tut, und einer, die nur so klingt.

Numbers

Zahlen

15,457 lines in twelve units Zeilen in zwölf Units
28 endpoints, plus your own Endpunkte, plus eigene
40 MCP tools from the same table MCP-Werkzeuge aus derselben Tabelle
835 tests for the component Tests für die Komponente
3 lines to switch it on Zeilen zum Einschalten
0 external dependencies beyond Indy Fremdbibliotheken außer Indy

Open only as far as you want it

The endpoints are grouped into areas, and each area has one of three levels. Nothing has to be secured call by call.

Nur so weit offen, wie man will

Die Endpunkte sind in Bereiche gruppiert, jeder Bereich hat eine von drei Stufen. Kein einzelner Aufruf muss abgesichert werden.

The area follows the subject, the level follows the effect. That is why /property sits under Control and not under Inspect: writing changes the interface, while reading stays possible at acReadOnly. A new endpoint is one row in one table, and the area is a field of it — the compiler asks for it, so an endpoint cannot quietly slip past the permissions.

ReadOnly is a cap on top of all of it: it pulls every area down to information only, without overwriting the setting. Switch it off and what was configured applies again. A denied area stays denied — the cap never opens anything.

Der Bereich richtet sich nach dem Gegenstand, die Stufe nach der Wirkung. Deshalb liegt /property bei Bedienung und nicht bei Auskunft: Schreiben verändert die Oberfläche, Lesen bleibt bei acReadOnly trotzdem möglich. Ein neuer Endpunkt ist eine Zeile in einer Tabelle, und der Bereich ist ein Feld davon — der Compiler fragt danach, an den Rechten vorbei kommt also keiner.

ReadOnly liegt als Deckel darüber: Es zieht jeden Bereich auf Auskunft herunter, ohne die Einstellung zu überschreiben. Nach dem Abschalten gilt wieder, was eingestellt war. Ein gesperrter Bereich bleibt gesperrt — der Deckel öffnet nie etwas.

PropertyEigenschaft EndpointsEndpunkte DefaultVorgabe
InspectAccess /components /component /actions /screenshot /wait /machine acFull Looking, without touching anythingAnsehen, ohne etwas anzufassen
ControlAccess /property /focus /action /click /key /mouse /dialogs acFull At acReadOnly: read properties, trigger nothingBei acReadOnly: Eigenschaften lesen, nichts auslösen
DatasetAccess /dataset acFull At acReadOnly: read and stay putBei acReadOnly: lesen, ohne die Anzeige zu verschieben
DiagnosticAccess /exceptions acFull What went wrong, and clearing the logWas schiefging, und das Protokoll leeren
CustomAccess /custom/… acFull Your own endpoints. What happens behind the event cannot be judged from here — the handler asks ReadOnly itself.Die eigenen Endpunkte. Was hinter dem Ereignis geschieht, ist von hier aus nicht zu beurteilen — die Behandlung fragt ReadOnly selbst ab.
FileAccess /file /directory /rename /copy acDenied Denied by default, and the root directory is empty too. One of the two is the one you forget.In der Vorgabe gesperrt, und das Wurzelverzeichnis ist ebenfalls leer. Eine der beiden Sperren vergisst man.
LifecycleAccess /terminate /restart acDenied Shutting the application down from outside has to be something you asked for.Die Anwendung von außen zu beenden muss man ausdrücklich gewollt haben.

Four endpoints have no setting

/nop, /ping, /time and /openapi.json answer even when everything else is denied — otherwise you could not tell a sealed-off installation from a dead one. They still sit behind authentication: with a token or basic credentials configured, /nop needs them too.

/machine is deliberately not among them. Network adapters and addresses are reconnaissance material, not a pleasantry.

Vier Endpunkte haben keine Einstellung

/nop, /ping, /time und /openapi.json antworten auch dann, wenn alles andere gesperrt ist — sonst ließe sich eine zugeschnürte Installation nicht von einer toten unterscheiden. Hinter der Anmeldung liegen sie trotzdem: Wer Token oder Basic eingestellt hat, kommt auch an /nop nur mit Zugangsdaten.

/machine gehört bewusst nicht dazu. Netzwerkkarten und Adressen sind Aufklärungsmaterial, keine Nettigkeit.

And the method has to match

Every endpoint declares the methods it answers to; anything else is a 405 with an Allow header naming what is left — with ReadOnly already folded into that list.

This is not pedantry about HTTP. Without it, one <img src="http://127.0.0.1:8377/terminate"> on any web page would shut the application down, and a browser’s preflight would trigger an action. The loopback address is no protection there — the browser runs on the same machine. For the same reason no Access-Control-Allow-Origin header is sent unless you name an origin yourself.

And a page cannot have the browser send anything at all: a foreign Origin, or a Host header naming the page’s own host after a DNS rebinding, is a 403 on every path, before the credentials are looked at. Scripts and AI clients send no Origin, and as Host the machine they talk to — they never notice.

Und das Verfahren muss passen

Jeder Endpunkt nennt die Methoden, die er beantwortet; alles andere ist ein 405 mit Allow-Kopfzeile, die sagt, was bleibt — ReadOnly in dieser Liste schon eingerechnet.

Das ist keine HTTP-Pedanterie. Ohne sie würde ein einziges <img src="http://127.0.0.1:8377/terminate"> auf einer beliebigen Webseite die Anwendung herunterfahren, und ein Preflight des Browsers löste eine Aktion aus. Die Loopback-Adresse schützt davor nicht — der Browser läuft auf derselben Maschine. Aus demselben Grund geht keine Access-Control-Allow-Origin-Kopfzeile hinaus, solange man nicht selbst eine Herkunft benennt.

Und eine Seite kann den Browser gar nicht erst etwas schicken lassen: Eine fremde Origin oder eine Host-Kopfzeile, die nach einem DNS-Rebinding den Namen der Seite trägt, ist auf jedem Pfad ein 403, bevor die Anmeldung geprüft wird. Skripte und KI-Clients schicken keine Origin und als Host die Maschine, mit der sie reden — sie merken davon nichts.

No HTTPS built in — on purpose

On the loopback address there is no line to listen to: a request to 127.0.0.1 never leaves the machine. What protects it there is what is already in place — a token or basic credentials, the areas, ReadOnly.

Across a network the readme recommends putting TLS in front of the agent rather than into it: an nginx or an stunnel on the same machine, passing on to 127.0.0.1. That brings real certificates and their renewal, once instead of once per application, and the promise of nothing beyond Indy stays intact — OpenSSL would otherwise travel next to every executable. And for loopback no public authority issues a certificate anyway: it would be a self-signed one that every client has to be told to accept.

Where TLS has to sit in the application after all, it can: OnCreateServer hands over the Indy server before it listens, and an SSL IOHandler goes in there — OpenSSL then comes with the application, not with the component. Indy's trap is taken care of: on its own it encrypts port 443 only and lets any other port through in plain text, IOHandler or not. Encrypted and an https:// BaseUrl say what actually happens.

Kein HTTPS eingebaut — mit Absicht

Auf der Loopback-Adresse gibt es keine Leitung zum Mithören: Ein Aufruf an 127.0.0.1 verlässt die Maschine nie. Dort schützt das, was ohnehin da ist — Token oder Basic-Zugangsdaten, die Bereiche, ReadOnly.

Fürs Netz empfiehlt die Readme, TLS vor den Agenten zu setzen statt hinein: ein nginx oder ein stunnel auf derselben Maschine, der nach 127.0.0.1 weiterreicht. Der bringt echte Zertifikate samt Erneuerung mit, einmal statt einmal pro Anwendung, und die Zusage „nichts außer Indy“ bleibt stehen — OpenSSL müsste sonst neben jeder Exe mitreisen. Für Loopback stellt ohnehin keine öffentliche Stelle ein Zertifikat aus: Es wäre ein selbstsigniertes, das jeder Client erst annehmen muss.

Wo TLS doch in die Anwendung muss, geht das: OnCreateServer reicht den Indy-Server heraus, bevor er lauscht, und dort kommt ein SSL-IOHandler hinein — OpenSSL bringt dann die Anwendung mit, nicht die Komponente. Indys Falle ist abgefangen: Von sich aus verschlüsselt es nur Port 443 und lässt jeden anderen im Klartext durch, IOHandler hin oder her. Encrypted und eine https://-BaseUrl sagen, was wirklich passiert.

It describes itself

One request and a caller knows what this installation can do — including what is switched off.

Es beschreibt sich selbst

Ein Aufruf, und ein Client weiß, was diese Installation kann — auch, was abgeschaltet ist.

GET /openapi.json returns an OpenAPI 3.0.3 document that is generated, not shipped. It names the actual address, the configured base path, the authentication scheme in force, and — per endpoint — whether that area is open here at all.

What is denied is listed and marked deprecated rather than omitted: a caller should be able to tell „does not exist here“ from „is closed here“. Two extra fields carry the detail: x-vcl-area and x-vcl-access, the latter with ReadOnly already folded in.

An agent asks once and knows what it may attempt, instead of walking into a wall of 403s. A file shipped alongside could never say this — it would describe what the component could do, not what this machine allows.

Over MCP the same happens in the protocol’s own terms: tools/list offers only what is open, and the server’s instructions name what is closed or read-only.

GET /openapi.json liefert ein OpenAPI-3.0.3-Dokument, das erzeugt und nicht mitgeliefert wird. Es nennt die tatsächliche Adresse, den eingestellten Pfad-Präfix, das verlangte Anmeldeverfahren und je Endpunkt, ob dieser Bereich hier überhaupt offen ist.

Gesperrtes wird genannt und als deprecated markiert, statt es wegzulassen: Ein Aufrufer soll „gibt es hier nicht“ von „ist hier zu“ unterscheiden können. Zwei eigene Felder tragen die Einzelheit: x-vcl-area und x-vcl-access, letzteres mit bereits eingerechnetem ReadOnly.

Eine KI fragt einmal und weiß, was sie versuchen darf, statt gegen 403er zu laufen. Eine mitgelieferte Datei könnte das nie sagen — sie beschriebe, was die Komponente könnte, nicht was diese Maschine erlaubt.

Über MCP geschieht dasselbe mit den Mitteln des Protokolls: tools/list bietet nur an, was offen ist, und die Hinweise des Servers nennen, was zu oder nur lesbar ist.

Where the description stops

Paths, methods, parameters, security and the response envelope are covered. The fields below data differ per endpoint and are not described there — that is what the readme is for.

Wo die Beschreibung aufhört

Pfade, Methoden, Parameter, Sicherheit und die Antwortform sind abgedeckt. Die Felder unterhalb von data sind je Endpunkt verschieden und stehen dort nicht — dafür gibt es die Readme.

Download the readme Readme herunterladen

Markdown, about 2,500 lines: setup, every endpoint with its parameters, the MCP tools, the permission areas, and the traps that cost time — dialogs that block, datasets left in dsEdit. Markdown auf Englisch, rund 2.500 Zeilen: Einrichtung, jeder Endpunkt mit seinen Parametern, die MCP-Werkzeuge, die Rechtebereiche und die Fallen, die Zeit gekostet haben — blockierende Dialoge, Datenmengen, die in dsEdit stehen bleiben.

Limits — and one warning

Grenzen — und eine Warnung

Not for production

This is a testing and diagnostics tool. It can drive the application and read its data, and it is not built to withstand an attacker. It binds to 127.0.0.1 by default, a bearer token or HTTP basic credentials can be required, and in the application it was written for it stays switched off unless it is enabled deliberately.

Binding it to anything other than the loopback address belongs in a sealed-off test environment — nowhere else. On a production machine it should not be enabled at all.

Nicht für Produktivumgebungen

Das ist ein Werkzeug zum Testen und zur Fehlersuche. Es kann die Anwendung fernsteuern und ihre Daten auslesen, und es ist nicht darauf ausgelegt, einem Angreifer standzuhalten. Es bindet per Vorgabe an 127.0.0.1, ein Bearer-Token oder HTTP-Basic lässt sich verlangen, und in der Anwendung, für die es entstand, bleibt es aus, solange es nicht ausdrücklich eingeschaltet wird.

Eine Bindung an etwas anderes als die Loopback-Adresse gehört in eine abgeschottete Testumgebung — sonst nirgendwohin. Auf einem Produktivrechner sollte es gar nicht erst aktiviert sein.

VCL only

It reads the VCL component tree. Application.MessageBox is a plain Win32 window without a VCL form and therefore out of reach; so is anything drawn by a control that keeps its state to itself.

Nur VCL

Gelesen wird der VCL-Komponentenbaum. Application.MessageBox ist ein reines Win32-Fenster ohne VCL-Formular und damit nicht erreichbar — ebenso wenig, was ein Steuerelement nur für sich selbst zeichnet.

Setting is not typing

A property set through the interface does not raise the same events as a real keystroke. Where validation hangs off OnChange, use /key instead — the component documents the difference rather than hiding it.

Setzen ist nicht Tippen

Eine gesetzte Eigenschaft löst nicht dieselben Ereignisse aus wie eine echte Eingabe. Wo die Prüfung an OnChange hängt, führt /key zum Ziel — die Komponente dokumentiert den Unterschied, statt ihn zu verbergen.

It waits with the application

If the main thread is busy, so is the interface. That is deliberate — but it means the component cannot be used to inspect an application that has hung.

Es wartet mit der Anwendung

Ist der Hauptthread beschäftigt, wartet auch die Schnittstelle. Das ist gewollt — heißt aber, dass sich eine hängende Anwendung damit nicht untersuchen lässt.