Skip to content

Creating games ​

A creator makes a Five On It game by talking to Claude in Claude Code with the five-on-it MCP integration (Game design document §10, Technical specs §9.7 and §9.8). Claude turns the description into data through the integration's tools: a level (an existing one, or one generated from a level spec) and missions in the mission language. The platform checks every document, builds it and opens rooms to play it in. Claude runs on the creator's own subscription. The other way is in the game: an owner writes a brief and the platform's own model makes the game with the same tools (below); Claude Code stays the way described here.

The integration is apps/mcp, a thin adapter over the platform API: it has the rights of the account it calls the API as, and the same quotas. A script does the same over HTTP (The platform API, its OpenAPI description at GET /openapi.json); the guides the tools read are the API's (GET /guides), so each reads the same.

Adding the integration to Claude Code ​

A deployed platform ​

console
claude mcp add --transport http five-on-it https://play.example/mcp
claude mcp get five-on-it   # must report it connected

The lines are the same in bash and in PowerShell. Claude Code signs the creator in with OAuth, the platform API being the authorization server (Technical specs §9.6):

  1. Until then the server answers anonymously: the catalogue, the levels and the public games. The first tool that acts as a creator (a draft, a build, a room) is answered 401 with WWW-Authenticate: Bearer resource_metadata="https://play.example/.well-known/oauth-protected-resource/mcp".
  2. Claude Code reads that metadata, which names the API (https://play.example) as the authorization server, reads the API's (/.well-known/oauth-authorization-server), registers itself (/oauth/register) and opens the API's sign-in page in the browser (/oauth/authorize, with a PKCE challenge). To sign in ahead instead: /mcp in Claude Code, the server, Authenticate.
  3. The creator signs in there with the e-mail and password of their account (Nakama's on a platform; a new e-mail makes an account), or through Steam where the platform takes Steam (on a platform for the game's owners, only through Steam and only with a copy of their own: Platform), then allows Claude Code: "Claude Code wants to make games as you". The browser goes back to Claude Code with a code, which Claude Code exchanges with its PKCE verifier for an access token (an hour) and a refresh token (30 days, replaced at every use).
  4. Every tool then acts as that account, with its rights and quotas. Claude Code renews the access token by itself; once the refresh token goes 30 days unused, or the sign-in is revoked (POST /oauth/revoke), the creator signs in again.

Instead of signing in, a client may send a session as a header (a script, or a client without OAuth). The platform API's sign-in answers with one ($FOI_API_URL is the API's address; behind the stack's proxy, https://play.example/api):

console
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')
claude mcp add --transport http five-on-it https://play.example/mcp --header "Authorization: Bearer $FOI_TOKEN"

In PowerShell (Windows), where curl is another command:

powershell
$FOI_TOKEN = (Invoke-RestMethod -Method Post -Uri "$FOI_API_URL/auth/login" -ContentType "application/json" `
  -Body (@{ email = "you@example.com"; password = "your password" } | ConvertTo-Json)).token
claude mcp add --transport http five-on-it https://play.example/mcp --header "Authorization: Bearer $FOI_TOKEN"

A pasted session expires (Nakama's after 24 hours); sign in again and replace the server (claude mcp remove five-on-it, then claude mcp add again) when the tools answer 401.

Your own Compose stack, on this machine or through ngrok ​

The stack (npm run stack:up, Deploying) serves the MCP server at /mcp on its address: http://localhost:8088 on its own machine, a tunnel's (ngrok http 8088) or a domain's elsewhere, with its OAuth sign-in on the same address (/oauth/authorize). The exact steps, with <domain> that address, are in Making a game in Claude Code on the stack: claude mcp add --transport http five-on-it-ngrok https://<domain>/mcp, signing in when Claude Code asks (or a session from POST /api/auth/login as a header), then /mcp__five-on-it-ngrok__create_game "…".

This repository, for development ​

console
npm run dev:platform   # game server with rooms on 7777, the API on 8080, the web client on 5173

Then start Claude Code in the repository. Its .mcp.json registers five-on-it over stdio (node apps/mcp/src/stdio.ts), calling the API on 8080 as the development account dev@five-on-it.local (development accounts are made on sight, with any password); Claude Code asks once to approve the project's servers, and /mcp shows them. The MCP server signs in on its first call, so the platform may start after Claude Code.

To try the HTTP transport instead, run npm run dev:mcp (the MCP server over HTTP on 8091, with npm run dev:platform running for the API) and add it by its address alone; the lines are the same in bash and in PowerShell:

console
claude mcp add --transport http five-on-it-http http://localhost:8091/mcp
claude mcp get five-on-it-http   # must report it connected

It names the API on 8080 as its authorization server, and Claude Code signs you in with OAuth. Until you sign in the tools act anonymously (the catalogue, the levels and the public games answer); the first tool that needs your account answers 401, and Claude Code opens the API's sign-in page in the browser (or sign in ahead: /mcp, the server, Authenticate). Development accounts take any e-mail, with a password of 8 characters or more.

Or, instead of signing in, paste a session as a header (a script, or a client without OAuth): sign in on the API and pass its token.

console
FOI_TOKEN=$(curl -s -X POST http://localhost:8080/auth/login -H 'content-type: application/json' \
  -d '{"email": "dev@five-on-it.local", "password": "development only"}' | node -p 'JSON.parse(require("fs").readFileSync(0)).token')
claude mcp add --transport http five-on-it-http http://localhost:8091/mcp --header "Authorization: Bearer $FOI_TOKEN"

In PowerShell (Windows), where curl is another command (an alias of Invoke-WebRequest in Windows PowerShell 5.1) and the block above does not run as it is:

powershell
$FOI_TOKEN = (Invoke-RestMethod -Method Post -Uri "http://localhost:8080/auth/login" -ContentType "application/json" `
  -Body (@{ email = "dev@five-on-it.local"; password = "development only" } | ConvertTo-Json)).token
claude mcp add --transport http five-on-it-http http://localhost:8091/mcp --header "Authorization: Bearer $FOI_TOKEN"

The loop ​

StepToolsWhat happens
Readcatalogue_search, catalogue_get, the resourcesThe blocks a game may use, the schemas, the mission language, the tone guide, the bundled games as examples.
Draftlevels_list, level_get, draft_create, draft_listA private draft, on an existing level or on one generated from a level spec.
Speclevel_spec_setFor a generated level: its size, districts, roads, landmarks, places, zones and seed.
Missionsgame_set, mission_upsert, mission_removeThe whole game document at once, or one mission at a time with the places and zones it needs. A document that breaks its schema is refused at once.
Builddraft_build, draft_getThe level is generated (when it has a spec) and every check runs; the tool waits and returns the validator's issues (a generated level's audit among its warnings: what was left out to keep the level clean, nothing for the spec to fix), the generated places, a link to the level's preview (a picture from above) and one to look round the level from a free camera in the web client (?preview=<artifact>, its places listed).
Fixthe same tools againClaude fixes each error, by its path in the document, and builds again before the creator sees anything.
Playtestdraft_playtestA private room on the good build, and a link that plays it in the browser; with a mission, a playtest run of that one mission in a room of its own (a co-op one tried alone).
Publishdraft_publish, room_open, games_searchA new version of the game, by link or public (public texts are moderated), with a link to its page (/?game=<id>: preview, missions, Play, ratings); a room to play it in, and its link.
Remixgames_search, game_remix, draft_getA published game (any public one, or your own) copied into a new draft of yours: its missions, and its level spec when its level is generated. The credit stays: every version it publishes says "Remix of title by creator".

