# Teztopia SDK

Build a Teztopia scene outside the site, with your own AI agent or your own scripts, then
import it into the builder. If you own a plot on the mainland, build the whole plot on your
computer and deploy it with your wallet, like a Decentraland scene. From ChatGPT or Claude on
the web, connect the site itself: see [Use it from ChatGPT or Claude](#use-it-from-chatgpt-or-claude).

| File                  | What it is                                                                            |
| --------------------- | ------------------------------------------------------------------------------------- |
| `third-life-mcp.mjs`  | An MCP server. Your agent (Claude Code, Codex, Claude Desktop, Cursor, VS Code…) builds through it. |
| `third-life-pack.mjs` | Packs a scene into the ZIP the builder imports; makes your plot's folder (`init`) and deploys it (`deploy`). |
| `world.schema.json`   | The scene format as a JSON Schema, for scripts in any language.                       |

You need **Node.js 20 or newer** and nothing else: each `.mjs` file is self-contained. There is
nothing to install and no account. The server reads the files you point it at and writes your
scene and your ZIP, on your computer. It goes online for one thing only: when a scene uses a
prefab that carries model files (such as tiled roofs), the export downloads those files from the
site once and checks them by SHA-256. `--offline` turns that off.

## First scene in 2 minutes

1. Put the server in a folder of its own:

   ```sh
   mkdir teztopia && cd teztopia
   curl -O https://teztopia.xyz/sdk/third-life-mcp.mjs
   node --version     # v20 or newer
   ```

   On Windows PowerShell, write `curl.exe` instead of `curl`.

2. Give it to Claude Code, from that folder (macOS and Linux):

   ```sh
   claude mcp add teztopia -- node "$PWD/third-life-mcp.mjs" --scene "$PWD/my-world.json"
   ```

   On Windows PowerShell:

   ```powershell
   claude mcp add teztopia -- node "$PWD\third-life-mcp.mjs" --scene "$PWD\my-world.json"
   ```

3. Start `claude` in the same folder and ask:

   > Use the teztopia tools. Read describe_rules, then build a small walled garden with a
   > fountain, benches, cypresses and a pergola. Use prefabs where they fit. After each round
   > look at preview and describe_scene, fix what they show, then export_zip.

4. Open the [free builder](https://teztopia.xyz/builder.html), go to
   **Share**, choose **Import world** and pick `my-world.zip`. Press **Test walk** and walk it.
   Then keep prompting for details ("make the pergola posts stone", "turn the benches to face
   the fountain"), export again and import again.

## Your plot, like a Decentraland scene

If you own a plot on the mainland, it is a folder on your computer: build it there with any
agent, look at it, and deploy it with a signature from your wallet. A plot is 16, 32, 48 or 80 m
square, made of 16 m cells; the folder holds all of them.

1. **Get your plot's folder.** On the site, open your plot editor, go to **Share** and press
   **Download my plot template** (or open
   [the deploy page](https://teztopia.xyz/deploy.html?template=mine) signed
   in). It starts from your draft or your published plot. Or make it here, empty or from a scene
   you have:

   ```sh
   node third-life-pack.mjs init --plot 5
   ```

   `--plot` takes your plot's number or its north-west cell (`8,-3`); `--from my-world.json` starts
   from a scene (a one-cell scene goes into the cell at the street). The folder holds `plot.json`
   (the cells, the street entrance, the neighbouring plots, buildings and streets, the limits),
   `scene.json` (the plot), a README, an example prompt, both SDK files and the agent setups.

2. **Connect your agent** in that folder. Claude Code reads the `.mcp.json` that `init` writes:
   run `claude` there (a downloaded folder's README has the `claude mcp add` line for it). Codex,
   from the folder:

   ```sh
   codex mcp add teztopia -- node "$PWD/third-life-mcp.mjs" --scene "$PWD/scene.json"
   ```

   Cursor and VS Code read the folder's `.cursor/mcp.json` and `.vscode/mcp.json`. With the server
   already set up for a one-cell scene, ask the agent to call `new_plot` with your plot's number.

3. **Build.** Positions are metres from the plot's north-west corner, across the whole plot.
   Walls, floors, slabs and steps may cross the 16 m cell borders: they are cut into one piece per
   cell, which looks and collides as one. Anything else (columns, vaults across their arch,
   frames, models, a prefab's roof) must sit inside one cell, and the tools say where it fits.
   Every cell keeps its own limits (500 objects, 16 models…).

   > Use the teztopia tools. Read describe_rules, then build a small gallery quarter on my
   > 48 x 48 m plot, its front on the south side where the street arrives. After each round look
   > at preview and describe_scene and fix what they show. Then validate.

4. **Look.** `preview` draws the plot from above and from the street, with its numbered cells and
   the entrance; `describe_scene` lists objects per cell and walks the plot from the street with
   the game's own movement. `validate` checks every cell the way the site will.

5. **Deploy**, from the folder:

   ```sh
   node third-life-pack.mjs deploy
   ```

   It checks the plot, then opens the site's deploy page in your browser, which reads the plot
   from your computer: a server on `127.0.0.1` only, under a random link that works for 15
   minutes and answers only the site. On that page, signed in with the wallet that owns the plot,
   you see the plot drawn and what changes against the published version, cell by cell. Approve,
   and your wallet signs the publication; the site checks your claim again. Nothing is published
   without that signature, and neither the folder nor the command ever holds a key. If your
   browser will not let the page reach your computer, export the plot with `export_zip` and choose
   that file on the deploy page instead.

6. **Visit**: `https://teztopia.xyz/?plot=<your plot's cell>`. Change
   anything and deploy again; each deploy replaces the published version.

## Use it from ChatGPT or Claude

Agents that live in the cloud (ChatGPT on the web, Claude on claude.ai, Claude Desktop and the
phone apps) cannot run a file on your computer. For them the site itself is the MCP server:

```
https://teztopia.xyz/mcp
```

It has the same building tools as `third-life-mcp.mjs`, with the same checks and messages, and it
builds a **draft of your own plot** kept online for your wallet. You sign in once with your Tezos
wallet on a Teztopia page and allow the agent. **The agent can never publish**: you open its
draft in your plot editor, walk it, and publish it there, signed by your wallet. It cannot sign,
spend tez, upload files or see anything else of your account.

You need an invited wallet with a claimed artist plot.

**ChatGPT** (web; a plan whose workspace allows custom MCP servers / developer mode):

1. Open [chatgpt.com/plugins](https://chatgpt.com/plugins), press **+** and choose **Add custom
   MCP server**. (In accounts that still show the older menus: Settings › Apps & Connectors ›
   Advanced settings › turn on **Developer mode**, then **Create**.)
2. Name `Teztopia`. Under **Connection**, the public URL above, ending in `/mcp`.
3. Authentication: **OAuth**. Leave client ID and secret empty: ChatGPT registers itself.
4. Accept the warning (*I understand and want to continue*) and create it.
5. A Teztopia page opens. Sign in with your wallet, read what the agent may and may not do, and
   press **Allow**. You return to ChatGPT.
6. In a new chat, type `@` and pick Teztopia (or turn it on in the tools menu), then ask, for
   example: *"Use Teztopia. Read describe_rules, then build a small gallery on my plot with a
   courtyard and a fountain. After each round look at preview and describe_scene. When it is
   done, run validate and deploy."* ChatGPT asks you to confirm each change it makes; that is
   ChatGPT's own safety step.

This is the same for any model you pick inside ChatGPT.

**Claude** (claude.ai, Claude Desktop, mobile):

1. Settings › **Connectors** › **Add custom connector**.
2. Name `Teztopia`, URL the address above. Leave the advanced OAuth fields empty.
3. **Connect**: the Teztopia page opens; sign in with your wallet and press **Allow**.
4. In a chat, turn Teztopia on in the tools menu and ask as above.

**Claude Code**: `claude mcp add --transport http teztopia https://teztopia.xyz/mcp`,
then `/mcp` in Claude Code to sign in. **Codex** and other agents that speak remote MCP with
OAuth: add the same URL (Codex: `codex mcp add teztopia --url <the URL>`, then
`codex mcp login teztopia`). An agent that only takes a header can use an access token from
your account › **Connected agents** › *Access token for agents without sign-in*, sent as
`Authorization: Bearer <token>`.

**From draft to published plot.** When the agent calls `deploy` it gives you a link. It opens
your plot editor with the draft under **Share › AI agents**:

- **Open in the editor** puts the agent's version in your editor as one step. If your browser
  already has a draft of the plot, it first says how many objects it replaces; **Undo** brings
  them back.
- **Send my draft** gives the agent your browser's version instead, but never over a version the
  agent saved after you last opened it: the site refuses and tells you to open it first.
- **Give my draft to an agent** starts a new online draft from what is in your editor.
- Publish as always from **Share › Publish your whole plot**. Your wallet signs; no tez is spent.

What the remote server does not do: upload model files (the agent uses shapes, helpers and
prefabs; add your own models in the plot editor), hang NFTs (you do, in the plot editor), or keep
more than 4 drafts per wallet. Tool calls are limited to 60 a minute and 1,500 a day per wallet,
within the site's free plan.

**Disconnect** an agent at any time: your account (wallet button) › **Connected agents** ›
**Disconnect**. It stops at once; its drafts stay. A connection also ends by itself after 90 days.

## Connect your agent

Use absolute paths: most apps do not start the server in your project folder. In JSON on
Windows, write paths with forward slashes (`C:/Users/you/teztopia/my-world.json`) or doubled
backslashes. `node` is a program, not a script, so no `cmd /c` wrapper is needed on Windows.

**Claude Code.** As above. `--scope project` writes the setup to `.mcp.json` in the project so
it can be shared; `claude mcp list` and `/mcp` inside a session show whether it is connected.

```sh
claude mcp add --scope project teztopia -- node /Users/you/teztopia/third-life-mcp.mjs --scene /Users/you/teztopia/my-world.json
```

**Codex CLI** (OpenAI). `codex mcp add` writes the server to `~/.codex/config.toml`; `codex mcp
list` shows it, and `/mcp` inside a session shows its tools:

```sh
codex mcp add teztopia -- node /Users/you/teztopia/third-life-mcp.mjs --scene /Users/you/teztopia/my-world.json
```

The same, written into `~/.codex/config.toml` by hand:

```toml
[mcp_servers.teztopia]
command = "node"
args = ["/Users/you/teztopia/third-life-mcp.mjs", "--scene", "/Users/you/teztopia/my-world.json"]
```

**Claude Desktop.** Settings › Developer › Edit Config opens `claude_desktop_config.json`
(macOS: `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`). Add the server,
save, and restart Claude Desktop:

```json
{
  "mcpServers": {
    "teztopia": {
      "command": "node",
      "args": [
        "/Users/you/teztopia/third-life-mcp.mjs",
        "--scene",
        "/Users/you/teztopia/my-world.json"
      ]
    }
  }
}
```

**Cursor.** `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for every project).
Cursor fills in `${workspaceFolder}`:

```json
{
  "mcpServers": {
    "teztopia": {
      "command": "node",
      "args": [
        "${workspaceFolder}/third-life-mcp.mjs",
        "--scene",
        "${workspaceFolder}/my-world.json"
      ]
    }
  }
}
```

**VS Code** (Copilot agent mode). `.vscode/mcp.json` in your workspace; note the top-level key
is `servers`:

```json
{
  "servers": {
    "teztopia": {
      "type": "stdio",
      "command": "node",
      "args": [
        "${workspaceFolder}/third-life-mcp.mjs",
        "--scene",
        "${workspaceFolder}/my-world.json"
      ]
    }
  }
}
```

**Options.** `--scene <file.json>` is the scene the server edits (created on the first change).
`--assets <folder>` adds a folder to search for your GLB models when exporting (repeatable; the
scene's folder is always searched). `--offline` never downloads prefab model files.

## The tools

The same tools, with the same arguments and messages, exist inside the builder page for agents
that drive it there (WebMCP, or `window.teztopiaBuilder` from DevTools or Playwright).

| Tool             | Does                                                                                      |
| ---------------- | ----------------------------------------------------------------------------------------- |
| `describe_rules` | Units, axes, the cell, shapes, materials, collision, the visitor's size, helpers, limits. |
| `describe_scene` | Groups and their bounds, a walk test, inside wall faces for frames, free ground, mistakes. |
| `preview`        | A picture: floor plans cut like an architect's, and elevations, as PNG (or SVG).          |
| `list_objects`   | One line per object, filtered by group or shape.                                          |
| `add_room`       | Four walls with a door and windows, a floor and an optional ceiling, as one group.        |
| `add_wall`       | One straight wall, cut for a door and windows.                                            |
| `add_floor`      | A slab whose top is the walking surface, with holes for stairs.                           |
| `add_stairs`     | A flight a visitor walks up, and the hole to leave in the floor above.                    |
| `add_frames`     | A row of empty picture frames on a wall face, for NFTs.                                   |
| `list_prefabs`   | Old-town pieces: three small buildings, walls, stairs, arches, roofs, garden furniture.   |
| `add_prefab`     | Places a prefab as one group: where, at what level, turned how far.                       |
| `add_objects`    | Boxes, cylinders, spheres, tori, vaults, foliage and frames, in one batch; `repeat` makes rows. |
| `update_objects` | Moves, resizes, turns, recolours, regroups objects, in one batch.                         |
| `move_group`     | Moves or turns a whole group as one.                                                      |
| `remove_objects` | Removes objects by id, or a whole group.                                                  |
| `undo`           | Steps back one change (up to 30).                                                         |
| `new_scene`, `open_scene`, `save_scene` | Start, open (a JSON or a ZIP from the builder) and save scenes.    |
| `new_plot`       | Writes your plot's folder and edits it as the whole plot, every cell.                      |
| `add_model`      | Places your own `.glb`, checked and given collision the way the builder's import does.     |
| `validate`       | Checks the scene the way the site will (every cell of a plot), and that every model file can be found. |
| `export_zip`     | Writes the ZIP to import; for a plot, the package the deploy page takes as a file.         |
| `deploy`         | A plot only, when you ask: opens the deploy page for you to approve with your wallet.      |

Every change is saved to the scene file at once. A batch is all or nothing: if one object is
wrong, nothing is added and the reply lists every problem with what to change ("its centre x
must be between 1 and 15", "Did you mean position?").

## How an agent builds well

- **Units and axes.** Metres, y up, one 16 × 16 × 16 m cell. `position` is the **centre** of an
  object, from the cell's north-west corner, so a box of height h on the ground has y = h / 2.
  Angles are **degrees**. Visitors arrive from the south, at [8, 0, 18], looking north.
- **Rough out with helpers and prefabs**, refine with `add_objects` and `update_objects`. A
  building is usually `add_room` (or a prefab), `add_stairs`, `add_floor` with the stair's hole,
  then a roof.
- **Look after every round.** `preview` shows plans and elevations; `describe_scene` runs a walk
  test with the game's own movement and says which floors a visitor cannot reach, what rests on
  nothing, and which frames hang in front of nothing.
- **Upper storeys.** `add_room` says where the next storey starts. `add_stairs` returns a hole a
  little wider than the flight (a visitor on the stair's edge needs the headroom); leave it open
  in the slab above and rail its open edges.
- **NFT frames.** `add_room`, `add_prefab` and `describe_scene` list the solid inside faces of
  walls; pass one to `add_frames` with a count. The creator hangs the NFTs in the builder's
  **Art** tab after import.
- **Rows and groups.** `repeat: { count, offset }` turns one object into a row of posts, slats
  or columns. `move_group` moves or turns a finished building as one.

Example prompts, each built and walked with these tools before this guide was written:

> Build a two-storey stone loggia: an open arcade of three arches on the south front, a stair
> inside along the north wall, and a gallery room above with frames on its side walls.

> Build a gallery pavilion with eight frames on its walls for my NFTs, a stone forecourt and two
> cypresses by the door.

## Import into Teztopia

- **The free builder:** open `builder.html` on the site, go to the **Share** tab, choose
  **Import world** and pick the ZIP. A scene without models can also be imported as its JSON.
  The import replaces the world open in the builder, so export that one first if you want to
  keep it.
- **Your plot on the mainland:** build the whole plot as its own folder and deploy it (see
  [Your plot, like a Decentraland scene](#your-plot-like-a-decentraland-scene)). A one-cell scene
  still imports into the plot editor, as the cell you are building in.

The builder exports ZIPs too (**Share**, **Export with assets**). `open_scene` opens one, so a
world can go back and forth between the site and your agent.

## Troubleshooting

- **The agent does not see the tools.** Use absolute paths, check `node --version` is 20 or
  newer, and restart the app after editing its config. In Claude Code, `claude mcp list` and
  `/mcp` show the server's state. Claude Desktop writes logs to `~/Library/Logs/Claude/` (macOS)
  or `%APPDATA%\Claude\logs\` (Windows), one `mcp-server-teztopia.log` per server.
- **Try the server by hand.** It answers one JSON message per line on stdin, and logs only to
  stderr:

  ```sh
  echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{}}}' | node third-life-mcp.mjs --scene test.json
  ```

  It prints one line with `"serverInfo":{"name":"teztopia"…}` and exits when its input ends.
- **"could not be opened"** at start means the scene file is not valid JSON or not a scene. The
  tools say why; fix the file, or ask the agent to call `new_scene` (with `overwrite: true`) or
  `open_scene`.
- **"File not found" on export** means a model the scene declares is not in the scene's folder
  or an `--assets` folder. Files are matched by content, so a renamed file is found; an edited
  one is not: add it again with `add_model`.
- **Prefab model files offline.** With `--offline`, or without a connection, the export names the
  file to download and the folder to put it in (`third-life-prefabs` beside the scene).
- **A visitor cannot reach a floor.** Read the walk test in `describe_scene`: it says which floor
  and what to add. Then press **Test walk** in the builder.

The server speaks MCP over stdio: the `initialize` handshake of revisions 2024-11-05 to
2025-11-25, and the stateless 2026-07-28 revision (`server/discover` and per-request `_meta`).

## Packing without an agent

Write the scene JSON from any language following `world.schema.json`, then:

```sh
curl -O https://teztopia.xyz/sdk/third-life-pack.mjs
node third-life-pack.mjs my-world.json --assets ./models -o my-world.zip
node third-life-pack.mjs my-world.json --check     # validate only
```

A model's declaration (its SHA-256, triangle budget and collision surfaces) is hard to write
by hand, so a script may name the file instead and let `pack` fill it in:

```json
{ "id": "fountain", "shape": "model", "file": "models/fountain.glb", "position": [8, 0.6, 5], "scale": 1, "yaw": 0.785 }
```

`file` is read from the scene's folder, then from each `--assets` folder. `scale` (or `size`)
and `yaw` are optional; without them the model keeps its own size in metres. In the scene file,
`yaw` and `rotation` are in radians, as three.js uses them; the tools take degrees.

## The scene format

A scene is one **16 × 16 m cell**, 16 m tall. Units are **metres** and **y is up**.

```
             north (-z)
   (0, 0) +--------------+ (16, 0)
          |              |
west (-x) |   the cell   | east (+x)
          |              |
  (0, 16) +--------------+ (16, 16)
             south (+z)
                 ^  visitors arrive here, looking north
```

- `position` is the **centre** of an object's bounding box, measured from the cell's north-west
  corner. A box standing on the ground has `y = height / 2`.
- `size` is the full extent `[x, y, z]`, each above 0 and at most 16.
- `rotation` is `[x, y, z]` in radians. Solid objects turn around y only: `[0, yaw, 0]`.
- Every object stays inside the cell: x and z from 0 to 16, bottom no lower than -0.3, top
  no higher than 16.

An example:

```json
{
  "spec": "tezworlds-dev/1",
  "name": "Loggia",
  "description": "A loggia by the sea.",
  "parcelSize": 16,
  "bounds": [0, 0, 48, 48],
  "spawn": [24, 0, 34],
  "parcels": [
    {
      "id": "1,1",
      "title": "Loggia",
      "common": false,
      "nodes": [
        {
          "id": "paving",
          "shape": "box",
          "position": [8, -0.1, 8],
          "size": [16, 0.2, 16],
          "color": "#b9ab92",
          "material": "stone",
          "collider": true
        },
        {
          "id": "column-1",
          "shape": "cylinder",
          "position": [2, 1.6, 13],
          "size": [0.5, 3.2, 0.5],
          "color": "#ddcfb5",
          "material": "plaster",
          "collider": true,
          "label": "Column",
          "buildGroup": { "id": "arcade", "name": "Arcade" }
        },
        {
          "id": "frame-1",
          "shape": "box",
          "galleryFrame": true,
          "position": [8, 1.6, 4.3],
          "size": [2, 1.6, 0.08],
          "color": "#3a2c20",
          "label": "Frame"
        }
      ]
    }
  ]
}
```

- `spec`, `parcelSize`: always these values.
- `bounds`, `spawn`: the builder's frame. The world is 48 m square with the cell in the middle
  (16 to 32 in world coordinates), and the visitor arrives at `[24, 0, 34]`, two metres south
  of it. Keep these as they are.
- `parcels`: one cell, `"1,1"`, with `common: false`. Its `nodes` are the objects.
- Shapes: `box`; `cylinder` (size is diameter x, height, diameter z); `sphere`; `torus` (an
  upright ring in the x-y plane); `vault` (a barrel-vault shell: an arch over x, extruded along
  z, open underneath, for arcades and roofs); `foliage`; `model` (a GLB, see above). A box with
  `galleryFrame: true` is an empty frame for an NFT; its picture faces +z.
- `color` is `#rrggbb`. `material` is one of stone, brick, plaster, metal, glass, copper,
  plywood, painted, concrete, fabric.
- `collider: true` makes an object solid. It collides as its bounding box; a vault as its
  arch; a model as its collision surfaces. `collisionOnly: true` makes an invisible solid box.
- `buildGroup` ties objects together: the builder selects and moves a group as one. One
  group id has one name.

### Walking

The visitor is 0.6 m wide and 1.72 m tall, and walks up any ledge of at most 0.28 m.

- Doors: at least 0.8 m wide and 1.82 m tall. The helpers use 1.2 × 2.3 m.
- Stairs: risers of at most 0.28 m; the helpers use 0.18 m with 0.3 m treads. Leave the
  stairwell open in the floor above, 0.6 m wider than the flight on each open side, or visitors
  hit their heads.
- Static GLB models get automatic surface collision, which keeps doorways and interiors open.
  Animated models and very complex surfaces stay decorative: add `collisionOnly` boxes.

### Limits per scene

| Limit                              | Value                                       |
| ---------------------------------- | ------------------------------------------- |
| Objects                            | 500                                         |
| Different model files              | 8                                           |
| Model instances                    | 16                                          |
| GLB file                           | 20 MB; 48 MB of files in total              |
| Triangles                          | 200,000 per model, 500,000 per scene        |
| Textures                           | PNG, JPEG or WebP embedded in the GLB, up to 4096 px and 16.7 megapixels per model |
| Model collision surfaces           | 8,192                                       |

GLB extensions accepted: meshopt compression, mesh quantization, WebP textures, unlit,
emissive strength, specular, IOR and texture transform. Draco and KTX2 are refused; export
without them. The builder's own import goes further and reduces models to 180,000 triangles
and about 3 MB; the SDK uses your file as it is and warns when it is above those targets.
