Drive the HyperLap2D 2D scene editor from an MCP client (create/modify entities & components, list assets, screenshot, scene settings, shaders, z-ordering, layers). Use whenever the running editor's MCP server is connected and the task is to build or inspect a HyperLap2D scene.
Resources
14Install
npx skillscat add rednblackgames/hyperlap2d Install via the SkillsCat registry.
HyperLap2D MCP Server — Tool Guide
The HyperLap2D editor exposes an MCP server (Streamable HTTP) that lets an external client drive it: create and modify entities and components, list assets, capture screenshots, change scenes and scene settings, create shaders, control z-ordering, and manage layers. This skill documents every tool, the editor's coordinate/layer model, and the edge cases that bite when authoring a scene blind.
Prerequisites & connection
- The editor is running (
./gradlew runHyperLap2D) with a project open and a scene loaded. - Settings → Plugins → "MCP Server" is enabled (and the port set). The server starts/stops live on toggle.
- The MCP client connects to
http://127.0.0.1:<port>/mcp(Streamable HTTP). Configure it like:{ "mcpServers": { "hyperlap2d": { "url": "http://127.0.0.1:8765/mcp" } } } - If a tool returns "no scene loaded" / "no current scene", open a project and scene in the editor first. If
list_*tools return empty, no project/scene is loaded.
All tool calls run on the editor's render thread via an internal bridge; they are synchronous with a timeout (≈5s, 15s for screenshots). Keep the editor open while calling.
Core model (read this before placing anything)
- World units, not pixels. Entity
x,y,width,heightare in world units. One world unit =pixelsPerWUpixels. A value like300can be enormous or tiny depending onpixelsPerWU— always readpixelsPerWUfirst (get_scene_settingsorget_asset_dimensions) before choosing coordinates. - Asset world size = pixel size / pixelsPerWU. Use
get_asset_dimensionsto get each region's pixel + world width/height so you can place tiles the right distance apart. - Never guess an asset's appearance from its name. Use
get_asset_previewto actually see the source PNG before placing it. Names likewalls3orplant1tell you nothing reliable about content or scale. - Coordinate origin. The editor scene camera is y-down (origin top-left, +y downward). Entity
(x, y)is the entity's position in its parent composite's local space, never in absolute scene space. - Layers + z-index. Every entity belongs to a layer (named, e.g.
Default) and has a z-index that is local to that layer. Lower z-index draws behind, higher draws in front. The runtime auto-adjusts z-indices into a linear progression, so setting an absolute integer is the intended usage (set_z_index). Read current order fromlist_entities(zIndex+layerfields). Layers are per-composite (each composite — including the scene root — has its own layer list) and layer names are case-sensitive in the editor (stored by hash; the default layer isDefault). The layer tools resolve names case-insensitively, so"default"matchesDefault— but when creating/renaming you pass the exact name you want. - Composites — everything lives in one. A composite is a container entity that holds children. The scene root is itself a composite, so every entity has a parent composite and every
(x, y)is local to that parent. A child's coordinates are an offset from its composite's origin — not a scene-absolute position. Creating a child at(5.5, 0)inside a composite places it 5.5 units from that composite's origin, wherever the composite sits. - The first child of a new composite lands at the composite's origin — this is correct, not a bug. With
automaticResize(the default), the composite has no size until it has children; the first child you add defines the origin of the local space, so it sits at local(0, 0)regardless of the coordinates you pass. This is the whole point of a composite. Build the group so its first/anchor child is the one meant to be at the origin, then add the rest relative to it, then position the composite (see Building a composite correctly). Never treat the first-child-at-origin as something to "fix" by deleting/recreating — you'll only scramble the group. - Lights belong to the object they light, not to a "lights" group. A point/cone light is placed next to the object it illuminates, inside that object's composite/hierarchy — a lamp light goes in the lamp's composite at the lamp's position, a glow light goes on the glowing prop. Do not collect all lights into a separate
lightscomposite or treat lighting as a render layer; that is not how HyperLap2D lighting works. Light position is meaningful and must track its object, so keep them together (moving the object then moves its light). - Validation is sacred. Component add/edit goes through the same properties panels the UI uses. Invalid values or disallowed components are rejected with the same messages the panels produce. Do not try to "work around" a rejected value — it's rejected because the editor considers it invalid.
Golden rules for scene authoring
get_scene_settings→ notepixelsPerWU.list_assets→ know what's available.get_asset_dimensions→ learn each region's world size.get_asset_preview→ see the assets you plan to use.create_entity/create_entitiesat correct world coordinates (computed from pixelsPerWU and asset world sizes).set_z_indexto order layers/depth.screenshot(modewholeorregion) to let the user verify.
Do not place entities blindly; do not invent asset semantics. The user judges all visual results — your job is correct, validated structure.
Tool reference
28 tools. Grouped by purpose.
Discovery (read-only)
list_scenes
No args. Returns [{ "name": ... }] — all scenes in the open project.
list_entities
No args. Returns a flat array of every entity in the current scene:{ uniqueId, parentId, name, typeId, type, depth, zIndex, layer }
uniqueId— the stable id you pass to all mutation tools.parentId— null for direct children of the scene root; otherwise the parent composite's uniqueId. Use withdepthto rebuild the tree.type— display name (covers core + Spine/Talos/TinyVG).zIndex,layer— the per-layer z-index and layer name (mirror ofset_z_index).
list_assets
No args. Returns loaded project assets grouped by category:imageRegion, ninePatchRegion, spineAnimation, spriteAnimation, particleEffect, talosEffect, font, tinyvg, shader.
Use region names for create_entity (image/9patch) and shader names for update_component/update_scene_settings.
get_asset_dimensions
No args. Returns { pixelsPerWU, regions: [{ name, pixelWidth, pixelHeight, worldWidth, worldHeight, ninePatch }] } for all image/nine-patch regions. worldWidth = pixelWidth / pixelsPerWU. Use to plan placement spacing.
get_asset_preview
Args: name (required), folder (optional, default <projectPath>/assets/orig/images). Returns the source <name>.png as MCP image content plus text with pixelWidth/pixelHeight/path. Use to see an asset before placing it. If the PNG isn't in the default folder, pass folder to point elsewhere.
get_scene_settings
No args. Returns { sceneName, pixelsPerWU, shader, physicsEnabled, gravityX, gravityY, sleepVelocity, lightsEnabled, pseudo3d, blurNum, lightMapScale, lightType, directionalRays, directionalDegree, directionalHeight, ambientColor, directionalColor }. Colors are [r,g,b,a] lists.
get_editable_components
Args: entityId. Returns { editable: [...], addable: [...] } — the componentKeys whose panel currently applies to that entity (for update_component) and the components it can still have added (for add_component). Use this when unsure what an entity accepts; it's the single source of truth shared with the validation path.
list_actions
No args. Returns the library action-graph names in the project (the node-graph actions attachable to entities). (Actions are out of scope for creation via MCP — listed only.)
Mutation (validated, undoable unless noted)
create_entity
Args: type (required), name, x, y, width, height, fontFamily, fontSize, lightType, parentUniqueId, layer.
type:image | spriteAnimation | spineAnimation | libraryItem | 9patch | tinyvg | particle | talos | primitive | composite | label | light.- Name-based types (
image,spriteAnimation,spineAnimation,libraryItem,9patch,tinyvg,particle,talos) requirenamefromlist_assets. primitive:width/height(default 100) define the rect at creation.label: optionalfontFamily(must be a loaded font) /fontSize(default 20).light:lightTypePOINT(default) orCONE.parentUniqueId: create inside that composite;x/ythen become local to the composite (offsets from its origin, not scene-absolute — see Edge cases → Building a composite correctly, and lay children out around(0,0), not at their intended world positions). The composite must be a direct child of the current viewing entity. To target a nested composite, enter its container in the editor first.layer: create on a named layer (resolved case-insensitively; must exist on the effective parent — seelist_layers). When omitted, the entity uses the currently selected layer.- Returns the new entity's
uniqueId. Creation is undoable (Ctrl+Z).
create_entities (bulk)
Args: entities (array of specs with the same fields as create_entity, including parentUniqueId and layer). Creates each sequentially via the same validated/undoable path. Returns a JSON array of { ok, uniqueId, error } in input order. Use for placing many tiles/objects at once instead of many create_entity round-trips. Some entries can fail without aborting the rest.
update_transform
Args: entityId, fields (object). Fields: x, y, width, height, scaleX, scaleY, rotation, flipX, flipY, id, tint. Pass only what you want to change. Drives the basic properties panel (validated, undoable).
width/heightare editable only for label, composite, and 9-patch entities. For images/sprites/spine/etc. size comes from the asset — usescaleX/scaleY. For primitives, see Edge cases (width/height are not the size; the polygon vertices are).
update_component
Args: entityId, componentKey, fields (object). Drives that component's properties panel (validated, undoable). Pass only the fields to change. componentKey selects the panel and the accepted fields:
| componentKey | applies to | fields |
|---|---|---|
basic |
all entities | x, y, width, height, scaleX, scaleY, rotation, flipX, flipY, id, tint |
label |
label entities | text, fontFamily, bitmapFont, fontSize, align, wrap, mono |
particle |
particle effects | matrixTransform, autoStart |
image |
images | renderMode, spriteType |
composite |
composites | scissorsEnabled, renderToFBO, automaticResize |
sprite |
sprite animations | fps, animation, playMode |
spine |
spine animations | animation, skin |
talos |
talos effects | matrixTransform, autoStart |
lightItem |
light entities | type, rayCount, radius, angle, distance, direction, intensity, height, softnessLength, falloff, isStatic, isXRay, isSoft, isActive |
shader |
entities with a Shader component | shaderName, renderingLayer |
physics |
entities with Physics | mass, density, friction, restitution, damping, angularDamping, gravityScale, height, centerOfMassX, centerOfMassY, rotationalInertia, bodyType, shapeType, allowSleep, awake, bullet, sensor, fineBoundBox, fixedRotation |
light |
entities with a Light component | rays, distance, intensity, constantFalloff, linearFalloff, quadraticFalloff, softnessLength, height, direction, isStatic, isXRay, isSoft, isActive, color |
circle |
entities with Circle Shape | radius |
polygon |
entities with Polygon Shape | verticesCount, openPath |
sensor |
entities with Sensor | sensorTop/Bottom/Left/Right, spanPercentTop/Bottom/Left/Right, heightPercentTop/Bottom, widthPercentLeft/Right |
layout |
entities with Layout | leftMargin/rightMargin/bottomMargin/topMargin, horizontalBias/verticalBias, leftEnabled/rightEnabled/bottomEnabled/topEnabled, leftTarget/rightTarget/bottomTarget/topTarget, leftSide/rightSide/bottomSide/topSide, matchWidth/matchHeight |
For shape/physics/light/etc. components, the entity must already have that component — add_component it first (or check get_editable_components).
add_component
Args: entityId, componentKey. componentKey ∈ Polygon Shape | Circle Shape | Physics | Physics Sensors | Shader | Light | Typing Label | Layout. Allowed set is enforced editor-side (Light/Shader can't be added to light entities; Typing Label only to labels; already-present rejected). Undoable.
remove_component
Args: entityId, componentKey (same keys). Only the removable components above. Undoable.
set_z_index
Args: entityId, zIndex (integer). Sets the entity's z-index to an absolute value local to its layer. Lower draws behind, higher in front; the runtime auto-adjusts z-indices into a linear progression. Undoable (Ctrl+Z). Read the result back via list_entities (zIndex, layer).
delete_entity
Args: entityId. Selects then runs the standard delete command. Undoable.
delete_entities (bulk)
Args: entityIds (array of strings). Deletes each via the standard path. Returns [{ entityId, ok, error }] in input order. Some can fail (e.g. not found) without aborting the rest.
open_scene
Args: sceneName (from list_scenes). Routes through the editor's unsaved-changes check (may prompt/save in the UI), then loads the scene. Timeout 30s; if the user cancels a save dialog the request times out.
update_scene_settings
Args: fields (object). Drives the scene properties panel (validated, undoable). Fields: physicsEnabled, gravityX, gravityY, sleepVelocity, lightsEnabled, pseudo3d, blurNum, lightMapScale, lightType (DIFFUSE/DIRECTIONAL/BRIGHT), directionalRays, directionalDegree, directionalHeight (only when lightType=DIRECTIONAL), ambientColor, directionalColor, shader. Pass only what you want to change.
create_shader
Args: name (required, unique), templateType (0=Default Array, 1=Distance Field, 2=Screen Reading), optional vertex + fragment (custom GLSL, overrides template). Writes .vert/.frag, compile-checks first (a failed compile leaves no files), registers the shader. Then apply it with update_component (shaderName) on an entity, or update_scene_settings (shader) for the scene shader.
save_project
No args. Saves the current project (all scenes and assets) to disk.
Layers
Layers belong to the current viewing entity (the scene root, or the composite currently being edited) — they are per-composite. Layer tools resolve layer names case-insensitively (so "default" matches Default), but the editor stores names case-sensitively; create_layer/rename_layer apply the name you pass exactly. create/delete/rename/set_layer_order/set_entity_layer all run through the editor's undoable layer commands.
list_layers
No args. Returns [{ name, index, isVisible, isLocked }] for the current viewing entity's layers, in editor order (index 0 = back-most layer). Use to learn exact layer names before referencing them.
create_layer
Args: layerName. Creates a new layer on the current viewing entity; layerName is used as-is (case-sensitive). Rejected if a same-cased or case-insensitive match already exists. Undoable.
delete_layer
Args: layerName (matched case-insensitively). Deletes the layer. WARNING: also deletes every entity on that layer. Undoable.
rename_layer
Args: layerName (existing, matched case-insensitively), newName (applied as-is, case-sensitive, must be unique). Undoable.
set_layer_order
Args: layerName (matched case-insensitively), direction (up | down). Moves the layer one step in the stack (swaps with its neighbor). Lower index draws behind, higher draws in front. Undoable.
set_entity_layer
Args: entityId, layerName. Moves an entity onto a different layer. The destination layer must exist on the entity's parent composite (so list_layers while viewing that parent). layerName matched case-insensitively. The entity lands at the front of the destination layer; use set_z_index to fine-tune its position within the layer. Undoable.
Visual
screenshot
Args: mode (whole | region, default whole); for region also x, y, width, height (world units). Returns a PNG as MCP image content.
whole— all entities regardless of viewport (recommended for verifying a scene). Frames the union of the root's direct children's bounding boxes.region— a world-space rectangle (x,yis the corner; good for inspecting a specific area).- Screenshots currently do not render lights (known limitation).
- Huge scenes/regions are capped to the GPU's max texture size per side, preserving aspect.
Edge cases & gotchas
Primitives: width/height are NOT the size
A color primitive's visual size is its polygon vertices (PolygonShapeComponent), not DimensionsComponent.width/height — those are a derived bounding box. Consequences:
update_transformrefuseswidth/heighton primitives (the basic panel disables them for primitives, images, sprites, etc. — only label/composite/9-patch allow width/height). This is by design, not a bug.- At creation time,
create_entitywithtype: "primitive"does acceptwidth/heightand builds the rect polygon from them (PolygonShapeVO.createRect(width, height)). - To resize an existing primitive, use
scaleX/scaleY(viaupdate_transform), or edit the polygon shape (update_componentwithcomponentKey: "polygon"controlsverticesCount/openPath, not direct coordinates). For an exact new rectangle size, recreate the primitive.
pixelsPerWU dominates coordinate choice
x, y, width, height are world units. With pixelsPerWU = 100, an entity at x = 300 is 300 world units from origin — possibly far off-screen if the scene is small. Always read pixelsPerWU (get_scene_settings / get_asset_dimensions) first, and compute placement from asset world dimensions (worldWidth = pixelWidth / pixelsPerWU).
Colors
Color fields (tint, color, ambientColor, directionalColor) accept any of:
"r,g,b,a"(e.g."1,0,0,1")- a JSON array
[r,g,b,a] "#RRGGBBAA"hex (e.g."#FF0000FF")
Label align is case-insensitive
update_component (label, align) matches the allowed names case-insensitively: "top left", "Center", "BOTTOM RIGHT" all work. Allowed values: Top Left, Top, Top Right, Left, Center, Right, Bottom Left, Bottom, Bottom Right.
Layers are per-composite and (mostly) case-sensitive
Each composite — including the scene root — has its own layer list; a layer named Default at the root is a different list from one inside a composite. To move an entity to a layer, that layer must exist on the entity's parent (call list_layers while viewing the parent). Layer names are stored case-sensitively (by hash), so Default ≠ default ≠ DEFAULT as far as the editor is concerned — but the layer tools resolve names case-insensitively so you won't trip on casing when referencing an existing layer. create_layer/rename_layer use the exact string you pass, so decide the casing deliberately. delete_layer deletes the layer and every entity on it.
Building a composite correctly (READ THIS — the #1 mistake)
A composite has its own local coordinate space, and by default automaticResize = true: the composite's size and origin are recomputed from its children. You do not know a composite's final position/size until its children exist. The first child anchors the local origin. This dictates the workflow.
The correct sequence — build the group in local space, then place the whole group:
- Create the composite (
create_entitytypecomposite) at(0, 0). Its position is provisional — you set the real one in step 4. - Add the anchor child first. The first child defines the origin: it lands at local
(0, 0)no matter whatx/yyou pass. So make the first child the object you want at the group's origin (e.g. the bottom-left tile of a wall, the base of a structure). Passing(0,0)for it is honest and clearest. - Add the rest with
parentUniqueId, in LOCAL coordinates relative to that anchor. A child at local(2, 1)is 2 units right / 1 down from the anchor. Design the group as its own little scene whose origin is the anchor. Pick the anchor so the other locals are simple (usually the group's bottom-left, giving all-positive locals). - Position the composite in its parent. Now
automaticResizehas computed the composite's size.update_transformthe composite's(x, y)to drop the finished group where it belongs. Everything inside moves rigidly — you place the container once, never the children.
Concrete example — a wall row that belongs at world y=5: create composite; add wall segments at local (0,0), (1,0), (2,0), … (all y=0, the row's baseline); then update_transform the composite to (0, 5). Result: the row sits at world y=5. Do not instead give the segments y=5 and leave the composite at (0,0) — that puts world coordinates into local space and makes the composite's position meaningless.
Two failure modes to never repeat:
- World coords as local coords + composite left at origin. Semantically wrong even when it happens to render: the composite's position no longer means anything, and the group can't be moved as a unit.
- "Fixing" the first child (or any child) after the fact. The first child sitting at the origin is correct — it defines the local space. Deleting/recreating it, or
update_transform-ing a single child in anautomaticResizecomposite, re-derives the bounding box and scrambles the group. Build it right in one pass; don't post-patch individual children.
Before concluding a child is misplaced or missing, check z-index between composites (see Z-index below). Sibling composites draw in composite-creation order — an entire composite (and everything in it) can be hidden behind another composite. A "missing" object is usually a z-order problem, not a position problem. Verify with a region screenshot of where it should be before touching anything.
Do not disable automaticResize. Leaving it on (the default) is how composites are meant to work — it is what lets the container size itself to its children so you can then position it. Turning it off is only for rare special effects, never as a workaround for a placement you got wrong.
Nesting / other rules:
- The parent must be a direct child of the current viewing entity (normally the scene root). The editor's enter-composite flow only supports entering one level at a time. If you get "parent composite must be a direct child of the current viewing entity; double-click its container in the editor to enter it first", enter the outer composite in the editor UI, then target the inner composite's uniqueId.
- Passing a non-composite uniqueId (an entity with no children slot) errors with "parent is not a composite/container".
- The enter/exit happens within one render frame (no visible camera jump) and is not pushed to the undo stack — only the entity creation is undoable.
Z-index is per-layer and absolute
set_z_index sets an absolute integer scoped to the entity's layer. The runtime re-linearizes z-indices, so don't try to compute "relative" offsets — just set the order you want (e.g. background=0, mid=5, foreground=10). Retrieve current values from list_entities.
Sibling composites draw in their own z-order, and it carries their whole subtree. Among children of the same parent (including the top-level composites under the root), z-index follows creation order by default — created earlier = drawn behind. So a composite created before another draws entirely behind it, and so does every child inside it. Consequence: an object placed at exactly the right coordinates can be completely hidden because its composite sits behind a later one (e.g. props created before path → the rune circle in props is covered by the path tiles). When something "isn't there," suspect z-order first: check the composites' zIndex in list_entities, and set_z_index the composite (not the child) to bring the whole group forward/back. Create composites in back-to-front order (background first), or fix the order afterward with set_z_index. Do not go hunting for a "wrong position" or delete/recreate anything until you've ruled out z-order.
Component edits are panel-validated
Every add_component / update_component / remove_component / update_transform / update_scene_settings drives the real UI panel off-stage and runs its VisUI validators. If a value is rejected, the error message is the panel's. Use get_editable_components to discover what's allowed for a given entity before editing. Never assume a field exists for a type.
Bulk operations
create_entities and delete_entities process entries sequentially and return per-entry results; a failure in one entry does not abort the others. Check the returned array for partial failures.
Undoability
create_entity/create_entities, update_transform, update_component, add_component, remove_component, set_z_index, delete_entity/delete_entities, update_scene_settings, create_layer, delete_layer, rename_layer, set_layer_order, set_entity_layer are all undoable in the editor (Ctrl+Z). Composite enter/exit for creation is intentionally not on the undo stack. create_shader writes resource files (not a scene edit).
Visual verification is the user's job
Never judge a screenshot as "looks good" on the user's behalf. Capture the screenshot, hand it over, and let the user decide. Your responsibility is correct, validated structure and correct coordinates/scale.
Recommended workflow: design a scene from scratch
open_scene(or confirm the current scene) →get_scene_settings(notepixelsPerWU).list_assets→get_asset_dimensions(world sizes) →get_asset_previewfor the regions you'll use (see them).- Plan placement in world units using
worldWidth/worldHeight(tile spacing = region world size; positions relative to origin). Group related entities into composites (e.g.ground,path,wall,props) — it keeps the scene tree navigable and lets you order/move whole groups at once. Create composites back-to-front (background group first) so their creation-order z-index already matches the draw order you want. - For each group: create the composite; add the anchor child first (it lands at the origin), then
create_entities(bulk) withparentUniqueIdand the rest in local coordinates relative to that anchor; thenupdate_transformthe composite to its final position (see Building a composite correctly). Capture the returneduniqueIds. - If any group is hidden or draws in the wrong order,
set_z_indexthe composite (not its children) to reorder whole groups background→foreground. Rule out z-order before ever suspecting a wrong position. - Add components as needed:
get_editable_components→add_component→update_component. Place each light inside the composite/hierarchy of the object it illuminates, at that object's position — never in a standalone lights group. screenshot(modewhole) → return it to the user for visual verification.- Iterate based on user feedback;
save_projectwhen satisfied.
If something is rejected, read the error — it reflects the editor's real validation. Don't retry the same invalid value. And never delete/recreate or post-update_transform a composite's children to "fix" placement — build each group right in one pass, then move only the composite.