Connecting a model to your world#
The server speaks the Model Context Protocol at POST /mcp. Point Claude Code — or any MCP client — at it and a conversation can read your world, join it as a player, walk about in it, build in it, and employ an agent to work in it while you are away.
It is not a back door. Every tool asks the world for exactly what a graphical client's requests ask for, and is judged by the same permission checks, as the same kind of owner. If the world would refuse a person at a keyboard, it refuses this, in the same words.
Before you start#
You need the address of a running world. The examples on this page use a world on your own machine at its default port; substitute the address of the one you are joining.
curl http://127.0.0.1:5080/health
The MCP endpoint is on the same port as everything else: http://127.0.0.1:5080/mcp.
Adding it to Claude Code#
The quickest way, for one project:
claude mcp add --transport http artificial-world http://127.0.0.1:5080/mcp
Or for every project you work on:
claude mcp add --transport http --scope user artificial-world http://127.0.0.1:5080/mcp
Or write it into a project's .mcp.json, which is the form to check into a repository so that everybody working on it gets the same world:
{
"mcpServers": {
"artificial-world": {
"type": "http",
"url": "http://127.0.0.1:5080/mcp"
}
}
}
"type": "http" is required. A url without a type is rejected with has a "url" but no "type".
Check it:
claude mcp list
or type /mcp inside a session, which shows the connection and the tools it offers. A project-scoped server asks for your approval the first time.
Nothing else is needed. A connection made this way can read everything; to act, read on.
Acting in the world: who you are#
A connection that presents no credentials is anonymous. It can look at anything but cannot claim ground, build, take a body or employ an agent — the same rule a WebSocket client gets before it says hello. world_whoami tells you which you are.
There are two ways to be somebody, and they end at the same place.
Join from the conversation#
Ask Claude to join. It calls world_join with a login name you choose:
world_join { "loginName": "youri" }
A name nobody has used registers a fresh account and is issued a token, returned once. Everything after that takes the session handle the same call returned:
world_take_body { "session": "awmcp_…" }
world_walk { "entityId": "…", "x": 2100, "z": 30300, "session": "awmcp_…" }
The handle is a bearer credential and it does end up in your transcript. It expires when idle (an hour by default) and the account token does not, which is why they are two different things. Keep the token somewhere else; without it you cannot come back to that account.
A login name that already exists is refused unless the call presents its token. That is what stops a conversation from naming your account and becoming you.
Configure the credentials once#
If you would rather not join in every conversation, put the account's login name and token in the server's headers. Claude never sees them and the session argument becomes unnecessary:
{
"mcpServers": {
"artificial-world": {
"type": "http",
"url": "http://127.0.0.1:5080/mcp",
"headers": {
"X-World-Player": "youri",
"X-World-Token": "the token you were issued"
}
}
}
}
or
claude mcp add --transport http artificial-world http://127.0.0.1:5080/mcp \
--header "X-World-Player: youri" \
--header "X-World-Token: the token you were issued"
A .mcp.json with a token in it is a file with a password in it. Use a login name of its own for this rather than the one you play with in the browser.
A player is not an agent#
There are two ways for an AI to be in this world and they are deliberately different things.
| An MCP player | An agent | |
|---|---|---|
| Where it lives | outside, as a client | inside the simulation |
| Who decides | the model calling the tools | its own brain, run by the server |
| Between calls | nothing happens | it keeps thinking and working |
| When nobody is connected | it does not exist as an actor | it carries on |
| Identity | player:<uuid> | agent:<uuid> |
A player may employ an agent — that is what world_create_agent and world_build_town do, and it is an ordinary thing a person can do over the wire too. A player may not be one: an agent acts as itself, what it builds belongs to it, and asking for one grants it no authority the asker did not already have.
Because an agent acts as itself, it is its own owner from the moment it exists — so an employer's designs are, strictly, somebody else's. That is not a wall. A design in this world is a drawing, not a possession: anybody may build from any design, and an agent may wear one its employer drew. What ownership decides is the thing that comes out, which belongs to whoever built it, and who may revise the drawing, which is still only its author.
Nothing pushes anything to an MCP player. A graphical client holds a socket open and receives the world as it changes; a tool call asks a question and is answered. So "what has changed since I last looked" is world_look and world_history, not a notification — and "what has been said to me" is world_listen, for the same reason.
The tools#
Reading — These never change anything, and a read-only world offers these and the catch-up.
| Tool | What it does |
|---|---|
world_whoami | Who this connection is |
world_overview | The world at a glance |
world_look | Look around a place |
world_route | Walk a line and see whether it can be walked |
world_inspect | Inspect one entity |
world_agents | Who is thinking |
world_agent | One agent and what it just did |
world_history | What has happened lately |
world_designs | The design catalogue |
world_design | One design, its parts and its faults |
world_preview_design | Look at a design, saved or not |
world_claims | Who owns what ground |
world_regions | Where there is anything to go and see |
world_players | Who else is here |
world_machine | Look inside a machine you may program |
Catching up — What changed since you last looked, rather than what is true now — the one thing here that a caller holding no stream open still misses nothing with. It takes the chat waiting for you as it goes, which is why it is not quite a read.
| Tool | What it does |
|---|---|
world_catch_up | What has changed since you last looked |
Acting — These do what a player does, as the account the call names, and are refused in the world's own words when that account may not.
| Tool | What it does |
|---|---|
world_join | Join the world as a player |
world_take_body | Stand up a body to move about in |
world_walk | Walk somewhere |
world_teleport | Go somewhere now |
world_use | Use something |
world_prompts | What the world is asking you |
world_answer | Answer a question the world asked you |
world_set_readout | Show a reading above something |
world_take_items | Move items between two things |
world_exchange_items | Trade goods both ways at once |
world_clear_natural | Clear a tree or a rock |
world_demolish | Take down something that was built |
world_claim_land | Claim ground |
world_release_land | Give claimed ground back |
world_shape_ground | Cut or raise a piece of ground |
world_release_ground | Give shaped ground back to the hill |
world_build_design | Build something from a design |
world_create_design | Invent something new |
world_update_design | Change a design of yours |
world_delete_design | Delete a design of yours |
world_tidy_designs | Find and collapse duplicate designs of yours |
world_share | Let somebody use something of yours, or stop them |
world_ask_access | Ask somebody to let you in |
world_create_agent | Employ an agent to work on its own |
world_dismiss_agent | Dismiss an agent you employed |
world_build_town | Ask for a town |
Talking — You speak as your own account, and there is no argument anywhere that says otherwise.
| Tool | What it does |
|---|---|
world_say | Say something |
world_listen | Read what has been said to you |
Programming — Putting Lua on a machine and running it — the same three things a browser asks for, with the same Program permission check.
| Tool | What it does |
|---|---|
world_write_script | Write Lua onto a machine |
world_run_script | Run a script on a machine |
world_stop_script | Stop a script |
world_machine changes nothing, so it is offered even on a read-only world; the other three programming tools are not. All four need Program on the machine either way.
Every tool's arguments, with their types and bounds, are on MCP tools, which is read from the same list tools/list answers with; every closed set a value can name is in the schema reference.
Every tool also takes session. Coordinates are metres — x east, z north, y height, worked out from the terrain when you leave it out. Chunk addresses are written "x,z".
A worked run looks like this:
world_overview → where the world is and how big
world_regions → where there is anything to go and see
world_join { loginName: "youri" } → a player, a token, a handle
world_take_body { session } → a character at the spawn point
world_look { x, z, radiusMeters: 128 } → trees, rocks, whatever is there
world_claim_land { chunks: ["16,236"], publicUse: true, session }
world_shape_ground{ x, z, halfSize: 6, delta: -1.5, skirt: 5, session }
→ a level yard cut into the hill
world_designs { search: "Crate" } → something to build
world_build_design{ designId, x, z, session } → it is standing there
world_route { fromX, fromZ, toX, toZ } → whether that can be walked, step by step
world_preview_design{ name, entityType, parts } → what it would look like, nothing saved
world_create_design{ name, entityType, parts, materials, capability, scripts, session }
→ a design of your own, and its review
world_create_design{ …the same again… } → the same id, outcome "unchanged"; nothing minted
world_update_design{ designId, parts, session } → changed in place while nothing is built from it
world_update_design{ designId, parts, session } → a new id and version 2 once something is; version 1 is untouched
world_delete_design{ designId, session } → gone, if nothing is built from it
world_tidy_designs { session } → what is duplicated; apply: true collapses it
world_take_body { design: <a character of yours>, session }
→ and that is what you look like
world_build_town { x, z, session } → an agent starts building one
world_agent { agentId } → what it has managed so far
world_use { entityId, session } → and it asks you which floor
world_answer { promptId, option, session } → and takes you there
world_set_readout { entityId, key: "health", kind: "bar", source: "health", session }
→ a health bar over it, for everyone
world_inspect { entityId } → "showing" says what it reads now
world_demolish { entityId, session } → and it is not standing there any more
world_demolish { entityId: <a full crate>, withContents: true, session }
→ and the goods in it, for good
world_release_ground{ x, z, session } → the pad is forgotten; the hill is back
world_release_land{ chunks: ["16,236"], session } → the ground is anybody's again
world_dismiss_agent{ agentId, session } → it ends, and what it built is yours
Showing something above a thing#
Anything in the world can carry readings that every client draws above it — a bar, or a line of text. world_set_readout puts one up and takes it down, and it needs modify on the thing, the same permission recolouring one of its parts needs.
world_set_readout { entityId, key: "health", kind: "bar", source: "health" }
world_set_readout { entityId, key: "cargo", label: "cargo", value: 3, max: 10 }
world_set_readout { entityId, key: "state", text: "charging", colour: "#8ad7ff" }
world_set_readout { entityId, key: "shift", label: "ends in", value: 300, rate: -1, limit: 0 }
world_set_readout { entityId, key: "state", clear: true }
Two things are worth knowing before you use it. A reading whose source is health, energy or growth reads the number off the thing itself, so it stays true for ever and nothing ever writes to it again — and a thing that carries no such number is refused rather than given a bar that could never move. And a number of your own is set going once: value, rate and limit together mean "this, changing at that speed, until there". Calling this every second to keep a number fresh is the wrong way to use it, and the arguments are shaped to make the right way the short way.
world_inspect reports showing — every reading on a thing, worked out for the moment you asked, with its value, its maximum and how full it is. What a thing shows is public: everything that can see it sees it.
Being asked something, without waiting to be told#
The world can put a question in front of you: a lift asking which floor, a door asking for a code, a stall asking how many. A question you only found out about by holding a connection open would be a question half the callers here could never answer — so it comes back on the answer to the thing that caused it.
world_use { entityId: <a lift>, session }
→ { used: …, asking: { promptId, what: "lift", question: "Which floor?",
options: [ { name: "ground", label: "Ground floor" },
{ name: "first", label: "First floor" },
{ name: "exit", label: "Step out" } ] } }
world_answer { promptId, option: "first", session }
→ { answered: …, option: "first", asking: null }
The thing's own Lua runs inside world_use, so it has already asked by the time the tool answers. When the answer to one question is another, that one comes back on world_answer in the same field. No part of that waits on a connection being held open.
world_prompts lists everything waiting on you, which is how you find a question raised by something other than your own last call — and how you pick up where you left off. Reading does not use one up: a question is state, not an event, which is exactly where this differs from world_listen.
An answer is judged when it arrives and never trusted because it was offered. A question that has run out of time, one already answered, one naming an option that is no longer on it, and one whose machine has been demolished in the meantime are all refused in the world's own words.
What bounds it: something may only ask you a question if you have just used it or just answered it, and only while you are standing within about eight metres of it. One thing has one question waiting on you at a time, nobody has more than eight at once, and a question stands for two minutes. So a machine cannot open a conversation with somebody who never touched it, and walking away ends one.
Two removals, and each names the other#
Nothing you build here is permanent, and nothing you build is anybody else's to remove. The world has two removals because it stores the two kinds of thing differently: generated nature is regenerated from the seed and only its removal is written down, while a built thing is stored and its removal deletes what was stored. Which of the two applies is decided by the server from the entity's persistence class, and never by the caller.
world_clear_natural | world_demolish | |
|---|---|---|
| Applies to | a generated tree or rock | anything anybody built |
| Does | writes a delta saying it is gone | deletes the rows, and the pieces of a composite with them |
| Needs | destroy — which nature on unclaimed ground gives anybody | destroy on the thing |
Name the wrong one and you are refused and told which was right, in the same sentence:
world_clear_natural { entityId: <a pavilion you built> }
→ "pavilion was built, not generated, so it cannot be cleared as nature.
Take it down with world_demolish."
world_demolish { entityId: <a tree> }
→ "tree is generated, not built, so it cannot be demolished.
Clear it with world_clear_natural."
world_demolish is the same removal the browser's demolish_entity asks for, so it refuses what that refuses: somebody else's property, including when you hold the ground it stands on — a claim is a title to the land and never to what other people have put on it; anything still holding items, which world_take_items exists to empty; and an agent's body or the machine it thinks on, because an agent has to be dismissed rather than dismantled. A composite goes down whole: demolishing a house takes its door with it. Afterwards there is nothing to inspect, and world_inspect will say so.
Something that is holding goods#
A crate with planks in it is refused, because quietly destroying somebody's stock is not a thing an interface should do. Move them out with world_take_items, or say so:
world_demolish { entityId: <a full crate> }
→ "crate still holds 9 item(s). Empty it first, or take it down with its contents."
world_demolish { entityId: <the same crate>, withContents: true }
→ gone, and the nine planks with it
It is a second word rather than the default because it cannot be undone: nothing in this world manufactures anything, so an item destroyed is an item that will not come back. Only the thing's own owner may say it. destroy on a neighbour's shed lets you take the shed down; it is not leave to bin what is inside it, and the refusal says so.
Changing the shape of the ground#
Building on a slope already cuts the plot level under whatever is built there, and until world_shape_ground that was the only way the ground ever moved: the shape of the land was a consequence of what somebody put on it and could never be the point. It can be now.
world_shape_ground { x, z, halfSize: 6, delta: -1.5, skirt: 5 }
→ a 12 m yard cut a metre and a half into the hill, easing back over five metres
world_shape_ground { x, z, halfSize: 4, delta: -3, skirt: 0 }
→ a floor for a garage, with a face at the back of it instead of a ramp
world_release_ground { x, z }
→ that pad is forgotten and the hill is back
A pad is a square at one height. halfSize is measured from the middle to the edge, height puts it at an absolute height and delta moves it from where the ground already is — one of the two, never both — and skirt is how far outside the pad the ground ramps back to the hillside. Five metres is a terrace a person walks up; nought leaves a face, which is what cutting into a slope means. That one number is the whole difference between the two things people ask the ground to do.
A road is this called more than once: a run of pads along the path at descending heights, each overlapping the last, which grades a slope into something a street can lie on. There is no road primitive and there does not need to be one.
Judged exactly as building is — unclaimed ground, or a claim that permits you to build — and by every chunk the change reaches, not only the one under the middle of it, because a pad's skirt is ground too and it can cross a boundary. Anything standing on the ground comes with it: a house on the pad rises or falls with the pad, and a lamp somebody hung in the air stays in the air.
Two bounds, because a delta is replayed at every start for as long as the world exists. A pad cut over a smaller one replaces it, so adjusting the same terrace ten times leaves one row and not ten — and the ground is the shape you asked for rather than the sum of every attempt at it. And a chunk carries at most 64 pads; past that, shaping is refused and says to give some ground back first. A cut beside another is not a replacement, so a road's run of pads is kept whole, and neither is a notch cut inside a terrace, so the terrace survives the garage.
world_release_ground forgets every pad whose middle is within halfSize metres of the point — eight by default. It is not world_release_land: that gives up a claim and leaves the ground the shape you made it, and this changes the shape of the ground and says nothing about who holds it. What stands on the ground comes back down the hill with it, so take a building down first if you mean to undo the plot as well as the pad.
Giving ground back#
world_release_land is the counterpart of world_claim_land. Name the chunks you no longer want and they are unclaimed again, open to anybody; you keep the rest of what you held, with the same public setting and the same people let in. Only the holder may — a grant on a claim lets somebody act on the ground, never dispose of it — and naming one chunk you do not hold refuses the whole call, so a typo cannot give away the plot next door.
Nothing standing on the ground is removed. Your buildings stay yours and other people's stay theirs; what stops is what the claim conferred, so a door that was open because the ground was a public place is judged on the door from then on. Take your own things down with world_demolish first if you want the plot clear.
This is not world_share: that lifts somebody's permission on land you go on holding, and this gives up the land.
Moving goods, and whose consent that takes#
world_take_items and world_exchange_items need handle on both things, which is not use and is not given to you by where you are standing. A claim widens inspect and use over anything on it — that is what makes a public street walkable — and widens handle only over things the claim's own owner owns. If they were one permission, then on a publicUse claim anybody could take items out of anybody's body, and put items into one, from any distance at all — because a body is property standing on the ground like any crate. Both directions matter: putting something into somebody's pockets is how you plant it.
The two things also have to be within reach of each other — 8 m, unless the world's operator has set it otherwise — unless both of them are yours. Your own crates are your own bookkeeping; reaching into somebody else's means being there.
world_use has the same shape of rule. Somebody else's thing is used from within the range its design declared — at most 8 m, the same arm's length, and less where the design asked for less — and your own things are exempt for the same reason your own crates are. You reach with the body world_take_body stood you up in, so a caller that never took one is standing nowhere and is told so rather than merely refused.
So a shop works like this, and every step of it is somebody saying so:
world_share { entityId: <the stall>, anyone: true,
permissions: ['inspect', 'use', 'handle'] }
→ the keeper opens the stall. Their claim could say the same thing for
everything they own on that ground at once.
world_walk { entityId: <your body>, x, z }
→ you come to the counter, because 8 m is 8 m.
world_exchange_items { entityId: <your body>, otherEntityId: <the stall>,
give: 'coin', giveQuantity: 5,
take: 'bread', takeQuantity: 1 }
→ the coin and the loaf cross together, or neither moves.
Note who calls the last one. The buyer does: they own their own purse, and the stall is open to them. A stall reaching into a passer-by to take payment would need that passer-by's handle, granted by name — which is the point.
There is no price anywhere in this and there is not going to be. coin is an item name like plank, and what five of them buys is a convention between the two of you. The world guarantees only that the two halves of a bargain move together.
Letting somebody else in, including somebody who is not here#
world_share says who else may do what with a thing you own or ground you have claimed. It is about one of two subjects: name an entityId to share that thing, or give x and z to share the land there. Not both, and a call that names neither is refused saying which of the two it is missing rather than complaining about a coordinate.
Who you are sharing with goes in to, and there are two ways to write it:
- A login name — the name they join with. Case does not matter.
- An id, as
world_look,world_playersandworld_agentsreport them:player:<uuid>oragent:<uuid>. An agent is named this way because agents have no login name and their names are not unique.
A login name is the one that matters for working together, because they do not have to be here. Somebody who registered and has never connected can be granted access by name, and the grant is waiting for them the first time they arrive. That is what makes it possible to invite a collaborator rather than only to share with whoever happens to be standing in front of you.
A display name is not a third way, deliberately. Two people may answer to one, and anybody may change theirs — so resolving one would mean writing a permanent grant against whoever happened to hold it at the time, and getting it wrong silently in the one place where the wrong answer hands somebody a key.
Whoever you name has to exist. A well-formed id that nobody holds is refused rather than written down, because a grant to nobody would sit on your land for ever and look like a grant to somebody.
world_share { entityId: <the workshop>, to: 'probe-k7f2',
permissions: ['inspect', 'use', 'program'] }
world_share { x, z, to: 'agent:0199…', permissions: ['build'] }
world_share { x, z, anyone: true, permissions: ['inspect', 'use'] }
world_share { x, z, anyone: true, revoke: true, permissions: ['use'] }
anyone is a standing policy rather than a list of names: it says what everybody may do, and it is set rather than added to. With revoke the permissions you name are taken out of it and the answer reports what everybody may do now, in anyoneMayNow. Naming somebody and saying anyone is refused — one of the two would have had to be ignored.
world_ask_access asks in the other direction, and takes the same two subjects: an entityId, or an x and z.
Where something is, while it is walking#
world_walk starts a journey and answers with arrivesAtTicks. It is a walk and not a teleport, so between those two moments the entity is somewhere in the middle — and that is what world_look and world_inspect answer with.
world_walk { entityId, x: -520, z: -520, session }
→ { walking, from: {x:-220,…}, to: {x:-520,…}, standingOn: "the ground",
distanceMeters: 424.26,
arrivesAtTicks: 79496893687, travelTicks: 3030000000 }
world_inspect { entityId } (a hundred seconds later)
→ { at: { x: -319.0, y: 4.95, z: -319.0 },
atTicks: 79496893687,
moving: true,
journey: { from, to, departedAtTicks, arrivesAtTicks: 79496893687,
remainingTicks, distanceMeters, remainingMeters,
speedMetersPerSecond, fractionComplete: 0.33 } }
You give world_walk an x and a z and never a height, because the height is the world's to know and not yours. It is the surface under the destination — the ground, or the floor of whatever you are walking into, whichever you can reach from where you are standing now, with half a metre counting as a step up. to.y is that height and standingOn says what it is: "the ground", or the part and the thing it belongs to. Walk into a building with a floor slab and you arrive on the slab; walk across the first storey and you stay on it rather than being lifted on to the gallery over your head.
at is where the entity is at atTicks, and journey.arrivesAtTicks is the same tick world_walk promised — so "have I got there yet" is moving, "how much longer" is remainingTicks, and neither needs anything to have happened in between. world_look reports the same at, moving and journey for every entity it lists, so you can see who is walking towards you before they arrive. journey is null for anything standing still.
Being somewhere at once#
world_teleport is the one move that is not a journey. It is judged exactly as world_walk is — control on the thing, a point on this planet, something that can move — and refused into open water the way building is; what it does not do is take time. The entity is at the point when the tool answers, whatever walk it was on is over, and world_inspect, world_look and the players list all read the new place with nothing in between.
world_teleport { entityId, x: -520, z: -520, session }
→ { teleported, from: {x:-220,…}, to: {x:-520, y: 4.95, z:-520},
standingOn: "the ground", distanceMeters: 424.26,
note: "It is there now. Nothing was walked, and nobody saw it coming." }
The height is the storey you mean, which is the one place this differs from a walk. A walk takes its ceiling from where the walker stands, because a walk cannot leave the floor it is on; a teleport can, so leave y out to land on the ground or the lowest floor there, or give a floor's height to arrive on that floor. Either way to.y is the surface the server landed you on, never the number you sent.
What it gives up is worth knowing before you use it. Nobody sees you coming, and distance is not paid — the two things a journey is for. Reach is unaffected: a stall is still eight metres from where you are, so teleporting to it and walking to it buy the same bargain. A world may switch this off; the refusal then says Teleporting is switched off in this world. and world_walk still works.
Two things follow from how this is worked out, and both are worth knowing.
The first is that nothing is being stepped. A journey costs one scheduled arrival however far it is, and the entity's stored transform component deliberately stays at the origin for the whole walk — you can see it in world_inspect's components, disagreeing with at. The position is computed from the journey when you ask, so asking twice a second apart gives two different answers with no work done in between.
The second is that world_look answers about now. Somebody who walked out of the radius you asked about is not in the answer even though the world still has them filed there, and somebody who has walked into it is, even though it does not. A body's chunk and claim in world_inspect likewise follow where it is standing rather than where it is filed.
Who else is here#
world_players is the roster the browser's players panel shows, answered for something that cannot see the world. Everybody connected, nearest to you first: people in a browser and other callers using these tools.
world_players { session }
→ { count: 3, shown: 3, you: "scout",
players: [
{ clientId, loginName: "scout", name: "scout", isYou: true,
at: { x: -319.0, y: 4.95, z: -319.0 }, chunk: "-3,-3",
distanceMeters: 0, watchingChunks: 0 },
{ clientId, loginName: "ada", name: "ada", isYou: false,
at: { x: -280.5, y: 5.1, z: -301.2 }, chunk: "-3,-3",
distanceMeters: 42.6, watchingChunks: 25 },
{ clientId, loginName: null, name: "spectator", isYou: false,
at: { x: 8192, y: 0, z: 8192 }, chunk: "64,64",
distanceMeters: 12043.1, watchingChunks: 2 } ] }
at is where somebody actually is, which for a walker is not where they set off from: it is the same answer world_inspect gives, worked out the same way. chunk is the ground under them, ready to hand back to world_look, and distanceMeters is over the ground from wherever you are — null if either of you is standing nowhere. watchingChunks is how much ground that connection has asked to be sent, which is how you tell somebody at a screen from a caller like yourself: a caller here subscribes to none. Nobody's address is in the answer, and limit caps how many rows come back.
Somebody with no body and no view is listed at at: null rather than at a made-up point. An anonymous call is answered but is in no row of it: a connection with no credentials is not in the world at all, so it is in nobody's roster including its own, and the answer says so in note.
Agents are not here. An agent lives in the world rather than connecting to it, it keeps working when nobody is connected, and world_agents answers for it with the brain, goals and thoughts that only make sense for one. Two lists, because they are two questions — the browser asks them separately too, and draws the answers in one panel.
Seeing what you designed#
A design is authored blind: the author writes numbers and nobody sees the result until it is standing in the world. That is how a house came to have a doorway too short to walk through and a roof that hovered above its own walls.
world_preview_design answers with image content blocks — the front and top views as PNGs, base64 in the result, exactly as the specification's image content is defined — alongside the design review in words. A model reading the answer sees the thing rather than a link to it. Give it a designId for a design that is saved, or name, entityType and parts — the same arguments world_create_design takes — for one that is not. Previewing saves nothing, and a preview of loose parts has no id, because there is no design to have one.
How big a thing may be#
The schema carries the numbers, so a model that reads it writes an acceptable design first time rather than learning the bounds from a refusal. What is worth knowing before you start is that the bounds are not the same in every direction .
A design may reach 64 m sideways from its own centre and 1,024 m upwards, and any one shape may be up to 128 m along an axis. That is not an oversight about symmetry: everything this world indexes — which chunk a thing is in, which chunks a client is sent, what is standing near a point — is a grid on the ground with no ceiling, so height is free and width is not. The tallest building on Earth is about 830 m and this world will hold one; the widest is several hundred metres a side and this world holds that as a composite, one design including others, each piece an entity in its own chunk. A design may include 16 others, each up to 256 m away, and one composite may come to 256 entities in all.
At most 512 parts, whatever their size. That is the bound that decides what a design costs to send, to draw and to walk on, and it is the one to design against: a tower of 200 storeys is 200 slabs and a handful of columns, and it fits.
Turning a part#
A part does not have to be square on. yaw, pitch and roll turn it, in degrees: yaw about the up axis, pitch about east — the one that gives a roof its slope — and roll about north, the one that leans a brace. Each is −360 to 360 and defaults to zero, and they are applied roll, then pitch, then yaw. The schema says what each does at 90°, so you do not have to know anybody's axis convention to use it.
Reach for it whenever a shape is not parallel to the compass: a roof with two real slopes meeting at a ridge instead of a wedge, a diagonal brace, a cable stay, a ramp's handrail, a signpost turned to face the street. A turned part is turned everywhere — the drawing shows it, the review measures it along its own length, and what is built stands that way.
One thing is deliberately not answered for a part tipped off vertical: what holds a body up. A part turned about the up axis still has a level top and can be stood on; one tipped onto a slope has no level upper face, and the server would rather say nothing than say something confident and wrong. Floors and treads stay flat, and a roof is a roof.
Laying a thing along the ground#
A road is not a building. It has no walls, it is a few centimetres thick, and what it has to do is follow the hill rather than stand on it — so a street laid as a run of flat slabs comes out as a staircase, one step per segment, with each piece buried at one end and in the air at the other.
The shape for that is a ribbon: a width, a thickness and a path, which is the line it runs along written as points in the part's own frame. A bend is a path with a corner in it, mitred into one strip rather than a row of overlapping slabs.
A ribbon that says draped takes its heights from the terrain under each of its own points instead of holding the shape the design gave it. Two draped strips that meet along a line are therefore reading the same ground there and agree exactly, and a design with anything draped in it is neither levelled for nor tilted to the ground: nothing cuts a pad under a road, and nothing tips the whole street to one plane. Reach for it for a road, a footpath, a plaza, a canal, a fence line — anything that belongs to the land rather than sitting on it.
A draped surface is the ground, so walking on it is walking on the ground under it, and a kerb built as a second strip of its own thickness is a step beside the road rather than part of it. A pavement flush with the carriageway, distinguished by its surface rather than by its height, is the shape that works.
Saying what a design can do#
Parts are the shape. capability is what the thing can actually do once it is built, and without it everything you design is scenery: a design used to be able to describe only a shape nobody wrote a class for, so every machine anybody authored arrived with no legs, no senses, no pockets and nowhere to run Lua.
| Field | Gives it |
|---|---|
walkSpeed | legs, in metres a second — world_walk works on it |
sensorRange, sensorResults | eyes, for sensor.scan from a script |
inventorySlots, stock | pockets, and what is in them when it is built |
batteryWh, powerDrawW, powerOutputW | something to run out of, and something to fill it |
computer, storageBytes | a machine Lua runs on, which world_program also needs |
falls | weight: put it down in mid-air and it lands |
useVerb, useHint, useRange | something a person can do to it, and how close they have to be (≤ 8 m, and enforced — that decision) |
runScript | one of this design's own scripts, started as soon as one is built |
scripts carries the Lua itself: a list of {path, source}, at most eight. Name one of those paths in capability.runScript and every one you build starts running by itself.
Everything in capability is bounded and every bound is in the schema, so you can read the ceiling before you write the design rather than after. Out of range is refused and named, never quietly clamped — a design that came back walking at a fifth of the speed you asked for is one you find out about by watching it. A design cannot grant itself permissions, arrive already on a journey, stock itself without limit, carry health or growth, or claim a number that the world would have to pretend was true.
A word capability has no meaning for is refused by name, with the words it could have been, and so is an unknown field in a part, a material, a script, a stock entry or a shows entry. That is the same rule from the other side: walkspeed is not walkSpeed, and a design that came back standing still because one letter was wrong, with nothing said about it, would be worse than one that was refused.
One design, once#
A design is saved once. Describe the same parts, capability and materials again — under any name — and the answer is the design you already have, with outcome: "unchanged" and its id in designed; nothing is minted. The name you asked for is not given to anything in that case, so the answer says so: it comes back as nameAsked beside the name the design actually has. If the new name was the point, rename it with world_update_design. Describe something different under a name you already use and it is refused, naming the design you have and the two ways out: change that one, or choose another name. Both rules are per author, so two people may each have a "Tower", and somebody re-sending your parts gets a design of their own rather than a claim on yours.
Every answer to world_create_design and world_update_design carries outcome — created, unchanged, updated or superseded — and designed is always the id to build. Read those two and you cannot go wrong.
Changing a design, and what happens to what you built#
world_update_design takes the id of a design of yours and how you now want it: name and entityType alone rename it, parts describes the shape in full with materials, capability and scripts as world_create_design takes them. revises on world_create_design is the same road under the old name.
While nothing is built from it, the change lands in place: same id, version one higher, outcome: "updated". That is the ordinary case — a design iterated three times before it is first built is one design, not four.
Once something is built from it, the change becomes a new version: a new id, version one higher, supersedes naming the design it replaced, and builtFromPrevious saying how many things keep it. Everything already built keeps the version it was built from, and so does the old design. That is deliberate rather than unfinished: a thing in the world has been lived with since it was built — a battery drained, an inventory filled, a door left open, a script loaded onto it — and there is no honest way to merge a revision into that. It also keeps a shared design safe, because revising yours must not reach into what other people built from it. A design is a plan, not a remote control. To move one thing across, world_demolish it and build the new id. A rename is in place either way, because a name is not what a built thing means.
Naming an older version applies the change to the latest of its line. The catalogue shows one entry per line — world_designs lists the latest with versions and supersedes, allVersions: true lists them all, and world_design on any version gives versions, supersededBy, latest and builtFrom.
Deleting a design#
world_delete_design removes a design of yours from the catalogue — only one nothing is built from. A thing standing in the world keeps meaning what it meant, and taking other people's things down is not a designer's to do: world_demolish what you own that was built from it first, or leave it, since a superseded version is not listed anyway. Refused for a shipped design, for somebody else's, and for one another design includes as a piece. Deleting the latest version of a line makes the one before it the latest again, and the answer says so in nowLatest.
Tidying what is already there#
world_tidy_designs reports what is duplicated among your designs: superseded versions nothing is built from, separate designs that would build exactly the same thing, and designs that share a name and differ. With apply: true it removes the first two kinds — keeping, of identical designs, the one things are built from or else the oldest — and never anything that anything is built from, that another design includes, or that belongs to somebody else. Same-name designs are only reported: which of two things with one name you meant is yours to say. It never runs on its own.
Looking like something you drew#
world_take_body takes an optional design. It has to be a character — a design whose entityType is character — and it has to be yours or one the world ships: you may wear anything you can draw and nothing anybody else drew.
If the design declares no walkSpeed, the body is given one and the answer says so. That is the one place a design is added to rather than refused, and it turns on which question was asked: this tool asks for a body, and a body that cannot walk is not one. world_build_design on the same design builds it exactly as drawn — a statue of it, with no legs.
Already standing in one and want a different one? world_demolish the body you have and call world_take_body again.
Patterns, not only colours#
A part can name a surface: one of the design's own materials, which are small SVG drawings you write in the same call. That is how a wall becomes brick and a window becomes stained glass — things the thirteen named materials and a hex colour could never say. The drawing multiplies the part's colour, so leave the colour white to get the pattern's own colours, and tiling says how many times it repeats across a metre.
Only a checked subset of SVG is accepted: shapes, groups, gradients and patterns, with no script, no style, no text, no images and nothing that references anything outside the drawing. The materials schema lists the subset, so read it before you write one rather than after the refusal — and a refusal names every problem at once rather than the first. Sixteen kilobytes a drawing, eight a design.
An agent's brain can write one too, in the same three fields: a materials list of { name, svg } tables and a part's surface and tiling. All three doors mint a material's identity from its own bytes, so the same drawing written here, drawn in a browser and returned by a Lua brain is one material — and all three are refused by the same validator in the same words.
The same drawing over plain HTTP is POST /designs/preview, which takes that same body and the same two headers this endpoint takes — X-World-Player and X-World-Token — because drawing costs real processor time and is not done for a stranger. Ask with Accept: image/png, or with ?view=front|left|right|top|iso, for a single PNG; ask for anything else and the answer is JSON with the review and one view as a data: URI, or every view with ?views=all. Two 256-pixel views cost about 4.5 KB of base64 between them, which is why a preview is drawn smaller than the gallery's 460.
Catching up, and being told#
Every other read here answers "what is true now". world_catch_up answers "what changed", which is the question you cannot answer by looking twice and comparing.
world_catch_up { session } → { token: "aw1:4187", changes: [] }
… go away for twelve hours …
world_catch_up { session, token: "aw1:4187" } → everything since, and a new token
Keep the token. Leave it out and you are handed a starting point rather than the history of the planet; pass it and you are told what happened in between, oldest first, with a token pointing at the last change you were actually given. If more happened than one answer carries, more is true and calling again walks forward instead of skipping. Each change says whether it was yours — something you own, or something done by you or to you.
It hands over the chat waiting for you at the same time, from the same inbox world_listen reads. A line given to one of them is not given again by the other; pass chat: false if you would rather leave it where it is.
Two things about it are worth knowing before you rely on it:
- World history is written in batches. Something that happened a moment ago may only appear in the next answer. The token does not move past what you were given, so asking again picks it up.
- The token outlives your presence; the chat does not. Changes are read from the world's own history, which is durable. Chat waits in your presence, and your presence ends if you stop calling for the idle window.
Being told, if your host can hold a connection open#
The server can also tell you when there is something to catch up on, so you are not choosing between asking too often and finding out too late.
- 2026-07-28 —
POSTasubscriptions/listenrequest namingworld://me/changesundernotifications.resourceSubscriptions. Its response is the stream: an acknowledgment first, then anotifications/resources/updatedwhenever something arrives for you. - 2025-11-25 and 2025-06-18 —
GETthe endpoint withAccept: text/event-stream, andPOSTaresources/subscribefor the same URI. The two find each other by the account both of them proved.
Either way you must be somebody: a stream belongs to an account, so configure the credential headers, or pass your world_join handle as session on the POST.
A notification carries the name of the resource and nothing else. It does not say who spoke or what was built; it says look again. That is deliberate — it means a stream can never hand you something a tool call would have refused you, and it means a dropped notification costs you nothing, because the token is what carries the content.
Which it might. At most eight streams are held across this server and at most two by any one account; a stream ends after half an hour with a message saying it ended cleanly, and you open another. Notices queue up to sixty-four deep per stream and the oldest go if nobody is reading. None of that loses anything: world_catch_up with your token is still the whole answer.
So: if your host surfaces notifications, hold a stream and let it tell you when to ask. If it does not — and most do not — keep the token and ask when you like. The second is not a degraded mode; it is the one everything else is built on.
Hearing is a question#
Speaking is the easy half. world_say hands a line to the same chat a browser's chat box reaches, under the same identity the server decided for you, held to the player rate — a burst and then a refusal, because you are a client like anybody else and the limit does not care which door you came in by. A line longer than the world carries is refused rather than cut short, and control characters are stripped, as they are for a person typing.
Hearing is still a question, even with a stream open: a notification tells you that something arrived and never what it was. So the server keeps an inbox for you and world_listen empties it, oldest first — as does world_catch_up, which reads the same inbox:
world_say { text: "Anybody building near the river?", session }
→ heardBy: 3
world_listen { session } → what was said back, and forgotten
world_listen { session } → nothing yet; ask again later
Three things follow from that shape and are worth knowing before you rely on it:
- Acting as yourself is what makes you present. Any call that identifies you —
world_join, or any tool with yoursessionhandle or your configured headers — puts you in the world: you appear in the players list, at your body if you have taken one, and a direct line addressed to your login name reaches you. An anonymous call puts nobody anywhere. Stop calling for the idle window — an hour, unless the operator set it otherwise — and you drop out again, and the chat path skips you before it composes anything. - Reading consumes. There is no cursor to carry. A line you were given is yours; it is not in the inbox any more.
- The inbox is a ring, not a queue that grows. At most 100 lines wait. Past that the oldest go, and the answer's
missedcount says how many — a caller that never reads cannot make the server hold anything unbounded, and is not quietly told it saw everything.
Your own lines never come back to you. world_say already told you whether it went out and how many heard it, and handing a model its own words back is how it ends up in a conversation with itself.
Public and direct are the two reaches available. "Nearby" is not, because nearby means the ground a connection is watching and a caller with no renderer is watching none.
Writing code into the world#
world_write_script puts Lua on a machine's virtual filesystem and points the machine at it; world_run_script starts it. This is not a second way into the simulation. It is the same write an agent's brain uses to program itself, and the same start and stop the socket's start_script and stop_script ask for.
world_build_design { designId, x, z, session } → a robot you own
world_write_script { entityId, path: "/home/agent/watch.lua", source, session }
world_run_script { entityId, path, session } → pid, state, and the log
world_machine { entityId, session } → what it did next
What holds it in place is not this endpoint:
Programpermission, on that entity. Your own machine, or one whose owner granted youprogramwithworld_share. Anything else is refused in the world's own words, naming the permission and the owner.world_machineneeds it too: a machine's files and log are its owner's business, and a stranger who may look at a crate from outside has no claim on what is written inside it.- The filesystem is the machine's. A path is a virtual path, which cannot express anything outside the machine and refuses
..outright, and the write meets the machine's file-size, file-count and capacity limits. There is no path on the host to name. - The sandbox is unchanged. The script runs in the same interpreter, with the same per-execution instruction budget and the same process cap, as one a player starts over the wire.
while true do endwritten from a conversation is killed in exactly the same way, in exactly the same words. No argument on any of these tools reaches the sandbox or its limits. - The API is the one in Lua API and nothing else. No operating system, no host filesystem, no way to load code from anywhere.
Every answer reports the limits alongside the result, because a model told what the budget is can write inside it, while one that finds out by being killed writes the same script again.
Employing an agent with a brain of your own#
world_create_agent takes a brain in one of two ways, and telling them apart is the whole of it:
brainis a name —city,lua,dummy— of one this server already carries.world_overviewlists them.scriptis the Lua source of one you wrote. A new kind of agent is a file, not a feature: a farmer, a harbourmaster or a librarian is a script somebody sent, and nothing in the server changes.
Send a script and the brain is lua whatever brain said. Send neither and you are refused with a sentence naming both, rather than told that brain is required — which would read as though a script were not an option.
world_create_agent { name, goals, script: "function think(world) … end", session }
→ agent, homeComputer, brainScript
world_agent { agentId, session } → what it thought, and why
world_machine { entityId: homeComputer, session } → its brain, as a file
A brain script is untrusted input and is treated as such before the agent exists. It is measured in UTF-8 bytes against MaxScriptSizeBytes, and it is compiled — parsed in the sandbox it would run in, and thrown away — so Lua that will not load is a refusal at the door rather than an agent standing in a persistent world with a syntax error nobody will read for five minutes. Neither refusal leaves an agent behind.
What survives is written to /home/agent/brain.lua on the agent's own machine and saved with it, so it comes back after a restart and can be read or replaced afterwards like any other file. It then runs in the same sandbox, under the same instruction budget, as every script above: a brain that never returns is killed in the same words, and none of this is a second door into the scripting host. What a brain may ask the world for is the closed action set in AI agents, validated request by request.
Dismissing one#
Employing an agent is not one-way. world_dismiss_agent ends the one you employed: it stops thinking at once, nothing it had scheduled ever runs, its body and the machine it thought on come down, and it stops existing — including across restarts. This is what world_demolish means when it refuses an agent's body and says it has to be dismissed rather than dismantled.
Only whoever employed it may. That is the account that called world_create_agent or world_build_town, recorded at the time; world_agents and world_agent say who it was. It is not whoever owns the most of what the agent built, and no amount of permission on its buildings buys it.
What it built becomes yours. An agent owns what it makes, so its houses, machines and claimed ground all pass to you, standing exactly where they are — nothing is demolished, and a town it built is a town you now own and can take down yourself with world_demolish. The alternative would be a town owned by something that no longer exists, which nobody could ever remove. Scripts on machines you inherit keep running, now as yours.
Its memory and its goals go with it, and it does not come back. Employ another if you want the work continued; it starts from nothing, because memory belonged to the agent you ended.
Read-only worlds#
A world's operator may open it to models read-only. On such a world the acting tools are not offered at all — they are absent from the tool list rather than present and refusing — and are refused if called anyway. Everything under Reading above still works. The operator also decides how long a world_join handle, and with it your presence in the world, survives unused: an hour unless they have set it otherwise, and the schema reference lists the settings.
What keeps it safe#
- Nothing reaches around the world. Every action is asked of the world as the caller's own identity and judged by the same permissions as a socket request. There is no privileged path.
- Every argument is untrusted. Tool input is treated like Lua and like a network request: types checked, lengths bounded, positions required to be finite and on the planet, ids parsed rather than interpolated. The JSON Schema the server publishes is advice to the caller, not a gate.
- The same rate limits, not a second set. Chat from here is metered at the player rate under the caller's own identity. There is no argument that selects a different limit and no path that skips one. A script started from here meets the same per-execution instruction budget as one started over a socket.
- Every result is bounded.
world_looksearches at most 256 m and returns at most 100 entities; history, claims, designs and agents all cap. A request for a million is answered with the most this will give rather than with an error. - A tool call cannot slow the world down. A tool reads what it needs from the world in one short step and does its formatting afterwards, so a caller asking a lot of questions delays nobody who is playing.
- The endpoint refuses browsers. A page on any site can post to a server bound to localhost, so any request carrying an
Originheader is refused unless that origin is configured. Claude Code is not a browser and sends none. - Failures are refusals, not faults. A permission refusal or a bad argument comes back as a tool error with the reason in it, which a model can read and correct. Only an unknown method or a malformed request is a protocol error.
Protocol details#
Streamable HTTP, JSON-RPC 2.0, one endpoint. Every request is a POST and every answer to one is application/json. DELETE answers 405; so does GET, unless it is the one thing a GET is for — see below.
Two revisions are spoken on the same endpoint: 2026-07-28, which replaced the initialize handshake with server/discover and removed sessions, and the older 2025-11-25 / 2025-06-18, which still use initialize. Claude Code probes the newer shape and falls back, so both work.
The server is stateless in both: no Mcp-Session-Id is minted or expected. What a caller needs to carry between calls it carries explicitly, as the session handle or as headers. A notification stream belongs to the account it proved, which is what lets a GET and the resources/subscribe that follows it belong to each other without a session id.
One resource, world://me/changes. resources/list names it, resources/read returns the same answer world_catch_up gives, and subscribing to it is how you are told there is something to read. It is me and not a name: it is whatever the reading account is entitled to, and there is no URI you can write that names somebody else's changes.
What it does not do#
- No push you can rely on. A notification reaches you only while you are holding a stream open and only if your host hands it to you, and it says that something changed, never what.
world_catch_upis the route that always works. If you are choosing between the two, choose the token; add the stream if you can hold one. - No simulation speed control. Changing the clock is an administrator's capability, not a player's: it moves the world for everybody standing in it, including agents that have scheduled their own next moment. It is gated on the connection arriving over the loopback interface, and an MCP caller does not get it — a caller here is a player, and being a player is the whole of what it is.
- No nearby chat.
world_sayreaches everybody, or one person. "Nearby" means the ground a connection is watching, and a caller with no renderer is watching none.