Case study

An open tournament data site on WREN

data.tkd-scores.com lists who is entered in upcoming World Taekwondo events, with each athlete's world-ranking seed, Olympic rankings and past results. It is a browsable site and an open JSON API at the same time. A scheduled pipeline writes the data, WREN stores and serves it, and a small Cloudflare Worker puts it on its own domain. This is how it's built, what we worked around, and how we'd build it with WREN 0.10.0.

Written October 2026 from the project's code, scripts and git history, notes from earlier working sessions, a backup of its WREN data from 2026-10-05, and the public site on 2026-10-05.

The site

data.tkd-scores.com covers World Taekwondo (WT) and WT Europe kyorugi and poomsae tournaments. For each event it shows the registered athletes per weight category with their current world-ranking rank and points, and for past events where each athlete finished and how many points they earned. A separate page shows the Olympic (LA 2028) rankings. The data is scraped from the registration platform the events use. Events that are still open for registration and start within a month refresh daily, the full set every three days.

At a glance
Content88 events in the index (28 upcoming or running, 60 past), 36,237 entries in total; Olympic rankings for 8 weight classes and 1,488 athletes
Writerone machine running a bash, Node and Python pipeline on a schedule: daily at 09:00 and a full run every three days
WREN dataits own org; public collection events with 94 documents and 4,096 versions; tree events with 92 paths (88 event slugs plus index, olympic, results-map, olympic-declarations)
Sitepublic tree site with 13 paths; the app is a single 84 KB HTML file served as /index.html
Front doora 56-line Cloudflare Worker on the custom domain

Who uses it

  • Coaches, athletes and fans, in the words of the tkd-scores.com landing page. The site highlights Switzerland by default; any country, club or athlete can be highlighted or searched instead.
  • AI assistants. The site publishes its own llms.txt and a help page with prompts people can paste into a chat assistant.
  • Other projects. The live tracker at tournaments.tkd-scores.com (see the tracker case study) reads the entry lists every day to build provisional brackets for events whose registration has closed, and to fill in athletes' clubs. An analysis project for team selection read the same entry lists and seeds.

Part A

As built

The org was created on 2026-04-09, and the first version of the events index was written the same day. Everything since then was added in small steps, almost all of it by AI coding agents; how they release changes is described in websites maintained by AI agents.

