About creator-hub-mcp
creator-hub-mcp is a skill guide for editing live scenes in Creator Hub through MCP. It sets out workflows for scene changes, asset handling, and choosing catalog items or file-based edits.
Key features
- 01Edit live scenes through MCP
- 02Search for and place Smart Items
- 03Handle prompt attachments and local assets
- 04Follow undoable scene-editing workflows
Use cases
- 01Add or modify scene entities in Creator Hub
- 02Use user-provided models or images in a scene
- 03Decide when to connect MCP or edit files
MCP support
Creator Hub runs an MCP server in the app with tools for reading and changing the open scene. Its built-in AI assistant and external MCP clients can use the server.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill creator-hub-mcpAfter installing, type/creator-hub-mcpin Claude Code to confirm it loaded.
Bundled filesreference/connect.md
Details
From the project READMECreator Hub MCP — live scene editing
The Creator Hub runs an MCP server inside the app (localhost, streamable HTTP, bearer token). Its tools read and mutate the scene that is open in the editor: changes apply to the live engine, show up in the viewport immediately, autosave to assets/scene/main.composite, and every one of them is a normal undo step for the user. The same server powers the Creator Hub's built-in AI assistant and can be exposed to any external MCP-capable tool.
Two ways you can be running:
| You are… | How the tools reach you |
|---|---|
| Inside the Creator Hub's AI assistant (Settings > AI > Enable AI Assistant; on by default since Creator Hub 0.50.0 — it is no longer experimental) | Pre-wired. The app launches your claude / codex / cursor-agent / gemini CLI with the server registered as creator-hub, links these skills into the scene, and injects the same rules as this skill. Nothing to set up, for any of the four providers. |
| In another tool (Claude Code terminal or VS Code extension, Cursor, Codex, Claude Desktop, …) | Connect to the running Creator Hub — see reference/connect.md. The user copies a JSON snippet from Settings > AI > Expose AI assistant MCP server. Skills are not wired up for you here — see Where the skills come from. |
In Claude Code the tools appear as mcp__creator-hub__<tool>; inside the Creator Hub's assistant and in other clients they appear under their bare names (scene_state, create_entity, …). The connected tools are self-describing — treat the live catalog as authoritative over the table below.
Files the user attaches to a prompt
Inside the Creator Hub's assistant the user can attach files to a turn (creator-hub 3395b643): the Attach files paperclip left of the composer input opens a multi-select picker, files can be dropped onto the composer (overlay: Drop files to attach), and images can be pasted from the clipboard. Attachments appear as removable chips; 8 per prompt is the cap. What this means for you:
- A file picked or dropped from disk arrives as its real absolute path — no copy, no base64 round-trip, no size cap. Read it in place; do not ask the user to re-paste its contents or to move it into the project. A pasted (clipboard) image has no path, so the app writes it to a temp file and hands you that path instead.
- Any file type is accepted. The app classifies it only for the chip icon:
image(png, jpg, jpeg, gif, webp, bmp, svg, avif),model(glb, gltf),audio(mp3, wav, ogg, m4a, flac),filefor everything else. - This is the natural way to hand over a reference image or a GLB. A dropped
.glbis a model the user wants in the scene: it is still outside the project, so copy it underassets/Models/before referencing it fromGltfContainer(see add-3d-models for the bounding-box and collider audit). A reference image is usually art direction for a UI or material, not a file to import. - A prompt may arrive with attachments and no text at all — the attachment is the request. Ask what to do with it rather than guessing.
Assets the user mentions from the Local Assets panel
A second, different channel (creator-hub d25eff1b8): double-clicking an asset in the inspector's Local Assets panel (tree or card) attaches it to the next prompt as a chip, next to the entity selection. Several assets plus the current selection can ride on one prompt; mentions clear after sending or when the chat closes. The asset arrives in the turn as [Editor context], so:
- A mentioned file comes as a project-relative path that already exists in the scene — reference it directly, do not copy or re-import it.
- A mentioned built-in avatar animation comes as a name, not a path: there is no file to find. Play it with
triggerEmote({ predefinedEmote: '<name>' })(see avatar-animations). Looking for a.glbfor it is the failure mode this channel is designed to prevent. - A chip is a pointer, not an instruction. "This one" plus a chip still needs a verb — ask if the request is ambiguous.
RULE — the MCP is the way to change the scene graph; the file is the fallback
Never write assets/scene/main.composite, main.crdt, or scene.json by hand while the Creator Hub MCP is available. Reasons, all verified in the Creator Hub source:
- The editor owns those files. It regenerates
main.compositewholesale from its in-memory engine on every autosave (~100 ms after any editor transaction) and never re-reads the file from disk. A hand edit to an open scene is silently lost; there is no error. - The tools do the bookkeeping the file workflow makes you do by hand:
inspector::Nodesregistration,core-schema::Name,inspector::TransformConfig,entity-names.ts, entity-id allocation,jsonSchemaon non-core components. - Every mutation is undoable by the user (Ctrl+Z / the assistant's Undo AI changes button). A file edit is not.
Decision ladder — pick the first row that matches:
- Tools present and answering (
get_selectionorget_scene_settingsreturns — probe with an editor-routed tool, notscene_state, which answersNo scene is open.from an external client even with the scene open; see Gotchas) → use them for every entity/component/settings change. Read the composite from disk only as a read-only cross-check. - Tools present but failing (
No editor window is open., connection refused,401) → the Creator Hub is closed, was restarted (URL and token rotate per launch), or no scene is open. Say so and ask the user to open the scene in the Creator Hub (or re-copy the connection snippet). Do not fall back to editing the file silently. - Tools absent, but the user has the Creator Hub → tell them the MCP is the safe route and point them at
reference/connect.md. Hand-edit the file only if they decline, and only with the scene closed in the Creator Hub (the composites skill's edit-mode rules then apply in full). If the scene is (or may be) open, present the three options — close the scene, connect the MCP, or explicitly accept static entities built in code (invisible in the editor) — and wait for the answer; do the work that does not depend on it first. Never pick the code fallback yourself, not even in a non-interactive session: a 2026-10-23 session built a whole room insrc/rather than ask, and the creator rejected it. Canonical rule: sdk-scenes → Agent Behavioral Guidelines, item 5. - No Creator Hub in play (CLI-only project, CI) → the composites skill's file workflow applies.
What still lives in files and is yours to edit directly: src/** (scene code; saving triggers a rebuild automatically), assets/Scripts/*.tsx (script components — write the file, then attach_script), and asset files you add under assets/Models/, assets/Images/, etc.
Static geometry is the editor's, not src/'s. Walls, floors, furniture and props — primitives or GLBs — go into the scene through these tools (create_entity + set_component MeshRenderer / GltfContainer, or place_smart_item), never as engine.addEntity() calls in scene code. A room built in code renders in the preview but is invisible and unselectable in the Hub's viewport and entity tree, and the creator's first question is "where is my office?" (2026-10-08: about 60 set_component calls to redo it). This is the composite-first rule of sdk-scenes, applied through the MCP. Without the MCP and with the scene open, the same holds: stop and ask (ladder row 3) — code-built static geometry is acceptable only when the creator explicitly chooses it (2026-10-23: a session built a room, a chair and ~60 props in code without asking; the creator: "NEVER do this without a warning").
A save is not proof the running bundle has your change. The preview's dev server (sdk-commands start) watches src/** and rewrites bin/index.js on every save (logic/bundle.ts in @dcl/sdk-commands). A manual sdk-commands build while it runs writes the same file and the two race, and a save that lands mid-edit bundles half-written code (SyntaxError, scene unloads) — the one-write-per-change rule in unity-explorer-mcp applies. When something you changed does not show in-world, grep bin/index.js for a string literal from the edit before debugging the scene; if it is absent, touch the file and wait for the next Bundle saved. (Session 2026-10: an agent reported a change as done that the stale bundle never contained; the creator caught it.)
RULE — search the catalog before manually building any standard object
Before creating an entity and manually assembling its components with create_entity + set_component, always search_catalog first to check whether a Smart Item already exists for the object. This applies especially to:
- Lights — the catalog has Spotlight and Point Light Smart Items (category
lights) that bundle a real GLB model, a pre-configuredLightSourcecomponent, and built-in Turn On / Turn Off / Toggle actions. Manually creating aLightSourceentity misses the visual model, inspector integration, and action wiring. - Interactive furniture — doors, chairs, platforms, chests, buttons, levers, etc. Newer script-based items: Floor Pressure Button (
buttons, pressed while anyone stands on it, shared across players by default), Button Pedestal (buttons), Lab Vault Door / Lab Capsule Door (doors), Lab Stair Platform (platforms, piston), Water Plane / Water Cylinder (materials, splash where a player enters). - Media — screens, image displays, NFT frames.
- Triggers — trigger areas, click triggers. The
utilscategory has exactly one entry named "Trigger Area" (e9fa0eab-44de-4efe-af77-a71171a1a73f), a script-based item: an invisible box or sphere the user places and resizes, with a control that seeds the AI-assistant composer with a prompt describing what should happen on enter/leave. The older non-script item of the same name (1ab2733f-1782-4521-9eda-6aa8ad684277) and the interim label "Trigger Area (Script)" were both removed in creator-hube43441b5— do not search for either.
place_smart_item handles all the boilerplate that manual set_component calls miss:
- Downloads the item's GLB files and resolves
asset-packs::Placeholderwith the real on-disk path (e.g.assets/asset-packs/spotlight/spotlight.glb). - Sets up
asset-packs::Script,asset-packs::Actions, andinspector::Configcomponents so the item works in the Creator Hub UI with configurable actions and triggers. - Registers the entity in
inspector::Nodesautomatically.
Only build manually when no catalog item fits the requirement.
Media the user already has: tell them to drag it from the Local Assets tab onto the viewport. Since creator-hub 062ed0a2 that is the fastest path and it spawns a catalog item pointed at their file, not a bare component — so the entity arrives with the same component set and basic-view inspector::Config a hand-placed item has:
| Dropped file | Item spawned | What gets repointed |
|---|---|---|
.png / .jpg / .jpeg | Image (37460a1e-affc-4f87-b725-118cc11d86fc) | the Material PBR texture src; the plane is also lifted +0.5 m so it stands on the ground instead of being half-buried |
.mp3 / .ogg / .wav | Ambient Sound - Forest Birds (5c8b4646-6ec0-41a0-8e9b-415a58728a9e) | AudioSource.audioClipUrl |
.mp4 | Video Screen (0201653b-bd38-48af-b9ef-a902a7e8bc9c) | VideoPlayer.src and asset-packs::VideoScreen.defaultURL — the admin message bus re-seeds src from defaultURL at runtime, so setting only src is silently reverted |
The entity is named after the file with the extension stripped, de-duplicated against existing names, and the template's own placeholder media file is skipped on import. .glb / .gltf still drop as plain models. You can reproduce this through the tools (place_smart_item the same asset id, then set_component the field above), but the drag is one gesture for the user — prefer it when they are at the keyboard.
Tool catalog
Names as registered on the creator-hub server (packages/creator-hub/main/src/modules/scene-mcp.ts).
| Group | Tool | What it does |
|---|---|---|
| Read | get_project_info | scene.json metadata (name, parcels, base, spawn points), SDK version, dependencies. Use it for Step 0 bounds. |
| Read | scene_state | Roster of authored entities: id, name, kind, world transform, component list, GLTF src, Smart-Item flag. Truncated to the first 200 entities. |
| Read | entity_detail | Every component value on one entity, by id or Name. Call it to learn a component's exact shape before set_component. |
| Read | get_selection | Entities currently selected in the editor. Resolve "this" / "the selected one" with it before acting. The Scene root is never reported (creator-hub d25eff1b8): with only the root selected the result is empty, exactly as with nothing selected — so an empty result does not mean the user has deselected everything, and "this" may still refer to the scene as a whole. Player and Camera are reported normally. |
| Read | get_scene_metrics | Live budget (triangles, entities, bodies, materials, textures) vs per-scene limits, plus entities out of bounds. |
| Read | editor_screenshot | Image of the editor viewport (no preview needed). |
| Mutate | create_entity | New entity, optional name and parent id. Returns the id. |
| Mutate | remove_entity | Deletes an entity and its children. |
| Mutate | set_parent | Reparents (world position preserved). parent: 0 = scene root. |
| Mutate | set_component | Create or update a component. component accepts short (Transform) or full (core::GltfContainer) names; value is the same JSON the composite stores. On update the keys you pass are merged. |
| Mutate | remove_component | Removes a component by name. |
| Assets | search_catalog | Search the Creator Hub asset-packs catalog by name/category/tag. Despite the name it covers static items as well as Smart Items. Omit query to list everything. Categories include buildings (whole buildings, listed after structures) and materials (the Water Plane / Water Cylinder items), and the Avatar Animations pack (45 _emote.glb clips + the 32 built-in emote names) — file-only assets, see place_smart_item. The same additions are catalogued offline in add-3d-models → references/asset-pack-catalog.md and avatar-animations → references/animation-catalog.md. Name, category, tags and caption are generated metadata, not a preview: search_catalog for "man" returns Human (decorations, tags man, person, figure, humanoid…, caption "Human figure in mid-action"), whose GLB is the word HUMAN in 3D letters (packs/cyberpunk/assets/human/Word_Human.glb, creator-hub origin/main 2026-10-19; placed as a character in a 2026-10-12 session). Before placing an item you have not seen, look at its thumbnail.png — next to its data.json under assets/asset-packs/<pkg>/ once downloaded, or in the creator-hub repo under packages/asset-packs/packs/<pack>/assets/<item>/ — or run the bounding-box audit from add-3d-models on the GLB. |
| Assets | place_smart_item | Place a catalog item by assetId at a world position (default 8,0,8). Downloads the item's files into assets/asset-packs/<pkg>/ and spawns it — Smart Items arrive with their behaviour. Most items now arrive as asset-packs::Script code (no Triggers); configure them through the Script's params, and wire "When X" hooks via their optional ActionCallback params. See script-components. Rejects file-only assets (the Avatar Animations pack — there is no entity to place): for a built-in emote the error names the triggerEmote({ predefinedEmote }) call to use; for a clip, ask the user to drag the card onto the viewport (adds the file under assets/asset-packs/<pkg>/, no entity) or download it per avatar-animations → references/animation-catalog.md, then play it with triggerSceneEmote from code. |
| Scripts | attach_script | Adds an asset-packs::Script component pointing at a file. Write the file first (under assets/Scripts/), then call this. Optional priority. |
| Settings | get_scene_settings / set_scene_settings | Read / change scene.json fields (name, description, categories, tags, age rating, spawn points, skybox, terrain, layout.parcels, flags). Each passed field replaces the current one wholesale — read first. Never pass spawnPoints to it: its input takes the scene.json shape, which lands unconverted in the metadata component and either kills the data layer or writes "position": {} into scene.json — write spawn points with set_component on entity 0 instead, see the data-layer rule below. thumbnail is not in this tool's input schema (a thumbnail key is silently dropped) — set it with set_component on entity 0's inspector::SceneMetadata-v*, or in the editor's Scene settings → Thumbnail, and never write a cover to assets/Images/autogenerated-thumbnail.png: see Gotchas. |
| Preview | launch_preview, preview_status, stop_preview | Start / poll / stop the scene in the Decentraland Explorer with its MCP server on. |
| Preview | explorer_<name> (dynamic), explorer_call | While a preview runs, the Explorer's runtime tools (explorer_screenshot, explorer_walk, explorer_click_entity, explorer_get_scene_logs, …) are registered live; explorer_call(tool, arguments) is the fallback if they haven't appeared yet. |
| Chat | ask_user | Blocks on a question shown in the Creator Hub chat. Only meaningful inside the embedded assistant. Times out after 10 minutes with no answer and resolves as a dismissal (ASK_TIMEOUT_MS = 10 * 60_000 in packages/creator-hub/main/src/modules/scene-mcp.ts, verified at origin/main 2026-10-15). On timeout the prompt is also closed in the chat ("This question is no longer active.") and any answer arriving afterwards is discarded — a timed-out ask_user can never be answered retroactively (creator-hub dc9d076be). Never block a turn on it if the creator may have stepped away; put the question in your final message instead. |
Workflow
Read before you write. get_project_info for parcels (compute bounds exactly as in the composites skill Step 0 — the tools do not stop you from placing entities outside the scene, and get_scene_metrics is a weaker safety net than it looks — see the Bevy-renderer gotcha below). scene_state to see what exists and avoid duplicate names. get_selection whenever the user's request refers to what they have selected.
Adding a model.
- Catalog item →
search_catalogthenplace_smart_item. Files land inassets/asset-packs/<pkg>/automatically. - Architecture (rooms, corridors, doorways, anything the avatar walks through or into) → size the envelope for the third-person camera before placing; realistic dimensions read cramped in-world. The dimensions are in add-3d-models → "RULE: Scale interiors up for the third-person camera".
- Model the user already has, or one you authored → put the GLB under
assets/Models/(see the add-3d-models skill for Blender authoring and the bounding-box audit), thencreate_entity(with aname),set_componentTransform({position:{x,y,z}, rotation:{x,y,z,w}, scale:{x,y,z}}), andset_componentcore::GltfContainer({src, visibleMeshesCollisionMask, invisibleMeshesCollisionMask}— mask rules and the animation/collider checks from add-3d-models still apply; addcore::Animatorwhen the GLB has clips). - Lights →
search_catalogwith categorylightsto find Spotlight and Point Light Smart Items, thenplace_smart_item. These include the light model,LightSource, and toggle actions. Only create a bareLightSourceentity manually when you need an invisible light source or non-standard parameters -- see lighting-environment for the fullLightSourceAPI and the Smart Item rule. - Behaviour on it → a Smart Item if one fits (
search_catalogfirst), otherwise write a script underassets/Scripts/andattach_script(rules in script-components), or reference the entity by name fromsrc/code (engine.getEntityOrNullByName).
Component shapes. set_component validates against the real schema. Before your first write of a component type, entity_detail on any entity that already has it — or read its entry in the composites format catalog. For asset-packs::Script, params are positional inside layout (see script-components); prefer attach_script and let the editor build the layout.
After writing. Read-after-write is safe while the data layer is alive — the server waits for the autosave before returning from a mutation, so a following scene_state is fresh. It stops being safe the moment that process dies (next rule), which is why the check below is on disk and not on a tool answer. Check get_scene_metrics when you added geometry. editor_screenshot shows the result without a preview; launch_preview + explorer_* when you need the running scene (walk, click, logs, FPS). The iteration loop, camera framing, and performance references in the unity-explorer-mcp skill apply to the explorer_* tools unchanged — skip that skill's Setup section (no claude mcp add, no bind gate) because the tools arrive through the creator-hub server. When done, leave the camera in the scene's intended retail mode (third person unless the scene forces first person) and stop_preview if you launched it only to check.
If you have no runtime view, say so once and label what you cannot see. From an external client the explorer_* tools never appear, and the unity-explorer-mcp server refusing its connection just means the Explorer is not running (that skill's Setup launches it). Offer to set it up; until a screenshot or the creator confirms a visual result, report it as unverified ("the screen plane should now sit inside the bezel — I have not seen it"), never as done. Two sessions on one scene (2026-10) delivered every visual defect — a screen floating in front of its monitor, a door clipping its frame, z-fighting walls, white materials — to the owner first, because fixes were reported on the strength of the code alone.
Never pin an editor entity's placement from code. Scene code that writes the Transform of an entity the editor owns — on every load, to "correct" where it sits — silently overrides every move the creator then makes in the Hub: they dragged a tray onto a belt and it kept reappearing 2 m away in mid-air (owner-reported symptom, cause read in the scene's own code, 2026-10-11). Code may read an editor entity's Transform; if it needs placement of its own, give it its own entity.
Transforms on nested entities are parent-local. An entity the Hub nests under a group reports Transform.position relative to that group, not to the scene, so resolve the parent chain before using a position for a distance check or to place something beside it (read in assets/scene/main.composite, 2026-10-11 — harmless in that scene only because the groups sat at the origin).
Mistakes. Undo is the user's — tell them what to undo, or remove_entity / set_component back to the previous value. Never "repair" a mistake by editing main.composite.
RULE — confirm a batch landed on disk; the data layer can die silently
The thing that holds the editor's engine and writes assets/scene/main.composite is the @dcl/inspector data layer, and under the Bevy renderer it runs in a separate child process — logged by the app as [UtilityProcess] Running "sdk-commands start --port <n> --no-browser --no-client --data-layer" (assembled in main/src/modules/bevy-realm.ts; the port is ephemeral, the binary is the scene's node_modules/@dcl/sdk-commands). Under the Babylon renderer the same data layer runs inside the inspector iframe instead. Either way the Electron main process never writes the composite — it only reads it.
When that data layer dies, the MCP server in the Electron app stays up and keeps answering every call as if nothing were wrong. Nothing in a tool result tells you the scene stopped changing.
So after every batch of mutations, check the file, not the tool: assets/scene/main.composite's modification time must have advanced (ls -l/stat). An unchanged mtime across a batch means the edits exist only in a process that is gone.
stat -f '%m %N' "assets/scene/main.composite" # macOS; coreutils: stat -c '%Y %n'
(Get-Item "assets\scene\main.composite").LastWriteTime # Windows (PowerShell)
Observed symptoms of a dead data layer (2026-10-08 session; check these when the mtime is stale):
set_component,create_entity,place_smart_itemand friends all still return success.scene_state/entity_detailkeep returning the pre-crash values — a read-back "confirming" your write is confirming the old snapshot.editor_screenshotreturns a blank image.get_scene_metricsreturned all zeros with an emptyentitiesOutOfBoundaries— but do not use that as the tell — under the Bevy renderer those fields are hardcoded zeros anyway (see Gotchas). The mtime check is the reliable one.
Recovery is the user's, and your in-memory edits are lost. There is no tool that restarts the data layer. Stop writing, look at the Creator Hub main log (macOS: ~/Library/Logs/creator-hub/main.log; Windows: %APPDATA%\creator-hub\logs\main.log) for an Exiting "sdk-commands start … --data-layer" … exit code=1 line — grep -n "exit code=1\|reading 'deserialize'" main.log, not a skim: the 2026-10-08 session had the crash line inside a 241 KB grep result and read only the first 2 KB of it — tell the creator to close and reopen the scene in the Creator Hub, then re-apply every mutation made since the crash — compare against the composite on disk to find where the truth stops. Leaving the editor page also kills that scene's preview server (handleBack → killPreview in the Hub's EditorPage), so after the reopen the creator presses Play again — or you call launch_preview again — before expecting anything new in-world.
Why set_scene_settings + spawnPoints kills the data layer or empties the position — read in source (creator-hub origin/main d25eff1b 2026-10-19; @dcl/ecs main). The tool's input schema takes position in the scene.json shape — x: number | [min, max] (main/src/modules/scene-mcp.ts) — and the inspector's handler merges the patch verbatim into the root entity's inspector::SceneMetadata-v* component ({ ...current, ...patch }, packages/inspector/src/lib/rpc/scene/server.ts), whose position.x/y/z are Schemas.OneOf({ single, range }) and expect { $case: 'single', value: 8 } or { $case: 'range', value: [a, b] } (lib/sdk/components/SceneMetadata.ts). Nothing converts one shape into the other, so a plain number sits in a OneOf slot and the CRDT serializer sees no $case. The Hub's own inspector runs @dcl/ecs 7.29.0 (root package.json), whose OneOf serializer writes an empty case for a value with no $case (js-sdk-toolchain 0dce4d2d, #1570, shipped in 7.28.0+). What happens next depends on the scene's own node_modules/@dcl/inspector data layer: on an @dcl/ecs older than 7.28.0 its OneOf deserializer cannot read that empty case (specs[specKeys[0 - 1]] is undefined) and the data layer dies — that is the TypeError: Cannot read properties of undefined (reading 'deserialize') in tooling-entrypoint.js (Object.deserialize → updateFromCrdt → receiveMessages), seen twice on 2026-10-08 with point-valued spawn points: version skew between the Hub's writer and the scene's older reader. With a matching @dcl/ecs the write survives but reads back as {}, and the data layer's scene.json writer (data-layer/host/utils/component.ts) emits "position": {} — seen twice on Creator Hub 0.51.0 (2026-10) while get_scene_settings still showed a well-formed value. Point and range values are both affected; the range-only suspicion in earlier versions of this skill was wrong.
So: write spawn points with set_component on entity 0, in the component's own shape — the same route as the thumbnail in Gotchas; take the versioned component name from entity_detail(0) and pass only the spawnPoints key:
{"spawnPoints": [{"name": "spawn", "default": true,
"position": {"x": {"$case": "single", "value": 8}, "y": {"$case": "single", "value": 0}, "z": {"$case": "single", "value": 8}},
"cameraTarget": {"x": 8, "y": 1, "z": 12}}]}
A ranged axis is {"$case": "range", "value": [4, 12]}. Then read scene.json back (it follows on the next autosave) and check every spawnPoints[].position holds numbers. If it came out as {}, close the scene in the Hub and fix scene.json by hand: on the next open the data layer re-seeds SceneMetadata from scene.json (overrideWithSceneJson, data-layer/host/composite-provider.ts), so the hand edit wins and the composite's copy is refreshed from it — the same holds for any other scene setting edited with the scene closed. The shape mismatch is a Hub bug; report it with report-sdk-issue if the running version still has it.
Preview lifecycle — stop_preview does not close the Explorer
stop_preview never closes the Explorer — by design, and there is no tool that does. Verified in main/src/modules/explorer-gateway.ts: it closes the MCP client/transport and treeKills the sdk-commands start --explorer-alpha --hub … --mcp --mcp-port <n> child (logged as [UtilityProcess] Killing process "…"), then answers {ok: true}. The Explorer app is not in that process tree — sdk-commands launches it with open decentraland://…, which hands off to the OS and exits — and nothing in the Creator Hub kills it.
preview_status does not notice, either: running is computed from the gateway object existing plus child.alive() on that same sdk-commands handle — not a probe of the Explorer or its port. So "no preview running" is a statement about the dev server, never about the app.
Why the next launch then fails — verified parts first: each launch_preview grabs a fresh ephemeral MCP port ([Gateway] launching preview … with MCP on :53880) and passes it as --mcp-port, and the desktop Explorer is single-instance, so a second launch's deep link is handed to the window already open. The inference (not read off the Explorer source) is that the open window keeps its MCP on the old port and never binds the new one. Either way, after ~45 s of retries the Hub returns:
The preview launched but its MCP server never answered. The Explorer may still be starting, or this build has no MCP support. (TypeError: fetch failed)
and — note — that failure path also kills the dev server it just started, leaving the Explorer orphaned on a dead realm. Observed 2026-10-08: two launches in a row reproduced it. (Related: preview_status can block up to 90 s polling an Explorer that is up but not ready, so a long hang there is not a crash.)
Tool call interrupted: the session ended… on launch_preview / preview_status is the creator stopping the turn, not a preview failure. Both tools block for a long time (the launch retries the Explorer's MCP for ~45 s, the status poll up to 90 s); inside the embedded assistant the chat's stop control kills the CLI child meanwhile (aiStop → killTree, main/src/modules/ai.ts), and the tool call reports itself interrupted. Four such interruptions in one session (2026-10-08) each coincided with the creator's next message, and the preview the agent had launched was running the whole time. Before concluding anything, pgrep the Explorer and read the log — and do not write "launch_preview ends the session" into the project's notes, as that session did.
Rule — before launch_preview, check both the tool and the OS:
pgrep -fl '[M]acOS/Explorer' # the Explorer app itself
pgrep -fl '[d]cl_watchdog' # its watchdog, from /Applications/Decentraland.app
Keep the brackets in these patterns. pgrep -f / pkill -f match against every process's full command line, and many agent shell tools run commands as bash -c "<command>", so a plain pattern also matches the wrapper shell: the check reports a live Explorer that isn't there, and pkill kills the shell running it. [d]cl_watchdog still matches the real process, but never the command text itself.
The inverse also happens: the creator closes the Explorer window and preview_status keeps reporting running: true. The sdk-commands child is still alive, so the handle still says yes, while every explorer_* call fails with fetch failed (observed 2026-10-12). A string of fetch failed results therefore means "the app is gone", not "the scene is broken" — confirm with pgrep -fl '[M]acOS/Explorer', then stop_preview followed by one launch_preview to recover. In both directions the rule is the same: pgrep is the only reliable answer to "is the Explorer running".
preview_status saying "not running" is not evidence the Explorer is closed. If either process is alive and you need a clean preview, close the stale instance, confirm it exited, then launch_preview once:
pkill -f '[d]cl_watchdog'; pkill -f '[D]ecentraland.app/Contents/MacOS/Explorer'
for i in $(seq 1 30); do pgrep -f '[M]acOS/Explorer' >/dev/null || break; sleep 1; done # wait up to 30 s
On Windows the Explorer is Decentraland.exe and the watchdog dcl_watchdog.exe. In PowerShell (matching by process name, so the check never matches the shell running it):
Get-Process Decentraland, dcl_watchdog -ErrorAction SilentlyContinue # is either alive?
Stop-Process -Name dcl_watchdog -Force -ErrorAction SilentlyContinue # watchdog first: it reacts to a non-zero Explorer exit
Stop-Process -Name Decentraland -Force -ErrorAction SilentlyContinue
Wait-Process -Name Decentraland -Timeout 30 -ErrorAction SilentlyContinue # confirm it exited
That Explorer window is the creator's, not a test rig. Everything you drive through the explorer_* tools — explorer_move_to, explorer_reload_scene, camera-mode changes, and any asset-file churn that triggers a hot reload — lands in the session they are playing. Check explorer_get_player_state and recent explorer_get_scene_logs for activity before taking the controls, prefer explorer_set_camera_pose over explorer_move_to for a look-check, and restore the camera mode you found. Full rule in the unity-explorer-mcp skill's iteration loop (observed 2026-10-10).
Connected first try on both attempts after that. Prefer reusing a healthy running preview (hot reload + explorer_reload_scene) over restarting — only kill when it is stale or the MCP never answered. When the user owns that Explorer window, ask before killing it.
A re-baked GLB can keep rendering its old contents in a running preview. Not the dev server's doing: @dcl/sdk-commands 7.30.1 re-walks the project on every content request, serves current bytes no-cache, versions each file's id by mtime, and runs a chokidar watcher the Hub never disables — so the staleness is downstream, in the Explorer's own asset cache. Observed 2026-10-08: GLBs added or renamed after launch_preview were missing in-world until explorer_reload_scene (one case) or a full preview restart (another), and a GLB overwritten in place under the same name kept showing the old mesh. src/ hot reload is unaffected. Practical rule: after re-baking a model, export it under a new file name and repoint GltfContainer.src — that is the form that reloaded reliably; otherwise explorer_reload_scene and, if the old mesh persists, restart the preview. (Symptom: the mesh you just exported is absent or visibly stale while explorer_get_scene_logs shows no load error. Also check .dclignore — an ignored path is never served at all.)
Where the skills come from
How these skills reach the agent depends on which of the two setups above you are in.
Inside the Creator Hub's AI assistant the app installs them — never hand-copy them. The installer lives in packages/creator-hub/main/src/modules/skills.ts:
- It downloads
decentraland/sdk-skillsmain into a per-user cache (keyed on the upstream commit SHA plus the app version, swapped atomically, tolerant of being offline). - It symlinks that cache into the open scene as
.claude/skillsand.agents/skills. Links the app created are added to a.gitignore. If the user already has a realskillsdirectory, it links each skill in individually and skips names the user already owns.
In that setup, do not copy skill directories into the scene and do not tell the user to: a hand-placed copy shadows the managed symlink and then silently goes stale; the app is the updater.
From an external client (Claude Code CLI or VS Code extension, Cursor, Codex, Claude Desktop, …) the skills are the user's to install. The MCP snippet only connects the tools; it does not bring the skills along. Two cases:
-
If the Creator Hub currently has the scene open, the app-managed
.claude/skills/.agents/skillssymlinks already exist in the scene folder, so a Claude Code session started in that folder picks them up. Do not duplicate them. -
Otherwise (a different working directory, the scene not open in the Creator Hub, or a client that reads skills from elsewhere), install them the normal way, from the scene folder or the user's skills root:
npx skills add decentraland/sdk-skills --allRe-run the same command to update;
npx skills updateonly refreshes skills already present and misses newly added ones. See the repo README for per-skill installs.
The app-linked set is the denylisted one. A Claude Code session in a Hub-linked folder therefore has no deploy-scene / deploy-worlds (a 2026-10 session web-searched the Worlds deploy command and hand-wrote worldConfiguration instead). Install them for the user, not into the symlinked folder — npx skills add decentraland/sdk-skills --skill deploy-worlds -g (user-level) — or follow the short path: add worldConfiguration.name to scene.json, then npx @dcl/sdk-commands deploy --target-content https://worlds-content-server.decentraland.org; thumbnails, the two name/description/thumbnail sets and the error table are in deploy-worlds.
Four skills are deliberately denylisted inside the Creator Hub agent: create-scene, deploy-scene, deploy-worlds, migrate-sdk6-to-sdk7. The Creator Hub owns scaffolding and publishing through its own UI, and it is SDK7-only. The installer handles them in two ways:
- A denylisted skill that has supporting files (
create-scene,migrate-sdk6-to-sdk7) ships without itsSKILL.md— itsreferences/stay resolvable from other skills' relative paths, but the skill itself can never trigger. - A denylisted skill whose directory holds nothing but
SKILL.md(deploy-scene,deploy-worlds) is skipped entirely.
If a user inside the Creator Hub asks to scaffold a project or publish, point them at the app's own flow rather than trying to load or reconstruct those skills. A manual npx skills add install from an external client is not denylisted: all four skills are available there.
Gotchas
-
URL and token rotate on every Creator Hub launch. A config that worked yesterday gives
ECONNREFUSEDor401today; the fix is re-copying the snippet from Settings, not retrying. -
One open project at a time. The server operates on the scene open in the single editor window. Mutation tools answer
No editor window is open.when the user is on the scene list. -
Disk-read tools and
launch_previewsayNo scene is open.from an external client even though the scene is visibly open: the project directory is bound toget_project_info/scene_state/entity_detail/launch_previewonly when the embedded assistant starts a turn (setSceneMcpProject, called frommain/src/modules/ai.ts; unchanged from the initial release, creator-hub217e2f9f, PR #1499, throughorigin/maind25eff1b, 2026-10-20). Everything routed through the editor (get_selection,get_scene_metrics,get_scene_settings, all mutations,place_smart_item) works regardless. So from an external client: prove access withget_selection/get_scene_settingsor one harmless mutation plus the composite mtime check above — never with the disk-read tools; readscene.jsonandassets/scene/main.compositefrom disk read-only for the roster; mutate through the tools; and for an in-world check launch the preview from the Hub's Play button with Enable MCP Server ticked and drive it per unity-explorer-mcp (its Setup section applies on this path — theexplorer_*tools never appear, since nothing arrives through thecreator-hubserver). -
Read tools trail the engine by one autosave (~100 ms). The mutation tools already wait it out; only matters if the user is dragging things while you read.
-
Scene RPC "<tool>" timed out (no renderer response)is the Hub's 5 s editor-RPC timeout (REQUEST_TIMEOUT_MSinrenderer/src/modules/rpc/scene/client.ts), not a crash: the inspector did not answer in time. Seen twice oncreate_entitywhile the Bevy editor was in Play (2026-10-08); both succeeded after Play was stopped. Ask the creator to stop Play before a batch of mutations, then retry — do not fall back to editing the file. -
scene_statecaps at 200 entities. For big scenes filter by name viaentity_detailor read the composite from disk. -
Skill denylist inside the Creator Hub. The app links
decentraland/sdk-skillsinto the scene as.claude/skills/.agents/skills, but drops theSKILL.mdofcreate-scene,deploy-scene,deploy-worlds, andmigrate-sdk6-to-sdk7: scaffolding, publishing, and SDK6 migration are the app's own flows there. Their reference files still resolve for cross-links. -
The Explorer
--mcppreview checkbox is a different server. Enable MCP Server lives in the Play Options popover (the dropdown arrow on the editor header's Play button), in the flyout that opens when you hover the Desktop Client row. It launches the Explorer with its own MCP on port 8123 (the unity-explorer-mcp skill) and is only shown when the project is on@dcl/sdk7.25.0 or later — if the user cannot find the checkbox, check the scene's SDK version before debugging the Hub. The Creator Hub MCP described here is the editor's server; inside the embedded assistant use itslaunch_previewinstead of that checkbox. From an external clientlaunch_previewfails (see theNo scene is open.gotcha above), so there the checkbox is the route. -
One Ctrl+Z = one of your operations, not one component write. Since creator-hub
fdc384b5a whole synchronous change burst commits as a single undo transaction (the old 200-operation batch cap used to split a big change across several entries, so one Ctrl+Z left orphaned entities behind). A multi-entity op — placing a composite Smart Item, a scripted item with children — is now one entry, and undoing an add removes the entity from the Bevy viewport too. Still describe your changes to the user in the units they will undo them in. -
Scene audio can be muted in the editor, but only under Bevy. The viewport toolbar has a speaker toggle (Mute scene audio / Unmute scene audio) that forwards every
AudioSource/AudioStreamto the renderer at volume 0; the state survives a reload. It is absent under the Babylon renderer, which does not play scene audio at all. So "I can't hear the sound you added" is not evidence the component is wrong — check the renderer and the toggle first, and note that an authoredvolumeis not what the Bevy viewport is playing while mute is on. -
get_scene_metricsreports nothing useful under the Bevy renderer. Verified inpackages/inspector/src/lib/renderer/bevy/BevyRenderer.ts:getSceneMetrics()returns{triangles: 0, bodies: 0, materials: 0, textures: 0}andgetEntitiesOutsideLayout()returns[], both hardcoded ("No renderer to introspect yet — report zeros"). The limits are still real, andentitiesis derived from the Nodes tree rather than the renderer, but all-zero content metrics and an emptyentitiesOutOfBoundariesmean "Bevy", not "empty scene" and not "problem" — do not read a budget or an out-of-bounds verdict out of them. Under Babylon they are real:computeEntitiesOutsideLayouttests each entity's bounding mesh against the layout, so it does catch oversized geometry there. For a trustworthy budget either way,launch_previewand useexplorer_get_scene_content_stats. -
[FIXED] Publishing under Bevy used to discard parcel-layout changes. Because the data layer runs in the sdk-commands realm under Bevy, the
scene.jsonwrite never passed through the Hub's storage RPC, so the Hub kept the layout it loaded the project with and publishing wrote that stale layout back — a scene resized in Scene settings published as its original (often 1x1) layout. Fixed in creator-hubee43befd0(Oct 2026): the inspector'snotify_scene_metadatareport now carriesparcelsandbase.get_project_inforeadsscene.jsonfrom disk and was never affected, so it is not a workaround and never showed the symptom. Mentioned only so an older Hub build's publish surprise is recognisable. -
Never save a custom thumbnail as
assets/Images/autogenerated-thumbnail.png. The Hub captures the viewport into that one fixed path, overwriting whatever file is there, every time it auto-generates a thumbnail — when a publish starts and when it refreshes the project card (getProjectThumbnailPath/saveThumbnailinpackages/creator-hub/preload/src/modules/workspace.ts), so a cover written there is replaced right before the upload. It only rewritesscene.json'sdisplay.navmapThumbnailin three cases (saveThumbnail,workspace.ts): the entry is unset, the file it points at is missing, or it names the autogenerated path with different casing. A miscased custom cover is left alone — macOS and Windows resolve it, so it only fails at deploy. That is why the overwrite is silent. Save custom covers under a distinct name (e.g.assets/Images/cover.png) and repoint the scene at it. -
Image assets live in
assets/Images/with a capital I, and the casing is load-bearing at deploy time. The deploy file list keeps the on-disk casing and the Catalyst matchesdisplay.navmapThumbnailagainst it case-sensitively, so a scene.json sayingassets/images/...while the folder on disk isassets/Images/publishes withScene thumbnail 'assets/images/autogenerated-thumbnail.png' must be a file included in the deployment. Nothing local notices: macOS and Windows resolve either casing, so the scene looks fine until the upload. The Hub now writes its autogenerated thumbnail toassets/Images/by default —resolveImagesFolderNamekeeps an existing folder that differs only in case, so a project that already hasassets/images/stays lowercase — and rewrites a miscased autogeneratednavmapThumbnailentry on the next thumbnail save (creator-hub9d82aac07) — but anything you write by hand must match the folder that is actually there. Check withls assetsbefore setting the path, and never create a second folder that differs only in case.set_scene_settingscannot (nothumbnailkey in its input schema; the MCP SDK strips unknown keys before the editor sees them), butset_componentcan:entity_detail(0)to read the root entity's scene-metadata component, thenset_component(0, "inspector::SceneMetadata-v5", {thumbnail: "assets/Images/cover.png"})— keys merge as in any update, andscene.json'sdisplay.navmapThumbnailfollows on the next autosave (verified live; 106 parcels and the spawn points survived). Take the exact versioned name from that read instead of hardcoding-v5: the editor binds to whichever version carries data (resolveActiveSceneComponent), so a scene on an older pinnedsdk-commandsis still on-v4, and writing the wrong version just adds a component nobody reads. The editor's Scene settings → Thumbnail field is the equivalent UI route. -
entity_detail(0)on a large scene is enormous — 3,335 lines / 79 KB on a 106-parcel scene, mostlyinspector::Nodesplus the parcel list, which is big enough to overflow a tool result (observed 2026-10-10). Redirect it to a file andgrepout only the component you need. And becauseset_componentmerges top-level keys, write back only the key you changed — never the whole component you just read. -
A thumbnail you generate (Blender render, image tool, screenshot) has to meet the publish spec before you point the scene at it. Publishing to a World rejects any thumbnail over 1,048,576 bytes —
Invalid thumbnail: size 1185064 bytes exceeds maximum of 1048576 bytes (1MB), hit with an ordinary 1920x1080 PNG. That check is not in creator-hub (nothing in the repo matches the message or the constant); the Worlds content server enforces it at deploy time, so neither the editor nor a local build warns you first. The editor validates the rest:.png/.jpgand 16:9 within a 1 % tolerance, recommended 1920x1080 (getThumbnailWarnings/validateThumbnailFile,packages/inspector/src/components/EntityInspector/SceneInspector/utils.ts; the Hub's own captures are centre-cropped to 16:9 and capped at 1920 wide inpackages/creator-hub/renderer/src/modules/image.ts). -
So: render 16:9 (1920x1080, or 1344x756 for a lighter file), export JPEG at high quality rather than PNG —
ffmpeg -i cover.png -q:v 2 cover.jpgcame out around 100 KB at 1344x756 — and keep a PNG only if it actually stays under the cap. Check the byte size (ls -l) and the dimensions (sips -g pixelWidth -g pixelHeight) before writing the path into the scene, not after a failed publish. Save it under a distinct name and repoint the scene as above — neverassets/Images/autogenerated-thumbnail.png. -
Every thumbnail must survive a square crop: put text, logo and main subject inside the central square. Parts of the platform show only the centre 1080x1080 of a 1920x1080 image, cutting
(1920 - 1080) / (2 * 1920) ≈ 0.219of the width off each side (THUMBNAIL_SAFE_AREA_INSET). That's x 420–1500 at 1920 wide and x 280–1000 at 1280 wide. This applies whether you capture, render or compose the image. Before pointing the scene at it, crop the square and look at the result. Don't judge it from the full frame: a title that runs edge to edge looks fine there (Locksmith, 2026-10).OUT="${TMPDIR:-/tmp}/cover-square.png" # macOS H=$(sips -g pixelHeight cover.png | awk '/pixelHeight/{print $2}'); sips -c $H $H cover.png --out "$OUT" # ImageMagick (any OS) H=$(magick identify -format %h cover.png); magick cover.png -gravity center -crop ${H}x${H}+0+0 +repage "$OUT"Then
Readthe square image. Every word of the text must be in it. -
No
node/npxon PATH is not a dead end. The Creator Hub ships a complete Node (v22, withnpmandnpx) and runs everysdk-commandschild on it (main/src/modules/node-runtime.ts,scripts/download-node.js): macOS/Applications/Decentraland Creator Hub.app/Contents/Resources/node-bin/bin/{node,npm,npx}, Windowsresources\node-bin\node.exe(plusnpm.cmd/npx.cmd) in theresourcesfolder next to the Creator Hub executable, Linuxresources/node-bin/bin/. Call it by absolute path fornpx tsc --noEmit,npx skills add decentraland/sdk-skills --all,gltf-transform, and the like on a creator's machine that has no Node of its own. (Development builds of the Hub carry no bundle and use the developer's Node.) -
asset-packs::Placeholdersrcneeds a resolved file path, not a template variable. The catalog stores paths with{assetPath}/model.glbas a template; setting that string literally viaset_componentproduces an invisible or broken gizmo because the engine cannot resolve the variable.place_smart_itemresolves the path automatically (e.g. toassets/asset-packs/spotlight/spotlight.glb). If you must setPlaceholdermanually, use the real on-disk path to the GLB file.