The platform API
Everything a creator does with Claude Code through the five-on-it MCP integration (Creating games), a script can do over HTTP: the MCP server is a thin adapter over this API, and so are the creator in the game and the studio. The API has the rights of the account that calls it and the same quotas; it serves the guides and the example games the tools read, so a script reads what Claude reads.
Its description is OpenAPI 3.1, at GET /openapi.json: every public route with what it does, who may call it and, for the creation loop, the bodies with the JSON Schemas of the game document, the mission and the level spec. A route the API registers without a description stops it from starting, so the document never lags behind it. The route table with every detail is in apps/api/src/app.ts.
The address is the platform's with /api (behind the stack's proxy: https://play.example/api), or http://localhost:8080 under npm run dev:platform. Below, $FOI_API_URL is it.
Signing in
Every route but the reading ones takes a bearer token: Authorization: Bearer <token>.
- A session:
POST /auth/login {email, password}answers{token}(a development API makes the account on sight, with any password).POST /auth/guest {deviceId}signs a guest in: a guest plays and rates, it does not create. On a platform for the game's owners (FOI_ENTITLEMENT=steam), only Steam signs in:POST /auth/steamwith the desktop app's ticket, or a browser's round trip fromGET /auth/steam/openid/startandPOST /auth/steam/exchange. - OAuth 2.1 with PKCE (S256), as Claude Code signs in for the MCP server: the metadata at
/.well-known/oauth-authorization-server, dynamic registration at/oauth/register, the sign-in page at/oauth/authorize, tokens at/oauth/token, revocation at/oauth/revoke. Access tokens are short; a refresh token rotates and lapses after 30 days unused.
There are no personal API tokens: a script signs in as its creator.
FOI_TOKEN=$(curl -s -X POST "$FOI_API_URL/auth/login" -H 'content-type: application/json' \
-d '{"email": "you@example.com", "password": "your password"}' | node -p 'JSON.parse(require("fs").readFileSync(0)).token')
AUTH="authorization: Bearer $FOI_TOKEN"
curl -s "$FOI_API_URL/me" -H "$AUTH" # the account, its limits a day and what is leftWhat a game is written from
These answer anyone, without a token:
| Route | What |
|---|---|
GET /guides | The guides, each with its ## sections: missions (the mission language), tiles (a region's), city-spec (a city drawn freely), tone. |
GET /guides/:name?section= | A guide as {name, title, text}, or one section of it (section: the heading's words, any case). |
GET /schemas, GET /schemas?template= | The JSON Schemas of a game document, a mission and a level spec; one template's alone (grid-town, region, city), a fraction of the whole. |
GET /catalogue?q=&limit=, GET /catalogue/:id | The blocks a game may name: kit pieces, roads, places, zones, vehicles, the mission language's verbs, events, conditions and actions, each with its parameters and an example. |
GET /levels, GET /levels/:name, GET /levels/:name/game | The bundled levels; one's roads, spawns, places and zones; its bundled game whole, an example that passes every check. |
curl -s "$FOI_API_URL/guides/missions?section=a%20mission" | node -p 'JSON.parse(require("fs").readFileSync(0)).text'The creation loop
A draft is a game in the making, private to its creator: its game document, and for a generated level its level spec. Every change makes a new revision; a build is of one revision.
# 1. A draft on a city drawn freely (or {"title": …, "level": "test-mission-basics"} on a bundled level).
DRAFT=$(curl -s -X POST "$FOI_API_URL/drafts" -H "$AUTH" -H 'content-type: application/json' \
-d "{\"title\": \"Harbour Hustle\", \"spec\": $(cat my-city.spec.json)}" | node -p 'JSON.parse(require("fs").readFileSync(0)).id')
# 2. The spec again, changed: solved at once, its issues at their paths, or its map, plan and warnings.
curl -s -X PUT "$FOI_API_URL/drafts/$DRAFT/spec" -H "$AUTH" -H 'content-type: application/json' \
-d "{\"spec\": $(cat my-city.spec.json), \"ifRevision\": 1}"
# 3. One mission, with the places and zones it needs (201 when added, 200 when replaced).
curl -s -X PUT "$FOI_API_URL/drafts/$DRAFT/missions/first_run" -H "$AUTH" -H 'content-type: application/json' \
-d '{"mission": {"title": "First Run", "objectives": [{"id": "go", "verb": "goto", "to": "depot"}]},
"places": {"depot": {"x": 0, "z": -60}}, "zones": {"depot": {"place": "depot", "radius": 8}}}'
# 4. A build, waited for: the generated level's places, the validator's issues, a preview.
BUILD=$(curl -s -X POST "$FOI_API_URL/drafts/$DRAFT/build" -H "$AUTH" | node -p 'JSON.parse(require("fs").readFileSync(0)).id')
curl -s "$FOI_API_URL/builds/$BUILD?wait=60" -H "$AUTH"
# 5. Played in a private room (a join URL for the web client's ?server=), then published.
curl -s -X POST "$FOI_API_URL/drafts/$DRAFT/playtest" -H "$AUTH"
curl -s -X POST "$FOI_API_URL/drafts/$DRAFT/publish" -H "$AUTH" -H 'content-type: application/json' -d '{"visibility": "link"}'| Route | What |
|---|---|
POST /drafts {title, blurb?, level | spec} | A draft on a bundled level or a level spec. |
GET /drafts, GET /drafts/:id | Your drafts with their last build and open playtest rooms; one draft, with the places its last build found. |
PUT /drafts/:id/game {game, ifRevision?} | The whole game document (its id and level stay the draft's). |
PUT /drafts/:id/missions/:missionId {mission, places?, zones?, ifRevision?} | One mission added or replaced, its places and zones merged into the game's. |
DELETE /drafts/:id/missions/:missionId?ifRevision= | One mission removed; its places and zones stay. |
PUT /drafts/:id/spec {spec, ifRevision?} | The level spec: the answer says its template (kind), and a region or a city is solved at once (its map in text, a city's plan, the solver's warnings, or its issues). |
POST /drafts/:id/build, GET /builds/:id?wait= | A build queued; how it went, waiting up to wait seconds (60 at most) for it to end, its place in the queue while it waits. |
POST /drafts/:id/playtest {mission?} | A private room on the last good build, or a playtest run of one mission. |
POST /drafts/:id/publish {visibility} | A new version of the game, by link or public (a public game's texts are moderated first). |
POST /games/:id/remix | A published game's latest version copied into a new draft of yours, credited. |
The MCP tools are these calls: draft_create is POST /drafts, game_set the PUT …/game, mission_upsert and mission_remove the mission routes, level_spec_set the PUT …/spec, draft_build the build and its long poll, draft_playtest, draft_publish, room_open (POST /games/:id/rooms, POST /rooms/:id/join), games_search (GET /games) and game_remix.
Revisions: writing beside the studio and the chat
The creator chat, the studio and Claude Code may write the same draft. Every write on a draft (its game, a mission, its spec) takes ifRevision, the revision it was meant for (in the body; a DELETE's in the query). When the draft has moved on, the API refuses it with 409 and the draft as it is now ({error, draft}): read it, apply the change again, write on that revision. Without ifRevision a write is taken whatever the revision, as before. The writes on one draft are taken one at a time, so two meant for the same revision never both pass.
Refusals
A refusal is JSON with its status: {error}, and for a document that does not pass, issues at their paths (spec.roads[5].from.onto, game.missions.2.objectives), each {path, message, severity}.
| Status | Means |
|---|---|
| 400 | The body does not match its schema (issues). |
| 401 | Not signed in, or the token lapsed. |
| 403 | The account may not (a guest creating; on a Steam platform no copy of its own; code says which). |
| 404 | No such thing, or not yours. |
| 409 | A write meant for another revision (draft); a playtest or a publish before a good build of the current revision; a draft a creation is writing. |
| 422 | Moderation refused the texts (reasons). |
| 429 | Today's quota of builds, rooms or creations is spent (they start again at midnight UTC). |
| 503 | No game server took the room. |
Quotas
Builds, rooms and creations are counted per account and day (UTC): GET /me says the limits, what was used and what is left. An admin may override an account's (Platform).