games_search takes a sort (new, top, featured) and shows each game's rating, so Claude can start from the best-liked game of its kind.

A play link opens the web client on the room: http://localhost:5173/?server= followed by the room's join URL. The join token in it names the creator and lasts five minutes; room_open with the room's id gives a fresh one. Other players join public games' rooms from the client's Games tab.

The resources Claude reads: foi://schemas/game, foi://schemas/mission and foi://schemas/level-spec (the API's JSON Schemas), foi://guide/missions (the mission language), foi://guide/tone (the tone and writing guide) and foi://examples/main-riverbend, foi://examples/test-mission-basics (the bundled games).

The prompts, which Claude Code offers as slash commands: /mcp__five-on-it__create_game with a description, /mcp__five-on-it__add_mission with a mission (and a draft id; the latest draft otherwise) and /mcp__five-on-it__fix_errors (a draft id, or the latest). Each walks Claude through the loop above.

What a city can be asked for ​

A generated city is a level spec of the grid-town template (foi://schemas/level-spec); its districts are where a creator says what kind of city it is. Everything below is in the catalogue (catalogue_search finds it by words: hospital, wellness, setback), with an example a spec can use.

Ask forIn the specWhat the generator does
A kind of buildinga district's mix (weights) or landmarks (each once)125 buildings made from OpenStreetMap's tags (a bakery, a pharmacy, a barn, a guest house, a stadium…: Levels), and 32 hand-made ones in five families (GAME_DESIGN §5.2): civic (police station, hospital, fire station, town hall, school, courthouse, library, post office), commercial (supermarket, mall, bank, gas station, body shop, car rental, restaurant, bar, café, hotel, shops, offices, towers), residential (house, townhouse, apartments), industrial (warehouse, factory, depot, power station), transport (bus station, car park); and five landmarks one walks into and climbs, of several floors (mega-mall, general-hospital, mensa, castle, ski-lodge, the landmarks kit; a district that names one wants blockSize 120). Landmarks are placed first, the avenues first.
A building by what it isa tag in the mix or the landmarks: "amenity=hospital": 1Every building carries OpenStreetMap-style tags (building=…, amenity=…, shop=…, tourism=hotel, office=…, industrial=…, power=plant, parking=surface); a tag stands for the buildings carrying it. A tag's catalogue entry names those buildings, whose places are <district>.<archetype>_<n>, so a mission finds "a building tagged amenity=bank" among a build's places (campus.bank_1); the places in the game document carry the tags too.
What a district is fora district's landuse, or a wildcard in its mixretail, commercial, residential, industrial, civic, leisure or farm: built of that landuse's buildings, the commoner the more often; a mix may weigh shop=*, family:industrial, landuse:farm beside other keys.
A satire themea district's themeEight: tech-campus, wellness-quarter, luxury-waterfront, gig-depot, old-town, city-hall, high-street, farm-village. The theme sets the district's default mix and density, its title on the minimap, its buildings' invented names ("name": "Dashr Fulfilment Hub" in their places, for a mission's texts), the colours they lean to and the pedestrians it suggests.
How built-upa district's density: built, setback, heightbuilt 0.2 to 1 leaves the rest of a street to trees; setback 0 to 10 m puts the fronts behind lawns; height low or high leans the mix to low or tall buildings.
A name players seea district's title (40 characters at most)Written over the district on the minimap; a public game's titles are moderated. Invented names only (the tone guide's "never" list holds).
What a building doesnothing: its anchors come with itEach building's named points become places, <district>.<archetype>_<n>.<anchor>: its entrance, a shop's counter, a hospital's respawn and ambulance_bay, a police station's cruiser_1 and _2, a bank's atm, hall, vault, vault_gate, terminal and deposit_1 to _3 (a bank is a building one walks into), a car rental's pickup, a bus station's stop_1 and _2, a car park's bay_1 to _4, a school's yard, a loading_dock… Some carry a block: a body shop's garage is a garage (respray, storage), a car rental's lot_1 to _3 and a depot's van_1 and _2 park locked cars, a bank's vault_gate is a door (a gate locked by keycard:vault: a mission's door actor there makes it a keypad with a code) and its camera a security camera. test-buildings has one to walk into.
A town lived in"dressing": "lived-in" (a grid town's or a region town's; none by default)Bins, benches, mailboxes and newspaper boxes by the kerb, parking meters in a gig-depot or city-hall district, bike racks and e-scooters in a tech-campus, phone booths in an old-town; bus stops on the avenues (<town>.bus_stop_<n>), street signs, billboards on the roads in, back yards, car parks with cars parked in their bays (<town>.carpark_<n>; a hard hit shoves a parked car loose), a construction site with a crane (<town>.site), a playground in a park or an old town's market (<town>.market).

