Releases: wfbcargo/wfbcargo_spawn_mcp
Release list
v2.0.0
Spawn 6 support. Spawn 6 is not a new version of the thing this server talked to — it is a
different write path, and 1.x could not reach it at all. A 6.0 world's code is a git repository, and
the whole-document PUT /game-specs that every 1.x push used answers 409 world_is_git against one
before it reads the body. Every spawn_push aimed at a 6.0 world failed, and nothing it reported
was reaching the tree.
2.0 detects which engine a world is on and routes itself. The tool names did not change, so an
agent prompt written for 1.x keeps working on both — but the lanes underneath are genuinely
different, and the breaking changes below are the places where that shows.
It also gains a body. A room boots for a player and never for a door, so spawn_exec,
spawn_logs and spawn_rooms read nothing until someone is standing in the world. In 1.x the only
way to arrange that was spawn_play_open — a headed Chromium with a working WebGPU adapter.
spawn_client_join puts the agent's own body in the room instead: no browser, no GPU, a few
seconds.
Worlds on a pre-6.0 engine are unaffected. Every document-lane behaviour in this list is
unchanged from 1.8.0.
Breaking changes
Only on engine 6.0+ worlds; the document lane behaves exactly as it did.
spawn_initclones instead of scaffolding. There is no spec document to scaffold from — the
tree arrives whole or not at all. A project directory that is not empty and is not already this
world's clone is refused rather than cloned over, where 1.x would have written agame.json
that could never be pushed.spawn_pushrequiresmessage. The commit's first line lands in the creator's chat under
your name, so it is a required argument on this lane, not an optional label. Calls without one
fail.spawn_pushrefusesforce. It meant "whole-replace over the base-version rail", and there
is no safe equivalent: on a git world it would mean a force-push, and the server never
force-pushes or merges.spawn_latestrefusesmode,version,updateSlugandapplyLocal. They are document
concepts. Silently ignoring "give me the published live snapshot" and handing back the dev head
would look like it worked, so it is an error instead. With no arguments it isgit pull --rebase.spawn_validateis a local pre-flight, not an authority. This lane has no server-side
validator — the push itself is the validator. "Clean" now means only that the failures visible
without the engine are absent.spawn_statusdrops the version rail.baseVersion,hasSpecRail,conflictReceiptsand
hasGameJsonare absent on a 6.0 world; branch, HEAD, ahead/behind and uncommitted files take
their place. Anything parsing those keys must handle their absence.- This server's own files moved out of the world tree. A 6.0 world tracks
.spawn/— its
engine pin, its skills index, its per-cell cost files — so the docs and caches written there in
1.x now live under.git/spawn-mcp/. If you read.spawn/guide.mdor.spawn/tome-api.mdby
path, read.git/spawn-mcp/on a 6.0 clone (spawn_initreportsdocsDir, and a clone also
carries its ownAGENTS.mdat the root). Screenshots moved with them. - Lane-sensitive tools now make an engine-detection call (cached per process, and persisted).
A world whose engine cannot be read from the API is now an error where 1.x would have gone ahead
and pushed. PassengineVersionexplicitly to proceed without detection. - New optional dependency: Bun, required by the four
spawn_client_*tools
and nothing else. Everything that worked in 1.8.0 still works without it.
Added
-
Both write lanes, detected per world. Every tool that reads or writes a world routes itself:
tool pre-6.0 (document lane) 6.0+ (git lane) spawn_initscaffold game.json/world//scripts/, pull the specclone the repo into projectDirspawn_pushcompile + PUT /game-specsstage, commit, git push, report the rooms' verdictsspawn_latestpull a saved spec, sync scripts, .theirsreceiptsgit pull --rebasespawn_validateserver-side schema validation local tree pre-flight spawn_statusbase version, receipts, head vs published branch, HEAD, ahead/behind, uncommitted spawn_docsguide + tome API + skills same, and the world's era + semver spawn_exec/spawn_logs/spawn_roomsunchanged unchanged — identical on both lanes -
engineVersion, an optional argument on every lane-sensitive tool. Omitted, the world's
engine is read from the API and cached. Passed — as a semver (6.0.0,5.4) or an era name
(6.0,document) — it is checked against the real pin, and a disagreement fails the call
without writing anything. It is an assertion, not an override, because the failure it exists to
prevent is a document-lane push aimed at a git world, or agame.jsonscaffolded over a live
clone.spawn_exec/spawn_logs/spawn_roomsdeliberately do not take it: those endpoints
are identical on both lanes, so the parameter would be a knob that does nothing.The API answers the engine question in three places now, and all three are used:
GET /api/agent/v1/meand/worldscarryengine: { semver, era, git }per world, and
GET /api/sdk/v1/{id}/agent/docscarriesengineVersion+era. Detection readsworldsfirst
(a few hundred bytes) and falls back todocs, which is authoritative for any variant the token
can reach. A409 world_is_gitfrom the document lane drops the cached era, so a world migrated
to 6.0 mid-session is picked up on the next call rather than retried into the wall. -
spawn_client_*— the agent's own body in the world.tool what it does spawn_client_joinstands your body in the world as a real player — boots the room, no browser, no GPU spawn_client_statuswhich sessions are standing, and how much ttl is left spawn_client_leavedespawns the body; the room folds when the last one goes spawn_clientany other client verb — where,players,inputs,move,look,witness,crossing,screenshot,runThis is the cheap way to a live room, and it is also how an agent plays the game it is
building: the body wears your name, stands in the room's census beside the creator and Savi, and
the world's own player hooks fire for it like anyone's.spawn_execandspawn_logsnow name the
join in their no-live-room errors instead of sending you to open a browser.A body and a browser answer different questions, and the build loop now says so in that order: a
body makes the room readable and lets you act in it; the browser is the only thing that tells you
whether the frame is any good. Join to query, look to judge.Four tools rather than one per verb, on purpose: the client is served by the stack rather than
shipped here ("the client it runs is the one the stack you join serves"), so its verb list can
change under us. Join, leave and status carry real schemas because they are the lifecycle an agent
has to get right; everything else goes through one passthrough that cannot drift. -
spawn_pushtakes amessageand abodyon the git lane. The commit's first line is not a
log entry: it lands in the creator's chat and their changes list under the agent's name, beside
what they and Savi said. The tool asks for one plain sentence about what changed for the player.
bodycarries the how — and since no agent account can wake Savi any more, that body is the only
channel to her an agent has. -
A local pre-flight for the git lane (
spawn_validate). The push is the validator on this
lane, and it is live in every open room the moment it lands, with no dev/live split to absorb a
broken tree. Sospawn_validatechecks locally what the push law names explicitly: every script
and template is ESM-parsed (vm.SourceTextModulein a child process — a real parse that accepts
import/export, and never evaluates the module),.scenefiles are checked against their
# spawn-scene v2 yaml <cellKey>header and the cell key their filename implies, image bytes are
checked against their extension, and binaries underassets/are caught before
law.git.asset-kindrefuses them.spawn_pushruns the same check over the changed files and
blocks on failure. -
depthonspawn_init— how much history a 6.0 clone takes (default 20,0for all of it).
Fixed
-
The play client could not open a 6.0 world.
agent/docsreturnsplayUrlabsolute on the git
lane and relative on the document lane, and four call sites prefixed it with the API origin
unconditionally — producinghttps://www.spawn.cohttps://www.spawn.co/@user/world. All of them
now go through one helper that leaves an absolute URL alone. -
spawn_savireported an opaque 403. The studio-chat door is now closed by account class: a
standalone agent account (sak_from/signup) can never wake Savi, linked or not, while a
human's own token still can. Retrying and re-wording both fail forever, so the tool names the
reason and points at the channel that does work — the commit body. -
spawn_pushechoed a commit subject that had not landed. When a push sends a commit that
already existed (usually one a refused push left behind), it reports the real HEAD subject and
says nothing new was committed, rather than echoing back themessageargument. -
spawn_latestreported a no-op pull as a change.git rev-parse --shortpicks its length
from the repo's object count, so the same commit abbreviates to 7 characters in a fresh clone and
8 once a fetch has brought more objects in...
v1.5.0
A cross-project asset catalog, and a correction to what this server told the model about making art. Design: ASSET-BANK.md.
Spawn generates a cdn/ asset on first fetch of its path and caches it there forever — the path is the asset — and it has no catalog API. So sharing across projects already costs nothing (the same path is the same asset, which is what Spawn's own "use the same file name and it will be the same asset" means), but nothing anywhere records which names worked. A name cannot be re-rolled: a path that produced something good is unrecoverable knowledge once you forget the spelling, and a path that produced something bad is permanently bad. That judgment is what the bank keeps.
Added
- The asset bank, a user-level catalog in
~/.spawn-mcp/assets/(SPAWN_ASSET_BANKoverrides; a setting pointing at a.jsonfile is read as its parent directory). User-level rather than per-repo on purpose: the team ledger lives in a repo's shared.gitbecause a team is scoped to one game, which is exactly wrong for a store whose value is crossing games. Concurrent MCP processes share it through the same cross-process lock the team ledger uses. Five tools, taking the solo list from 27 to 32.spawn_asset_scan: harvest everycdn/path from one or more directories, recording which files cite it and which game it belongs to. Reads raw file text rather than the compiled spec, so it also catches paths assembled in script strings and works on a project that does not compile. A?animations=query normalizes away, so one asset never becomes three rows.spawn_asset_search: query by text, name, category, filename prefix, kind, slug, namespace, verdict, project, game, or reuse count. Ranks named, described and known-good assets above bare path matches and sinks bad ones, and reports the true match count rather than the page size, so a truncated result never reads as "that is all there is".facets: truereturns counts by category, kind, style family and namespace over the whole match set, which is why there is no separate listing tool.spawn_asset_note: a name, a category, description, tags, a good/bad verdict, andreplacedBy— including for a path that has not been scanned or even used yet, since the moment before the first fetch is when the name is still changeable. There is deliberately no delete: a bad name is the one record that cannot be recovered by scanning again.spawn_asset_preview: existence check plus, for images, the bytes inline so the model can judge the art instead of inferring it from a filename. Checks the storage host rather than the/cdn/cook route, because the cook route generates on first fetch and answers 401 for anything it will not cook anonymously — it conflates "does not exist" with "not allowed to ask", and asking there is not side-effect-free. Storage answers a plain 200/404 and never cooks. The storage prefix carries an environment and a version segment, so it is resolved by following one redirect and re-resolved when a cached prefix starts missing, rather than hardcoded.
spawn_asset_sync, the authoritative fill. There is no asset API —/api/agent/v1/assets,/mediaand/cdnall answer404 not_found, and the only account-wide endpoint is the game list — so the closest thing to a catalog is the set of specs your own games have pushed. The sync lists every game on the account, fetches each one's current server-side spec, and harvests itscdn/paths, attributing each to the script it appears in. This sees what a local scan structurally cannot: games with no checkout on this machine, and assets a teammate or Savi pushed that never reached your disk. On the account it was built against, a local scan of three projects found 169 assets and the sync found 408 — the same game with 105 references on disk had 298 in its pushed spec. Slow on purpose (one spec fetch per game, each carrying every script source; four at a time, six games ≈ 3s). Specs are fetched outside the bank lock so a multi-minute sync cannot stall other agents' notes, and a game that errors is reported and skipped rather than losing the whole run.- Every asset tool recommends a sync when the bank is empty, never synced, or over a week stale — not just search. The damage happens at the moment of invention: a stale bank answers "no match", the model coins a fresh path, and an asset that already exists under a good name is regenerated under a second one. Because a path cannot be re-rolled, those two names can never be merged afterwards, so the warning belongs on every surface that could precede it.
- Names. A name is a short unique handle, and every tool that takes a path takes a name instead —
spawn_asset_preview path="knight"rather than sixty characters of style family and hyphenation, which is the same problem the bank exists to solve. Collisions are refused rather than silently reassigned, since a handle that resolves to two things is not a handle. Lookup is case-insensitive, andreplacedByaccepts a name too. - Your own categories, kept separate from the filename's. The token a path starts with (
model-,texture-) is nowprefix, andcategoryis free-form and assigned by you ("enemies", "ui-icons"). They are different axes — one is a type that largely restateskind, the other is what the asset is for — and both are searchable. Assigned fields are the only ones written to disk; derived ones are recomputed on read, so a rescan can never overwrite a judgement and a grammar change cannot leave an asset mis-filed. - Reuse is counted in games, not directories. In team mode one game is several worktrees, each its own project directory, so counting directories reported a three-agent team as three games — a headcount of your own agents dressed up as a reuse signal. Each use now records the
variantIdfrom that project's own.env(never the process fallback, which during a multi-directory scan would stamp one variant onto every project), and the count collapses uses that share one. A project naming no variant counts on its own rather than being quietly merged. Reuse across games feeds ranking, andminGamesfilters on it. - Sharded storage, one file per style family plus
_meta.json, with therootnamespace split by filename prefix (root-effect.json,root-sfx.json) because it has no style family and is typically the largest group — on the bank this was developed against, 116 of 170 assets, which would have recreated the single oversized file sharding exists to avoid. Measured at 821 bytes/asset a 10,000-asset bank is ~7.8 MB and parses in ~19 ms, so this is explicitly not a search optimisation: it keeps a one-field note from rewriting the whole catalog, and keeps each file openable by a human. A corrupt shard loses that one family rather than the catalog. - Namespace classification and collision warnings. Paths are read as
moodboard(the documentedmoodboard-<slug>/<category>-<name>.<ext>form),root,custom, oringested. Arootpath is a bare global name shared with every other Spawn game — if naming is creating and nothing namespaces it,cdn/model-tree.glbis whatever the first fetch anywhere produced — so it warns on every retrieval. A moodboard slug outside the nine canonical families is noted but allowed.ingested(public.<base64>) paths are opaque uploads, not names, and are ranked last so they are never offered as a style example.
Fixed
- The session guide no longer tells the model it cannot make art.
spawn_sessionandspawn_getting_startedboth claimed "you CANNOT generate images or conjure 3D models through this MCP … your art levers are cdn/ assets that already exist", and pointed at Savi instead. Naming a path is the generation lane, so this steered agents away from the engine's main art affordance and toward untextured primitives — the exact failure the 1.3.0 skills work existed to prevent. It now explains the mechanism, the moodboard convention, the canonical slugs, that a path cannot be re-rolled, and that bare names are globally shared.
v1.4.0
Running several agents against one game (see TEAM-MODE.md). A Spawn identity is a property of a project directory, not of a session, so one git worktree per agent gives you a team with no connection registry and no agent runtime in this server.
Added
- Team mode, off unless
SPAWN_TEAM=1or a ledger already exists. Adds six tools and leaves the solo tool list at 27.spawn_team_init: create the shared ledger if absent and register this worktree under a label. The ledger lives in the repo's common.git/spawn-team/, so every worktree resolves to the same path with no configuration, it is scoped to one game, and it cannot be committed by accident.SPAWN_TEAM_DIRoverrides for agents that are not worktrees of one repo. Warns when a worktree has no key of its own, when its key came from the MCP config rather than its.env, and when its variant differs from the rest of the team.spawn_team_status: the whole team at a glance: every agent, how far behind head each local rail is, whose worktree has unresolved conflict receipts, open claims, recent pushes, and head vs published.spawn_team_claim/spawn_team_release: ownership ofgame.jsonkey paths (entities.player) andscripts/globs (scripts/hud/**). Everything exceptscripts/**is claimed by key path, because that is wherespawn_initputs the whole spec; a pattern that looks like a file path outsidescripts/is rejected rather than silently never matching.spawn_team_add: stands up a new agent in one call: writes its variant, trades its one-time key for its own token, scaffolds the project, and registers it. Called before the worktree exists it returns the exactgit worktree addcommand instead of running it, because this server executes no subprocesses. It refuses to proceed without a distinct key for the new agent: sharing one token would make every agent indistinguishable on the version rail.spawn_team_brief: a ready-to-paste opening prompt for one builder or the whole team, covering who it is, its worktree, what it owns, what teammates own, whether it is behind head, and the working rules. Text out; the LLM decides what to do with it. This is the dispatch affordance, and it is deliberately not an agent runtime.
- Pushes serialise and rebase in team mode.
spawn_pushtakes a ledger-wide lock, and from inside it "behind head" can only mean a teammate landed a push since your last sync, so it pulls first. A clean rebase costs nothing and the 409 never happens; a rebase that collides stops the push with your work intact and the colliding paths named.force: trueskips it, being a deliberate whole-replace. The lock's stale window is longer than the HTTP timeout so a slow push cannot have its lock stolen mid-flight. - Claim warnings on push, and attribution instead of "someone else". Changes are diffed against the base rails, so exactly this agent's own edits get checked against other agents' claims, and collisions are reported alongside the successful push rather than blocking it. Every push is logged to
pushes.jsonl, so a 409 names the teammate who took the version and what they touched, andspawn_latestsays whose push you just pulled. - The session latch.
spawn_push,spawn_latest applyLocal,spawn_revoke, andspawn_play_openbind to the first project directory they see and refuse a second, because identity, the.spawn/base-versionrail, and the single Chromium session all belong to one directory. Read-only tools still inspect any worktree, and provisioning (spawn_bootstrap,spawn_init) stays unlatched so a new worktree can be set up from anywhere. - A globally configured
SPAWN_PROJECT_DIRis refused in team mode, with the reasoning in the error rather than a bare rejection: it would resolve every session to one.env, so every agent would push to the same rail as the same connection while appearing to work in its own worktree, and nothing would error. An explicitprojectDirargument is unambiguous and never refused.
Fixed
- A pull no longer silently discards your
game.jsonedits.spawn_latestused to overwrite the file wholesale, with no receipt and no mention in the sync summary, even thoughspawn_initputs the entire spec in it. Script sources were the only content with a merge story. There is now a.spawn/base-game.jsonrail and a three-way merge by key path: keys only you moved stay, keys only upstream moved fast-forward, and a key both sides moved keeps your value, lands in the reportedconflictslist as a dotted path, and writes agame.json.theirsreceipt that blocksspawn_pushuntil you resolve it. Same contract the script receipts already had. - The project
.envnow wins over the process env forSPAWN_AGENT_KEYandSPAWN_VARIANT_ID; process env remains the fallback for a project that carries none. A key in the MCP config used to override every project, which madespawn_bootstraplook like it had done nothing and pinned every checkout to one connection.spawn_statusreports where each credential came from.
Notes
- Existing projects have no
.spawn/base-game.json, so the first pull after upgrading keeps the old whole-replace behaviour, copies the previousgame.jsonto.spawn/replaced-game.jsonwhen it would drop anything, and establishes the rail. Pulls merge from then on.spawn_statusreportshasSpecRail. world/*.jsonoverlays are still not reconciled: they re-apply over pulled content at compile time. Disjoint overlays compose fine, overlapping ones do not. In practicespawn_initputs the whole spec ingame.jsonand leavesworld/empty, so the key-path merge covers the common case.- The shared
.gitis located by reading.gitdirectly (a directory in a main checkout, agitdir:pointer pluscommondirin a worktree) rather than by runninggit rev-parse. This server still spawns no subprocesses. - Tool count is unchanged at 27 solo, and 33 with team mode on.
spawn_init's scaffolding is now shared withspawn_team_add, so provisioning a teammate's worktree cannot drift from provisioning your own.
v1.3.0
First tagged release. Everything in this version is about closing the quality gap between what an agent builds through this server and what Savi builds in the Spawn studio. Two causes, addressed separately.
The engine's craft lives in its ~60 skills, not in the API reference, and an agent that never loads them writes code that validates and pushes but lands as untextured primitives and default DOM. Rather than add a prompt telling the model to go read them, the endpoints themselves are now shaped to pull skills in.
Added
spawn_getting_started: the whole workflow in one tool call, plus a checklist of what the project already has (token, variant,game.json, docs) and which step is next. Needs no credentials. Most MCP clients never surface prompts to the model, so thespawn_sessionprompt was effectively unread; both now serve the same text from one constant.spawn_skills: the menu of skill ids with descriptions, so a build can be planned against what actually exists. Reads.spawn/skills.jsonwhenspawn_initorspawn_docsalready saved it, so browsing costs no network call and no credentials, and falls back to the API. Supportssearch,detail: full|brief, andrefresh.CHANGELOG.md(this file).
Changed
spawn_skillnow takesids: [...]and loads a set in one call, each skill labelled in the response. Passing several is the natural shape rather than something to repeat.idstill works as a single-value alias, and duplicates collapse.- A wrong skill id answers with the full menu, so guessing costs one call instead of two and there is no reason to skip loading for want of an id. A partial miss keeps whatever did load and reports the rest.
- Tool descriptions name the missing craft at the moment it shows up.
spawn_play_screenshotsays that grey boxes or default browser UI is a missing skill rather than a missing feature;spawn_pushsays a successful push only proves the spec parsed;spawn_validatesays schema-valid is not the same as good;spawn_inithands over a concretespawn_skillcall. - The session guide gained an art, UI, and look section, including the fact that image generation and model conjuring are not available through this server at all. That lane belongs to Savi in the studio, so an agent's levers are code-drawn textures, scripted materials, composed primitives, and existing
cdn/assets. spawn_saviis now described accurately as a one-way note into the creator's studio chat. There is no reply channel and no way to hand Savi a task from here, so an agent should not wait on one.- README gained an "Art and UI" section; the getting-started guide gained the plain-language version of the same two limits.
Notes
- Skills are long documents, roughly 7k tokens each, so
spawn_skillasks for 2 to 4 ids for the work in front of you rather than the whole menu. - Tool count goes from 25 to 27.