Architecture as built A scheduled pipeline scrapes the registration platform, looks up rankings, rebuilds each event as a local JSON file, and replaces the WREN documents in the public events collection. Site files are uploaded by hand into the public site tree. A Cloudflare Worker on data.tkd-scores.com serves pages from the site tree and passes /api requests to WREN. Browsers and AI assistants use the domain; other projects read the public WREN API directly. Registration platform Scheduled pipeline headless Chrome scrape ranking lookups local JSON per event Page uploads multipart PUT + tree PUT staged under /preview/ WREN · public data collection events tree events: /{slug} /index /olympic … no schema, no natural key rule *: read, no label filter WREN · public tree site index.html (the app) olympic, guide, llms.txt rule *: read, no label filter Other projects read the API directly Cloudflare Worker data.tkd-scores.com /api/* → WREN API else → tree site BUILD id, 30 s edge TTL Browsers and AI assistants
Figure 1. As built: everything the site shows is public in WREN, and the local JSON files on the pipeline machine are the source of truth.

Data model

Each event is one JSON document in the events collection: name, dates, city, country, registration deadline, venue, the event's documents, a registration history, and a report with every entry. A seeded entry looks like this:

json · one event, shortened
{
  "name": "…", "startDate": "09/19/2026", "endDate": "09/20/2026",
  "city": "…", "country": "…", "regDeadline": "…", "venue": { … }, "documents": [ … ],
  "history": [ …, { "date": "2026-09-16", "total": 929, "cadet": 233, "junior": 309,
                    "senior": 387, "male": 499, "female": 430 } ],
  "report": { "divisions": [ { "name": "SENIOR DIVISION", "categories": [
    { "name": "F-57 kg", "total_athletes": 24,
      "ranked":   [ { "rank": 11, "points": 32.48, "name": "…", "country": "…", "club": "…",
                      "gmsId": "…", "result": { "place": …, "points": …, "gRank": … } }, … ],
      "unranked": [ … ] } ] } ] }
}

result is filled in for past events. In this category 13 athletes are seeded and 11 are unranked.

Some details matter for what follows:

  • The slug lives only in the URL. Documents have no slug field. An event is found by its path in the events tree, for example /{event}-2026.
  • One collection for everything. The events index, the Olympic rankings, a 4.2 MB map of athlete results and the Olympic declarations are documents in the same collection, mounted at fixed paths.
  • No schema. Dates are MM/DD/YYYY strings, so a where on dates doesn't work; both the pipeline and other projects parse dates in their own code.
  • Sizes. The average event document is 161 KB of JSON; all current documents together are 15 MB.

Getting data in

The pipeline opens the registration platform in headless Chrome (the site is behind a Cloudflare challenge), reads the entry lists through its REST API, looks up each athlete's ranking, and writes one JSON file per event to a local folder. The local folder is the source of truth. The deploy step then pushes those files to WREN:

bash · simplified from the pipeline
# 1. map slugs to document ids, from the public tree listing
curl -s "$WREN_BASE/api/v1/orgs/{slug}/tree/events" > tree.json      # children: [{path, documentId}]

# 2. a new event: create the document, then mount it at /<slug>
id=$(curl -s -X POST "$WREN_BASE/api/v1/events" -H "Authorization: Bearer $WREN_API_KEY" \
       -H "Content-Type: application/json" --data-binary @"events/data/$slug.json" | jq -r .id)
curl -s -X PUT "$WREN_BASE/api/v1/tree/events/$slug" -H "Authorization: Bearer $WREN_API_KEY" \
     -H "Content-Type: application/json" -d "{\"documentId\":\"$id\"}"

# 3. a known event: replace the whole document
curl -s -X PUT "$WREN_BASE/api/v1/events/$uuid" -H "Authorization: Bearer $WREN_API_KEY" \
     -H "Content-Type: application/json" --data-binary @"events/data/$slug.json"

Around that core sit the safeguards the project needed over time:

  • A single-instance lock. A daily run and a full run share the local folder and the live documents, and two runs at once would corrupt each other.
  • A recovery script. When the WREN server was unreachable mid-deploy, finish-deploy.sh polled the events index for up to six hours and then pushed everything again.
  • A bootstrap step. When the project moved from a Mac to a Windows machine in September 2026, the local folder had to be filled from WREN before the first run. Otherwise a daily run would have pushed documents without athlete results over the good ones in WREN.

Site pages are deployed separately: each file is uploaded with a multipart PUT to its document in the media collection, and new files are mounted in the site tree.

Serving it

Both trees are public, so nothing needs a key to read. The Worker on data.tkd-scores.com forwards /api/* to WREN and serves everything else from the site tree:

js · Worker, simplified
// wrangler.toml: UPSTREAM = "https://wren.aemwip.com/orgs/{slug}/tree/site", BUILD = "2026-08-08a"
if (path.startsWith("/api/")) {                       // the app's data and _query calls
  const r = await fetch(new Request(WREN + path + url.search,
              { method: request.method, headers: clean(), body: request.body }));
  const out = new Response(r.body, r);
  out.headers.set("cache-control", "public, max-age=30, must-revalidate");
  return out;
}
if (path.endsWith("/")) path += "index.html";          // directory → index.html
const cb = (url.search ? "&" : "?") + "cb=" + env.BUILD; // bump BUILD to flush the edge
const mutable = /\.(html|js|json)$/i.test(path);
return fetch(env.UPSTREAM + path + url.search + cb,
             { cf: { cacheTtl: mutable ? 30 : 3600, cacheEverything: true } });

The app is a single HTML file. It loads /api/v1/orgs/{slug}/tree/events/index for the overview and one tree path per event, and reads the payload from .document.data. Searching for an athlete across all events is a single public _query, with no search index:

js · from the app
// which events list this athlete? array paths are unnested, $path is the event slug
const res = await fetch("/api/v1/orgs/{slug}/events/_query", { method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    where: `report.divisions[].categories[].ranked[].gmsId ~* ^${id}$`,
    aggregate: { groupBy: ["$path"],
                 metrics: { n: { count: "report.divisions[].categories[].ranked[].gmsId" } } },
    limit: 300 }) });
// the app runs the same query for unranked[] and merges the slugs from rows[].key.$path

A second Worker serves a one-page landing site on the tkd-scores.com apex and links to both tools.

For AI assistants

The site has its own /llms.txt (3.6 KB). It describes the index, one event report, the Olympic rankings and the cross-event query, notes that tree responses wrap the payload as { "document": { "data": … } }, and links WREN's generated org llms.txt as the "underlying store schema". A help page, /llms.html, gives people prompts to paste into a chat assistant. A hook in the project reminds whoever changes the app to check whether the help page, llms.html and llms.txt need an update too.

Timeline and scale

WhenWhat
2026-04-09Org created; first events index and first version of the app
August 2026Olympic page (Aug 4), athlete results map (Aug 7), llms.txt and llms.html (Aug 27); the Worker's build id is 2026-08-08a
2026-09-16Pipeline moved from a Mac to a Windows machine; daily run at 09:00, full run every three days
2026-09-17Mobile-first redesign live (app version 43), after staging under /preview/
2026-10-05Events index at version 132; 4,096 event versions in total

Writes grew month by month. Versions added to the events collection per month: April 294, May 254, June 248, July 583, August 902, September 1,673. All 4,096 versions hold 440 MB of JSON (175 MB as stored).

What worked well

  • Open data with no backend code. The same public URLs feed the app, AI assistants and the tracker's daily jobs. No one wrote an API.
  • _query over nested arrays. Athlete search across every event and every category is one request against public data.
  • Versions as a time series. The registration curves on the event cards were first rebuilt from each event's version history, one snapshot per day. The curve now lives in each document's history.
  • Rollback without backups. The rollback plan for the redesign was simply "WREN version 41 of the app".
  • A small front door. The custom domain is 56 lines of Worker code.

Warts and workarounds

An honest list. The status column refers to WREN 0.10.0, the release that publishes this page.

The workarounds from all case studies, with the better way for each, are collected in I did this. Is there a better way?

What happenedWorkaroundRoot causeStatus
Every deploy first listed the tree to find document ids; a new event took a POST plus a tree PUTslug-to-id map built from GET /tree/eventsNo natural key; the slug is only a tree pathAvailable: naturalKey and PUT /by-key (Part B)
2,078 of 4,002 event updates were identical to the version before (228 MB of JSON)NoneEvery run replaces whole documents, changed or not, and a JSON write always made a versionFixed in 0.9.0: a write equal to the current version returns "unchanged": true and creates no version; trim the old repeats with retention
Date filters failedParsing dates in Python and JavaScriptMM/DD/YYYY stringsData modeling: ISO dates and a schema; since 0.9.0 range filters compare ISO dates (Part B)
Files stayed stale at the edgeBuild id in the cache key; 30-second edge TTL for HTML, JS and JSONWREN used to send a long max-ageFixed: max-age=60, stale-while-revalidate; errors no-store
A redesign needed a preview that worked on a phoneA copy under /preview/ in the public tree; after go-live, 12 paths and 11 documents deleted by handNo label filter on the site rule; no atomic promote at the timeAvailable: labels and atomic promote (Part B)
Listing the media collection stopped at 200 documentsNone needed at the timeThe list endpoint returns at most 200Use _query with cursor (paging fixed in 0.7.0)
791 of 937 public media documents aren't part of any tree, but anyone can list themNoneA collection:media rule in addition to tree:siteA tree rule alone serves a site (Part B)
A WREN outage interrupted a deployA script that waits up to six hours, then redeploysServer unreachableOperational; by-key writes make re-runs safe
The local folder had to be rebuilt from WREN on a new machineA documented manual bootstrapLocal files are the source; writes replace whole documentsDesign choice, see Part B
On Windows, slug\r broke ids, and CRLF in .env corrupted valuesStripping \r; pinning LFShell glue between bash and native PythonFewer pipes once writes are single upserts

Part B

The same site, built today

Same scraper, same app, same domain, same public URLs. What changes is that the workarounds above become WREN features. Existing tree URLs stay, because other projects and the site's llms.txt link to them.

Architecture, built today The pipeline upserts changed events by natural key into a public events collection with a schema, a materialized list and a retention policy. Pages are deployed with a preview label and promoted atomically into a site tree whose public rule only shows published versions. The Worker maps the domain and its /mcp route. Other projects get webhooks and live events instead of polling. Scheduled pipeline PUT …/by-key/{slug} changed events only wren deploy --label preview wren promote site WREN · public data events: schema, naturalKey slug _materialized/upcoming ISO dates, where works rule *: read, no label filter retention on events no public media rule WREN · public tree site rule *: labelFilter published one transaction per release Other projects webhook or SSE Cloudflare Worker pages → /orgs/{slug}/tree/site /mcp → /orgs/{slug}/mcp public reads only Browsers, agents fetch, wren.js, MCP
Figure 2. Built today: WREN becomes the source of truth for events, pages are released with preview and promote, and changes are pushed to whoever needs them.

1 · Keys and upserts

Put the slug into each document and let an admin declare it as the natural key, together with a schema that requires ISO dates. Run the proposed schema through POST /api/v1/events/_schema/validate first: it reports which existing documents would fail without changing anything. Then write each event once with a slug field. Since 0.9, setting the natural key registers the keys of the documents that already exist, so the first upsert by key finds the old event instead of creating a duplicate; if two live documents share a slug, the schema change is refused with 409 and lists them.

http · admin, once
PUT /api/v1/events/_schema
{
  "naturalKey": "slug",
  "displayName": "{name}",
  "schema": {
    "type": "object",
    "required": ["slug", "name", "startDate", "endDate"],
    "properties": {
      "slug":      { "type": "string", "pattern": "^[a-z0-9-]+$" },
      "startDate": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
      "endDate":   { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }
    }
  }
}

The index, Olympic rankings and results map move to their own keyed collection (for example datasets), so events holds only events. The deploy step becomes one idempotent call per event. The client no longer needs to know what changed: since 0.9, a write whose JSON equals the current version (compared by meaning, so key order and whitespace don't matter) returns 200 with "unchanged": true and creates no version.

bash
for f in events/data/*.json; do
  slug=$(basename "$f" .json)
  code=$(curl -s -o resp.json -w "%{http_code}" -X PUT \
           "$WREN_URL/api/v1/events/by-key/$slug" \
           -H "Authorization: Bearer $WREN_API_KEY" -H "Content-Type: application/json" \
           --data-binary @"$f")
  case $code in
    200) jq -e .unchanged resp.json >/dev/null && echo "same $slug" || echo "updated $slug" ;;
    201) mount_in_tree "$slug" ;;                                             # new event
    *)   echo "PUT $slug failed: $code" >&2 ;;
  esac
done

No tree listing, no id map, and a run that dies halfway can simply run again. A 201 means the event is new, which is the only time a tree PUT is still needed to keep the /tree/events/{slug} URLs working. New readers can use GET /api/v1/orgs/{slug}/events/by-key/{slug} instead.

With WREN as the source of truth, a new machine doesn't need a bootstrap step. The pipeline reads the current document by key, merges in what it scraped, and writes it back.

2 · Public rules, on purpose

Entry lists change every day and should be visible as soon as they're written, so the data rules keep no label filter. Pages are different: they should change only when released. The media rule goes, because a tree rule alone serves every file in the tree.

http
# data: public and live, as today
{"principal":"*", "resource":"collection:events", "access":"read"}
{"principal":"*", "resource":"tree:events",       "access":"read"}
# pages: only what was promoted
{"principal":"*", "resource":"tree:site",         "access":"read", "labelFilter":"published"}
# removed: {"principal":"*", "resource":"collection:media", "access":"read"}

Order matters. Label the live files first (wren promote site), then add the label filter. The other way round, the site is offline until the next promote. And before dropping the media rule, make sure nothing links to raw media URLs directly.

3 · Releasing pages: preview, then promote

The redesign was staged as a copy under /preview/ and cleaned up by hand afterwards. With a label filter on the site tree, a release looks like this:

bash
export WREN_URL=https://wren.aemwip.com WREN_API_KEY=wren_…    # from the environment, never in a script
wren deploy ./site --tree site --label preview                 # changed files only (SHA-256)
curl -H "Authorization: Bearer $WREN_API_KEY" \
  "$WREN_URL/api/v1/tree/site/index.html?label=preview"          # check it
wren promote site --from preview --label release-2026-10-05    # name the release
wren promote site --from preview                               # every page live in one transaction

The app, the Olympic page, the help page, llms.txt and the screenshots change together, so visitors never get a new app with an old help page. Rolling back is wren promote site --from release-2026-09-17 instead of a git tag and a version number on a sticky note. Unchanged files are stored once and create no new version.

4 · The index as a query

The events index is built by a script that reads every event file. Part of it is a plain projection, and a materialized query keeps that part current after every write:

http · admin, once
PUT /api/v1/events/_materialized/upcoming
{
  "query": { "select": ["slug", "name", "startDate", "endDate", "city", "country", "regDeadline"] },
  "refreshOn": "write"
}

The rest of the index is computed outside WREN: entry counts per country, registration trends, and bracket links found by probing the tracker site. Those fields stay in a document the script writes, and that document pulls the live list in with a $ref, so one read returns both:

json · datasets/index
{
  "slug": "index",
  "brackets": { "example-open-2026": ["https://tournaments.tkd-scores.com/…"] },
  "events": { "$ref": "query:events",
              "$select": ["slug", "name", "startDate", "endDate", "city", "country"],
              "$limit": 200 }
}
// GET /api/v1/orgs/{slug}/datasets/by-key/index?depth=1

With ISO dates, "what's coming up" is also a plain filter: since 0.9, range operators compare anything that isn't a number as text, so GET /api/v1/orgs/{slug}/events?where=endDate>=2026-10-05&select=slug,name,startDate works on the public route. Before, every reader parsed MM/DD/YYYY in its own code.

References resolve under the reader's own rules, so a public reader only ever sees public data through them. A club website that just wants a list of upcoming events doesn't need the app at all:

html
<script src="https://wren.aemwip.com/wren.js"
        data-base="https://wren.aemwip.com/api/v1/orgs/{slug}"></script>
<ul>
  <wren-materialized collection="events" name="upcoming">
    <template><li>{{name}}, {{city}} ({{startDate}})</li></template>
  </wren-materialized>
</ul>

5 · The domain and its agents

The Worker keeps mapping the domain onto /orgs/{slug}/tree/site. Two things change. Its cache workarounds can go: WREN now sends max-age=60, stale-while-revalidate on public reads and no-store on errors, which the live site already shows. And /api/* should only reach public reads. Today it forwards any method with its headers, so the domain also works as an alias of the private API for anyone who sends a key.

js · Worker additions
// public data only: GET, and POST for _query, under /api/v1/orgs/{slug}/
const isPublic = path.startsWith("/api/v1/orgs/{slug}/") &&
  (request.method === "GET" || (request.method === "POST" && path.endsWith("/_query")));
if (path.startsWith("/api/") && !isPublic) return new Response("Not found", { status: 404 });

// AI agents: this domain's own MCP endpoint
if (path === "/mcp") return fetch(env.MCP, { method: request.method, headers: mcpHeaders(request),
                                             body: request.method === "POST" ? request.body : undefined });
// wrangler.toml
// MCP = "https://wren.aemwip.com/orgs/{slug}/mcp?tree=events&collections=events"

Without a key, an agent connected to https://data.tkd-scores.com/mcp gets read-only tools over exactly what the domain publishes, and can run the same _query aggregations as the search box. That replaces "read llms.txt, then fetch JSON by hand" for clients that speak MCP; llms.txt stays for those that don't. With a key from the site's org, the owner's own agents get full tools; keys from other orgs are refused.

Agents write to live data here. MCP writes label the new version preview, but the events rule has no label filter, so the new version is public at once. Give agents that only need to read a key narrowed to read access with key: rules.

6 · Reacting to changes instead of polling

The tracker's daily job looks for events whose registration has closed. A webhook on the events collection tells it when something changed; deliveries are batched over five seconds, signed with HMAC, retried, and can be replayed for seven days:

http
POST /api/v1/webhooks
{ "url": "https://hooks.example.com/tkd-events", "events": ["document.created", "document.updated"] }
// payload events carry collection, id, version and key (the slug), never the data

The receiver must be a public HTTPS address. The tracker's job runs on a desktop machine today, so it either keeps its daily check or gets a small public receiver. For a browser, live events need no setup at all: an open event page can listen to the public stream and reload its document when a run updates it. Because unchanged writes create no version, a run that finds nothing new sends no events at all.

js
const es = new EventSource("https://wren.aemwip.com/orgs/{slug}/_events?collections=events");
es.onmessage = e => {
  const ev = JSON.parse(e.data);
  if (ev.type === "document.updated" && ev.key === currentSlug) reloadEvent(currentSlug);
  if (ev.type === "reset") reloadEvent(currentSlug);   // missed events: re-read
};

A receiver that wants to know what changed, say who entered since yesterday, can ask WREN instead of comparing files. With deep=true (0.9), the diff walks nested objects and arrays and reports each changed leaf:

http
GET /api/v1/events/{id}/diff?v1=130&v2=131&deep=true
→ { "op": "add",     "path": "/report/divisions/2/categories/4/unranked/7", … }
  { "op": "replace", "path": "/history/41/total", "value": …, "oldValue": … }

7 · Keeping history in check

Each run rewrote every event it touched, changed or not, so history grew either way. Since 0.9, identical writes create no version, so new repeats stop without any client code. For the history that's already there, a retention policy on events trims old versions; the current version and every labeled version are always kept:

http · admin
POST /api/v1/retention/events/_preview   {"maxVersions": 30}     # what would go, changes nothing
POST /api/v1/events/{id}/labels          {"label": "final"}      # keep a finished event's last entry list
PUT  /api/v1/retention/events            {"maxVersions": 30}     # then hourly

On the 2026-10-05 backup, maxVersions: 30 would remove 1,754 of 4,096 event versions, 121 MB of the 440 MB of JSON.

Versions were once the history. The registration curves were first rebuilt from version history, and the backfill script that did it replaces each event's history with what it finds in the versions. The curves now live in the documents, so trimming loses nothing the site shows, but retire that script before the first trim.

Files are stored once per content since 0.8.0. The site's own files gain little, since every version of them is different. The org as a whole does: its media collection has 1,047 file versions taking 118 MB, with only 338 distinct contents (about 14 MB).

Side by side

ConcernAs builtBuilt today
Finding a documenttree listing, slug-to-id mapby-key/{slug}
Writing an eventPOST + tree PUT, or PUT by idPUT /by-key/{slug}, changed files only
Source of truthlocal JSON folderWREN
DatesMM/DD/YYYY, parsed in codeISO, enforced by schema
Indexbuilt by a scriptmaterialized query plus a $ref
Page releaseslive on upload; /preview/ copypreview label, atomic promote, named releases
Edge cachingbuild id in cache key, 30 s TTLWREN's headers
Public surfaceall of media listableonly the trees and events
AI assistantsllms.txt, fetch by handllms.txt plus /mcp on the domain
Other projectsdaily pollingwebhooks or live events
History sizeevery run adds a versionunchanged writes add none; retention policy

Still open

  • Keys and paths are separate. An upsert by key doesn't mount a tree path. As long as the /tree/events/{slug} URLs are kept, a new event still needs one tree PUT.
  • Previews on a phone. A labeled preview is only visible with a key. The redesign was staged on the public domain because a phone couldn't reach the development machine. Checking a preview on a phone needs a signed-in browser or a private proxy that adds the key.
  • Natural keys need an admin. That's intentional, but it is a one-time step before the pipeline can upsert.
  • Overlapping runs. With WREN as the source of truth, the pipeline reads an event, merges and writes it back. Since 0.10 the write can carry the version it read (If-Match), so an overlapping run gets 412 instead of overwriting; it re-reads and merges again. Keep the single-instance lock anyway: it avoids the wasted work.

Lessons

  1. Put the key in the data, not only in the URL. A slug that exists only as a tree path forced every deploy to rebuild a lookup table.
  2. Know what your writes cost. Half of this project's history is repeats. WREN now drops identical writes for you, but a pipeline that rewrites everything still pays for it in requests and time.
  3. Treat data and pages differently. Data: public, no label filter, live on write. Pages: labelFilter: "published", preview and promote.
  4. Make only what you mean public. A tree rule serves a site. A collection rule on the file store also publishes everything else in it.
  5. Let WREN be the source of truth. Local files as the source made moving machines a risk to live data.
  6. Describe your data for machines. A hand-written llms.txt made the data usable by assistants and other projects with no further work; an MCP route on the domain is the next step.

New to these ideas? Start with Concepts, or read the live tracker case study for the sibling site and websites maintained by AI agents for how this site's releases are made.