To see them all before asking, open the kit gallery, /?kit-gallery on the web client (npm run dev:web, no server needed): every building by its archetype and variant, every prop, then the region's terrain shapes, features, biomes and road classes, each labelled; ?kit-gallery=bank or =volcano looks at one. The panel in its corner finds anything by its name (/), goes to a section or a kit, maps the whole and sets the camera's speed (a big region wants it fast); over a building one walks into, [ takes its storeys off one by one to show its stairs, lifts, shops and named places, and ] puts them back.

Claude sees the same pieces in the catalogue: each piece of the kits the generators build with is a kit-piece entry (kit-piece.street.bench, kit-piece.civic.bld-bank-a), from the kit library (packages/catalogue/src/kits.generated.json, written by npm run kits:export after a kit is regenerated, see Levels). catalogue_search lists a piece with its size (width × depth × height in metres) and its tags (a building's OpenStreetMap ones, else words from its kit, kind and name: tree, bench, playground), and catalogue_get adds its bounds around its origin, its collider and its anchors with where each stands. A spec still names buildings and switches, not pieces: the pieces say what a level can hold and how big it is.

The build's places are the ids to use: which buildings stand where is the seed's choice, and only a district's landmarks are sure to stand. A mission meets a player at campus.bank_1.atm, sends them to wellness.hospital_1.respawn, or has them steal a car from depot.car_rental_1.lot_2. A walk-in landmark has places on every floor, each with its height: campus.mega_mall_1.food_court (the mall's top floor), campus.general_hospital_1.helipad (up the roof's ladder), old_town.castle_1.keep_roof; a zone on one holds a player within 3 m of that floor, not the floors under it.

What land can be asked for ​

A city drawn freely (the city template, its official spec in The city spec, foi://guide/city-spec) is a city like Riverbend: roads of six classes drawn through points that start and end on each other ({"onto": "<road>"}), at a roundabout's arms or on a turned grid's streets; rivers crossed by bridges, with quays along their banks; hills a lane climbs to a lookout; districts that take the lots nearest their seeds, each in one of Riverbend's thirteen styles (old-town, docks, garden-suburb…); parks and squares. Everything is in the game's frame (x grows westward, bearings as on a compass). level_spec_set solves it at once: every problem comes back at its path with where on the map it is (spec.roads[3].to: neither crosses nor reaches quay …), a city that can be built with its map in text. Its places are every roundabout, every hill's top, every bridge (bridge.<road>), the spec's own places and every district's buildings. test-city is one (apps/level-editor/assets/kits/test-city.spec.json, the catalogue's template.city example). Blender builds it on the generator worker: about 10 s for a small city, two to three minutes at Riverbend's size.

A region (the region template, the same foi://schemas/level-spec, its official spec in Tiles, foi://guide/tiles) is open land on a grid of 64 m tiles: hills, mountains, plateaus, valleys, lakes (water a player wades, swims and sinks a car in), volcanoes (lava in the crater) and ski runs, in a meadow, forest or alpine biome, with roads from a dirt track to a highway between the spawn apron, the region's edges, its features and each other. level_spec_set solves it at once: a region that cannot be built comes back with every problem's path (spec.features[0].level: the hill's peak rises to level 2, not 4: …), one that can with its map in text, so Claude can show the creator the land before building it. A region may hold towns too (towns): grid towns on its tiles, their streets climbing its hills and dipping into its valleys, a district on a flat terrace where asked; a road of the region ends at one by its name, and start puts the players in one (on its first east-west avenue) instead of on the spawn apron. A volcano may pour lava down one flank (flow); every lake comes with a beach and a jetty, every volcano with fumaroles and a lookout on its rim. Its places are every tile's middle (tile_<i>_<j>), every feature by its name (and <lake>_dock, <lake>_beach, <volcano>_lookout, <volcano>_crater, <volcano>_flow), every road's ends and every town's (<town>.<place>), for the missions. Not every one can be reached: a volcano's crater is lava and a steep hill's top may be too steep to climb, so a mission that sends a player there fails its build (places.volcano_crater: … cannot be reached from the spawn points); the lookout on the rim is the place to send them to. test-tiles-town is such a region, with a town on its hills (apps/level-editor/assets/kits/test-tiles-town.spec.json). A region without towns needs no Blender to build; its towns are built by the generator worker.

An example conversation ​

You: /mcp__five-on-it__create_game "a rental car that has to reach the drop-off before the app notices the scratches"

Claude: Read the tone guide and the mission language; test-mission-basics has a car park and a drop-off, so no level needs generating. Draft draft-3f9c2a17b0 created on it. Wrote one mission, "Scratch and Sniff": go to the checkpoint, take the rental compact from bay 2, deliver it to the drop-off within 120 s and under 60 % damage. The build found 1 error: missions[0].objectives[2].to names a zone drop_off the game does not have. Renamed it to drop and built again: it passes (preview: http://localhost:5173/api/previews/…png). Play it here: http://localhost:5173/?server=ws%3A%2F%2Flocalhost%3A7777%2Frooms%2F… (good for five minutes).

You: Fun. Publish it, public.

Claude: Published "Scratch and Sniff" (game g-3f9c2a17b0, version 1, public); its page is http://localhost:5173/?game=g-3f9c2a17b0, and it is in the Games tab. Opened a room: http://localhost:5173/?server=…

In the game: a brief ​

Where the platform runs its own model (FOI_CREATOR, Platform), an owner creates without Claude Code: the main menu's Create, or the creation page (?create), signed in.

  1. The brief. A few lines (600 characters at most): the place, the story, the missions. Where (Riverbend, a new town or a region), how big, and how many missions (1 to 3). It is the owner's wish, not instructions: the model makes a game of it within the catalogue and the mission language, nothing else.
  2. The queue. The creation waits its turn, its place in line said (and why it waits: the day's budget, or the owner's other creation still running), then runs with its step (reading the catalogue, starting the draft, writing the missions, building) and how long it has taken. Cancel stops it: before it starts, its creation is given back; while it runs, it stops within seconds and its draft so far stays. Where the platform moderates, a brief it refuses is turned away at once with the reason (no creation spent), and the draft a creation writes shows once its texts have passed.
  3. The result. The game's preview, its missions, its level in text (a region's) or its places, and Playtest (a private room), Look round (its level from a free camera), Publish (by link, its texts checked first) and Revise (a new creation on the same draft: say what to change). In a game the platform opened, a toast says when a creation ends, and opens it. A creation whose texts the moderation refuses fails with the reason and shows nothing of what it wrote (a draft it revised is put back as it was); its creation stays spent.

How many. A few creations a day (5 by default, reset at midnight UTC, never carried over); the page says how many are left and when they start again. The platform's model runs under a hard cap a month, spread over its days: when the day's share is spent, the page says when creation resumes and takes no brief (and spends no creation). Behind it, a creator runner (apps/creator) runs the model over the MCP server's tools in its own process, as the creation's own session, which reaches its one draft and nothing else.

Remixing from a game's page ​

A player who likes a game presses Remix on its page (/?game=<id>, signed in with a creator's account). The page makes the draft and shows how to go on in Claude Code: ask Claude to continue with draft_get {"draftId": "draft-…"}, and the loop above takes it from there (change the missions, draft_build, draft_publish). In Claude Code alone, games_search then game_remix {"gameId": "g-…"} does the same.

When a wish is beyond the blocks, Claude builds the nearest thing that exists and says so, in character ("Legal said no to the helicopter. You get a golf cart.").