01 / AI system · Web · 2026
Conversational Analysis Workbench
The dashboard is the easy part. Everything the model isn't allowed to do is the interesting part.
Flightradar24 publishes a Model Context Protocol server over its flight API. I wanted to know whether one MCP is enough raw material to build something that assembles its own analytics — not “chat with your data,” but ask a question and get the application that answers it. Six prompt chips, a chat rail, a dashboard grid, and a real pipeline underneath.
It works. But almost everything I learned came from the constraints, not the capability.
ONE ENGLISH QUESTION → ONE ASSEMBLED DASHBOARD
- CHATthe question, and every refinement after it
- PLANNERintent → AnalysisPlan | DashboardPatch
- ZOD GATEvalidate · one repair attempt · then fail out loud
- REQUESTSdeclarative provider calls, cost rules enforced
- TRANSPORTreplay (default) · sandbox · live
- ADAPTERwire format → typed normalized datasets
- ANALYTICS16 pure operations, golden-tested
- SPEC11 frozen widget types
- RENDERERfixed React components, interpreting the spec
THE MODEL AUTHORS TWO OF THESE BOXES · EVERY NUMBER ON SCREEN COMES FROM THE OTHER SEVEN



The model plans. It never computes, and it never draws.
The language model’s entire output surface is one Zod-validated object: an AnalysisPlan, or a DashboardPatch when you’re refining. Both are written in a closed vocabulary — 16 deterministic analytics operations and 11 frozen widget types. The model chooses which operations run and in what order. The arithmetic, the geospatial work, the time bucketing, the pairing logic are pure TypeScript functions with golden fixture tests. No generated code is ever executed or rendered.
When planner output doesn’t validate, the Zod error goes back for exactly one repair attempt, and then the turn fails with a visible message rather than guessing. Unbounded retry loops hide prompt problems; one attempt fixes format slips and surfaces everything else.
The follow-up is a typed patch, not a new question.
“Departures only.” “Break it down by aircraft type.” “Show these on a map.” Each one becomes a DashboardPatch — addFilter, regroup, addWidget, setThreshold — applied to session state against datasets already in memory. Only setScope and refetch are permitted to reach the provider, so most refinements cost zero requests. Re-planning from scratch would have been easier and would have thrown away the dashboard every time you spoke to it.
The subtle one is clicking a bar to cross-filter the whole board. That bar belongs to a grouped, derived dataset — {type: 'B77W', count: 8} — but the filter has to land on the raw rows the grouping consumed. Filter the widget’s own dataset and it looks like it worked: the panel you clicked narrows to one bar while every other panel on screen stays quietly wrong. So the filter walks dataset provenance back to the roots and verifies the field survives there. When it doesn’t, the click is declined rather than applied somewhere misleading.
Every live call costs money, so replay is the default and the guardrail is code.
FR24 bills per returned record — live full positions run about 8 credits a flight. That makes agentic iteration against live data a real cost hazard, and nondeterministic tests on top of it. So the default mode serves recorded fixtures with no network at all, a free sandbox tier handles genuine HTTP round-trips, and live is reserved for deliberate fixture refreshes. A missing fixture in replay mode is an explicit error naming the command that would record it — never a quiet fallthrough to a call that spends money.
Endpoint selection follows the same rule twice on purpose: count before summary before light before full, and full flight tracks only for flights you explicitly selected. Those rules live in the planner prompt and in executor guardrails with a record cap and a confirm flow, because prompts steer and guardrails guarantee.
FR24 is Data Provider #1, and a test is what keeps that true.
The whole point was a pattern, not a flight tracker: everything downstream of the normalization adapter is supposed to know nothing about aviation, so a future MCP exposing ships or weather or database tables plugs into the same pipeline. That is exactly the kind of rule that’s true the day you write it and false a month later — so it’s a test. tests/boundary.test.ts fails the build if anything in core, analytics, llm, or app imports provider code or so much as names an FR24 tool. It caught a real leak: the first cut of the data route reached straight for the FR24 transport, and the offline fallback planner had tool names hardcoded in the LLM layer.
It also decided where a security fix went. A sandbox key pasted into the Vercel dashboard carried a leading newline; fetch rejected the header and put the entire header value into its own error message, which the route returned to the client — the credential was readable in an HTTP response body. Found in production, not in review. The fix is trimming plus redaction on every outbound error path, and it lives in core/secrets.ts rather than inside the FR24 transport, because the Anthropic provider had the identical bug and isn’t allowed to import from a provider.
WHAT IT TAUGHT ME
A question really can assemble its own application — but only if the model gets a small closed vocabulary and is refused everything outside it. Every part of this that feels like intelligence is the planner picking from 16 operations; every part that has to be correct is ordinary tested code.
The rest of what I’d keep is about honesty. A dashboard saves to a shareable link that carries the question and never the answer, so no flight data is stored anywhere and there’s nothing to expire. The compare-to-previous-weeks control refuses to draw a baseline for metrics that have no past — “how many aircraft are over water right now” can’t be asked about last Tuesday — and says why instead of inventing a series. And the proximity feature is called aircraft proximity analysis, never collision detection: an analytical lens over public position data with visible, adjustable thresholds, not a safety system.
I set out to find how easy it is to build a dashboard on top of an MCP. The MCP turned out to be the easy part.