Repository navigation
Conversation
…e scene store divide_zone over the hosted MCP crashed with "Cannot read properties of undefined (reading 'getState')" (Hawkesbury run 2): the store's history step wrapped a host's runtime too, and the hosted server's store has no history. Main now applies a host's runtime without that step; this case keeps it so. Slice: S1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- add_wall, add_level, create_stair, place_items, create_room, furnish_room, search_assets and the fourteen room and floor tools (divide_zone, merge_zones, set_floor_foundation, create_mezzanine…) each have one contract and one core operation, registered on the MCP and defined by the chat from the same object. create_wall, create_level, place_item, create_stair_between_levels and cut_opening leave the MCP: add_wall, add_level, place_items, create_stair, add_door and add_window replace them. create_roof keeps its own definitions for now. - An operation that reads construction the host derives (re-derived rooms, auto ceilings, floor plates) returns afterReconcile; both hosts run it through applyAgentOutcome with their own reconciler, in one undo step. - A mutating shared tool says what it achieved: what the scene holds after the call, against a copy taken before it, so a host that writes its node map in place no longer reads every creation as unchanged. - add_object requires a reason, what the object stands in for, checked before its script runs; verify_scene lists authored objects with their reason. A plain box with a wall's size, or a floor plate, is refused. - The MCP takes the host's item library for search_assets, place_items and furnish_room; a standalone server keeps the built-in list. A search with no match points to add_object. - No listed input schema holds a tuple, so Claude Code keeps every tool (it had dropped nine room and floor tools), and shared tools declare loose outputs, so a field core adds no longer fails a client that listed them. Slice: S1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d opening) A cased opening or an arched passage, as the editor's door panel offers: add_door takes openingKind 'opening' and builds a door node with no leaf, framed or arched by its outline. Slice: S1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in its way A build that deletes plan-only floors, then copies a floor up, pushed every floor above with each copy, gap included, so a model spent calls on reorder_level. A copy now fills the free floor; without a gap nothing changes, in the editor's Duplicate too. Slice: S1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run 3 (2026-10-05) wrote its front door's three glass strips off as "not possible": both tools take code, a script for what the fields cannot express, and their descriptions stopped at outline, type and style. They now say that glass strips, a pattern of lites or carved panels are written as a script in code, never left as not possible. Slice: S1 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 923f64005ae8abc2a05977ed34ead326eec2d0c3)
Working parity, from the MCP ↔ chat runner's first report: - add_door and add_window are one core operation, addWallOpening, and one result the contract declares: ok, the id, wallId, localX, t, wallLength, clamped, coordinateSystem, sillHeight, message and achieved. The MCP's `position` copy of t is gone; the input keeps it. A rebuilt opening answers the same, rebuiltOpeningResult. - find_by_type refuses a level that is not there (level_not_found), as get_zones does; it answered "nothing found". - add_level on a scene with no building starts it as the editor does: a site (unless one stands), a building, its ground level. An agent that cleared the scene could not begin again with the tools. - create_project saves the session's scene as the project's first draft: from any other session it was scene_not_found until a save. - Every write says where it went, top-level: project, and `unsaved` with how to keep it on a scratch scene no project holds (writeTarget). A result's own `note` stays the operation's. Shared-table cases for find_by_type and the fresh start; the MCP's tests move from `position` to t and achieved. Slice: S1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run 3 took five attempts to paint a pier grey through patches:
`material: {color}` reported applied and stored {} (the schemas drop
unknown keys), `materialPreset: null` was refused (no way to clear), the
preset hid the material set under it, and an unknown preset rendered grey
with no word.
honestNodePatch, on apply_patch's updates: a path the node would drop or
change is refused naming it (unknown_field: material.color, where a colour
goes in material.properties.color), a material the library lacks is refused
naming the nearest of the target's kind (unknown_material), a material a
set preset hides is refused naming the preset (shadowed_field), and null
clears a field (field_required when the field is required). Free-form
records (metadata, slots) pass as sent.
Slice: S1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What this PR doesEvery agent tool now has one definition. Each tool is declared once in Riding along with the move: mutating tools answer with
Good place to start: the two registries, |
S1's rename of place_item into place_items placed on a level's floor only. As main's place_item did, an item may now name its host (targetNodeId), its position in level coordinates as main took it: - a wall: along it from its start, y the height of the item's bottom (required), on the side of the wall the point is on (the front faces its left; the back is turned round), centred in the wall or, for a wall-side fixture, on its face; kept inside the span and the height, as the editor's wall placement keeps it; wallId, wallT and side set; - a ceiling: under it, its top flush; - an item standing on a floor: in its frame. An object built with add_object takes it on its real surface under the point, or hangs a ceiling item from its underside above it (a sloped vault), and the result names the part (restingOn); a catalog item, on its top; - a room, a slab or a level: its floor. Each item is refused on its own: height_required, item_too_tall, unsupported_host (a door), host_not_found, host_not_on_level (an item on an item). An id the library lacks stays refused (asset_not_found) rather than placed as main's 0.5 m placeholder. The surface logic is the chat's place_on_surface's, moved into core. Slice: S1 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tions S1's create_stair dropped what main's create_stair_between_levels set: - railingMode (none, left, right, both; default both); - materialPreset, on the flight and its segment, refused when the library lacks it, naming the flooring it has; a stair takes no paint, so the colour it points to is a flat library one (library:preset-*). The lab's requireMaterialRef checks it, as apply_patch and update_node do: upstream's unknownMaterialPresetRefusal rides on its node-slots module, which the lab does not have yet; the two meet when the lab merges upstream; - name (default Staircase N); - the opening controls, under main's names: createDestinationSlabOpening, createSourceCeilingOpening, destinationSlabId, sourceCeilingId, openingWidth, openingLength, openingOffset, openingCenter and openingRotation (in degrees, as the flight's rotation). With none of them, the stair cuts its own opening as the editor's stair tool does (openingOffset is its margin, 0.08 m by default; both cuts off cuts none). With any, the opening is cut as given, as main cuts it: a floor opening owned by the stair, with the pose the live opening systems move it by, and the stair's own opening off so nothing is cut twice. Main centred that opening at z + length / 2 whatever the flight's turn; here it follows the climb, so a turned flight's opening stays over it. slab_not_found and ceiling_not_found refuse ids that name no slab or ceiling. Slice: S1 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…read alike A Zod tuple renders as prefixItems in the chat's JSON schema and as a flattened array on the MCP, so agent-surface parity found the two input schemas apart. openingCenter is a length-2 number array, as every other contract writes an (x, z) point. Slice: S1 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pascal-3d workflows said place_items places on a level's floor only; it now takes a host per item (targetNodeId), as main's place_item did. Slice: S1 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… default requireMaterialRef names a flat library colour (library:preset-*) unless its caller asks for paint, as paint does: honest patches and create_stair then point to no tool a slice may lack, and create_stair needs no flag. Slice: S1 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… it empty 2026-10-03, a Claude Code run on the hosted MCP: create_project in a session that had another project loaded returned that project's levels, and the next save would have written it into the new project. The session is now bound to the new project first, then, when it was bound to another one, its scene is emptied to the default site, building and level, with no undo back to the old one. Work not bound to any project is kept: it is what the new project is for. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… was not loaded from 2026-10-03, a Claude Code run on the hosted MCP: the server reloaded, the session started over on a blank scene, and save_scene(projectId) wrote it over the project's draft (8 levels, 10 imported plans, the calibration and the alignment, no history). save_scene now refuses scene_not_loaded when the session's scene was not loaded from (or created for) the target project or scene and that one holds more than the default site, building and level; replace: true writes over it on purpose. Live sync already writes only to the scene a session loaded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Run 4's first save_scene, right after create_project bound the session to its project, was refused "projectId is required for Supabase store. Call create_project first." With no id and no projectId, save_scene now saves the current scene to the scene this session is bound to (create_project, load_scene or an earlier save); with nothing bound, it saves as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A floor copy is hundreds of ids the model never uses; written into the prompt cache once, they are read back on every call after. The result keeps the counts per type in `copied`, lists the first 40 ids and says how many it left out (`newNodeIdsOmitted`), which the MCP declares. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since owned floor openings, a stair's opening is a floor-opening node on the floor above, owned by the stair. The check already looks for it; these cases pin it: an owned opening on the floor above counts, one drawn on the stair's own ceiling does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every write gives an `achieved` (what the scene holds after the call), but the MCP's add_object returned none; the chat's add_object did. The headless parity gate could not see it, its chat lane unreachable on that row; parity with an editor tab showed it. The MCP now counts the change against a copy of the scene before the call, as its shared tools and the chat do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run 4 (L66): the agent made the porch an outdoor room, so the house wall behind it had a room on both faces and knew no outside. The front door faced the hall, view_scene showed its outside from inside, and the porch face took the inside finish. A room is outdoor when it has no ceiling and can be left without crossing a wall, through a separator no room stands behind or into another such room, found as a fixed point. A wall face on an outdoor room is exterior, so door facing, view_scene, facades and finishes follow on both paths. A room that only lost its ceiling, a kitchen open to a terrace, and a courtyard walled on every side stay inside; a carport (no ceiling, an open side) is outside. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ywhere L59 replaced "blocks door clearance" with the item, its size and the door it is in the way of; the MCP test and the clearance log still read the old text, and the old reason entry was left unused. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r too large for its room L59, run 4: furnish_room skipped the catalog's 2.34 m bathtub as blocking the bath's door; the agent then set it there itself with place_items, which checked only that its centre stood in a room, and verify_scene ended on two blocked doors. A floor item is now refused on its own when the room it stands in holds it in no turn (too_large_for_room, with both sizes, pointing to a smaller one or add_object at the room's size), or when it stands in the space a door needs, as verify_scene's blocked-door check sees it (blocks_door, naming the door and a spot in the room that clears every door, when there is one). Overlaps are not refused: a chair under its table and a bed on its rug overlap by design. Items on a host keep their own checks. furnish_room's skips name the item's size, the door in the way, or that the room is too small and what instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tant (L68) The store's new-scene level wrote a literal 2.5. It now writes DEFAULT_LEVEL_HEIGHT, as the hosted MCP's default scene and a new project's empty scene do, so the three cannot drift apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test asked for one roofing material by its id's prefix, which is one library's naming, not every library's. It now asserts roofing first as the library tags it (surfaces include roof), which every library with roofing passes; which roofing that is stays the data's business. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s what they protect
apply_patch held the honest-update check inline. A patch guard now
registers from its own module, run by order; apply_patch runs what is
registered and passes an McpError through as it is. The honest-update check
is the first, from honest-patch-guard.ts, and answers as it did:
{ code, patchIndex, id, message }.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A door's swing and a sectional door's track run behind its facing; add_door always faced the wall's front, so a garage drawn with its front inside ran its overhead track on the street, and a front door behind a porch faced the hall. add_door now faces a door out when the wall knows its outside (resolveWallExteriorSide): rotation π and side 'back' when the outside is the back. Walls that do not know their outside, and inside walls, keep the front as before. One rule for agents and people: the editor's door tool places a door on an outside wall facing out whichever face is hovered (placedDoorFace); R still flips it. The shared opening cases pin both wall directions and the inside wall. Doors made in the same call as their walls are not covered: their walls learn their sides only afterwards. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The outdoor-sides cases came with checks of a facade fill and a view's framing from the porch, tools this branch does not have. The porch case now checks what it owns: the wall is exterior on the porch face, and add_door places a door that faces the porch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ir winders) into the shared tools foundation Where main and this branch did the same thing, main's version stands: - measure_stair and fit_stair (pascalorg#1000) are shared tools next to create_stair: one stairs contract module and both operations registered. - verify_scene keeps main's informational issues (hasIssues ignores severity 'info') with this branch's authored objects and checks. - create_stair_between_levels stays replaced by create_stair; main's sizing of a new flight from the design targets (planStairCreation) is carried into create_stair in the next commit. - The tool lists (annotations, README, the skill) gain main's two tools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main sizes a new flight from the stair's design targets and resolves its rise against what it stands on and arrives at (planStairCreation, pascalorg#1000). create_stair now builds its flight the same way: the run and the risers come from the design targets unless given, and the stair keeps uniform risers and its flight's step count. main's two sizing cases now run on create_stair. The ring of a stair's owned opening may start at another corner once main re-plans it after a move; the case compares the ring's extent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into the foundation Both sides kept: the foundation's furnish_room export and asset catalog, main's optional hosted service tools, registered before the resources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… labels it, a Separator Divide, merge, delete and an outdoor room's outline now say "a separator (the editor's Separator: a room boundary with no wall)", so an agent describes what the person finds in the Scene panel instead of an "open divider", the door and window panels' word for the bars between panes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment-only: each comment keeps the failure that motivated the code and drops which session found it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object named after something Pascal builds is built with a hint naming the tool for it. Four of those hints named tools this server does not register (create_reference_elements, create_stairs_and_lifts, correct_plan_reading, apply_facade), so an agent following one called a tool that fails. They now name add_wall, create_room with its floor tools, add_door / add_window, create_stair and create_roof; lifts and balconies, which have no tool here, get no hint. A test checks every tool a hint names against tools/list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… falls in With opening controls, create_stair reported the storey's first slab and first ceiling as the surfaces it cut, and took any slab upstairs as proof a hole was cut there. On a floor of several rooms that named the wrong room's slab, and an opening under no slab was still reported cut. It now picks the slab above and the ceiling below that hold the opening's centre; a slab or ceiling id given still wins. main's create_stair_between_levels picked the first surface the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… above is cut
slabHoleCut counted every opening the flight cut, so a stair cutting only the
ceiling below reported a hole in the slab above, and an agent took the upper
floor for open when it was not. It is now true only for an opening drawn on
the floor above, as the chat's create_stair meant it on main ("auto floor
opening").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ject A session's scene with no projectId was taken for another project, so create_project emptied the session and saved the new project empty: what the session held beyond that scene's last save was dropped. Only a session bound to a different project starts the new one empty now; work bound to no project, a saved scene of none included, becomes the new project's first draft, as main's create_project carried it. On the hosted store every scene is a project, so that path is unchanged there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…her storey A destinationSlabId or sourceCeilingId was checked only for its type, so a slab on another floor was taken as the surface opened while the hole was cut on the floor the flight arrives at, and returned as the surface cut. A slab id must now be on the arrival floor and a ceiling id on the departure floor, or the call is refused (slab_not_on_level / ceiling_not_on_level) before anything is built. main's create_stair_between_levels checked the type alone too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ac1b35d. Configure here.
| const host = wallAt(scene, level.id, a, b, lerp(a, b, t)) | ||
| if (!host) return skip('no_wall', `No wall runs along edge ${wallIndex} at t ${t}.`) | ||
| try { | ||
| const planned = planWallOpening(scene, { kind, ...spec, wallId: host.wallId, t: host.t }) |
There was a problem hiding this comment.
New-room doors ignore exterior facing
Medium Severity
create_room places declared doors through planWallOpening on the post-createZone graph, before the host reconciles wall frontSide/backSide. doorFacing then sees unknown sides and keeps the wall's front, so a door on a newly minted outside wall is not turned out. afterReconcile only reads slab and ceiling ids and never revisits those doors, so the baked side and rotation stay wrong after space detection runs.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit ac1b35d. Configure here.


Reviewer guide: the shared tools foundation
Frozen for testing (2026-10-06): this PR's content is fixed. Any later foundation commit goes to a follow-up PR (
slice/s1b-…, stacked on this one), not here. The foundation and its s1b follow-up merge together.Merge order: this PR first, then pascalorg/private-editor#843, which points its submodule here.
On upstream main (2026-10-06): this branch merges main twice: 0f18783 (#915 fences, #1000 stairs) in 478bdeb, then a860e19 (#996, optional hosted service tools) in bb6dd80. Its diff against main is only the foundation. #996 merged as a union: main's optional service tools register beside the foundation's asset catalog. Where both solved the same thing, main's version wins:
create_stairsizes its flight with main'splanStairCreation, as the editor's stair tool does (risers of at most 18 cm, a run from the design targets).measure_stairandfit_stairsit beside it in the shared contracts.create_stair_between_levelsstays removed: its options live oncreate_stair.read-tool-annotations.test.tsstays dropped.What changes for a user (an agent on the MCP or in the chat):
create_wall→add_wall,create_level→add_level,place_item→place_items,create_stair_between_levels→create_stair.cut_openingis gone:add_door/add_windowcover it, andadd_doorwithopeningKind: 'opening'makes a passage with no leaf.save_scenenever writes a session's scene over a project it wasn't loaded from; with noidorprojectIdit saves to the project the session is bound to.apply_patchwithmaterial.colornamesmaterial.properties.color; it also refuses an unknown material, naming the nearest, and a material a preset hides.place_itemshangs items on walls, ceilings and other items, andcreate_stairkeeps the railing, preset, name and opening options of the tool it replaces.add_doorturns it out, and the editor's door tool places it facing out whichever face is hovered (R still flips it). A garage door's track runs inside, a front door opens into the hall.place_itemsrefuses a floor item standing where a door needs space (blocks_door, naming the door and a spot that clears every door) or that its room can't hold in any turn (too_large_for_room, with both sizes). Overlaps stay allowed: a chair under its table.How to test:
bun install && bun run cion this branch.Known flaky on main: the
qualitycheck can fail on one timing test, core'sspace-detection.test.ts"stays near-linear on 2,000 isolated walls" (a ratio under 40). Main failed it today too (run 37464791592, 40.3); here it read 47.8, then 41.8, and passes locally. This PR doesn't touch space detection.Manual parity (every agent capability this PR adds or brings to a new surface, and how a person does the same in the editor):
divide_zonemerge_zonesmove_zone,rotate_zone,duplicate_zonedelete_zoneset_zone_intentlock_outside_facescut_floor_openingcutsPrimarynot settableremove_floor_openingset_floor_foundationset_room_floor_constructionrebase_floor_referencecreate_mezzanineadd_doorwithopeningKind: 'opening'add_doorfacing out on an outside wallplace_itemson a wall, a ceiling, an itemplace_itemsrefusing a floor item in a door's way or too large for its roomcreate_stairoptionsduplicate_levelfilling a freed flooradd_object(a scripted object, with a reason)Missing or partial before this PR:
rebase_floor_reference,cut_floor_opening's shaft,create_stair's opening controls andadd_objectalready existed on main's MCP; this PR brings them to the chat too. New in this PR: theplace_itemsdoor and room-fit refusals. Decided (2026-10-06): theplace_itemsdoor and room-fit check gets its manual half in the s1b follow-up that ships with this PR: the item tool warns "blocks the door" or "too large for the room" while placing, from the same core check. The four that predate this PR ship as they are; their manual halves are tracked as follow-ups: a way to set a floor plate's reference elevation by hand (rebase_floor_reference), creating a scripted object by hand (add_object), a multi-level shaft and a level-only opening in Cut opening (cut_floor_opening), and the stair's opening size, position and floor-or-ceiling choice in the stair panel (create_stair).Not in this pack: the creations that run the editor's own fixes, the roof tool's single definition (roofs come later), and the agent's vision (its own pack). Don't look for them here.
What to read (behaviour):
packages/mcp/src/tools/scene-lifecycle/save-scene.ts(save_scenenever writes a session's scene over a project it wasn't loaded from, and saves to the session's project when given no target) andcreate-project.ts(a session bound to another project starts the new one empty, and saves its first draft).packages/core/src/agent-operations/node-patch.ts,material-refs.ts,packages/mcp/src/tools/apply-patch.ts,patch-guards.ts.packages/core/src/agent-operations/place-items.ts,agent-tools/place-items.ts.packages/core/src/agent-operations/create-stair.ts,agent-tools/stairs.ts.packages/core/src/building/level-duplication.ts.agent-operations/add-level.ts: an empty scene starts a site and a building;agent-operations/find-by-type.ts: a missing level is refused;packages/mcp/src/tools/scene-lifecycle/create-project.ts: the first draft is saved;agent-operations/add-object.ts: a reason is required;agent-tools/wall-openings.tsandbuilding/wall-openings.ts:openingKind.What to skim (tools moved onto one definition, registered by the MCP and the chat):
packages/core/src/agent-tools/{walls,levels,stairs,place-items,create-room,furnish-room,assets,room-structure,verify-scene}.ts.packages/core/src/agent-operations/{add-wall,add-level,create-room,furnish-room,room-structure,search-assets,wall-opening}.ts.packages/mcp/src/tools/shared-tools.ts;room-tools.ts,structure-tools.tsandconstruction-tools.ts;create-wall.ts,create-level.ts,place-item.tsandcut-opening.ts.create_walladd_wallcreate_leveladd_levelplace_itemplace_items(withtargetNodeIdfor walls, ceilings, items)create_stair_between_levelscreate_stair(same options)cut_openingadd_door/add_window(openingKind: 'opening'for a passage)What to ignore (plumbing and tests):
achieved.ts,apply-outcome.ts,scene-measure.ts,types.ts,agent-tools/write-target.tsandrefusal.ts;packages/mcp/src/tools/normalize-schema-dialect.ts, the fix for tuples that Claude Code dropped;contracts/reference-inventory.ts,plugin-evals/,scripts/openai-tool-annotation-policy*;__fixtures__/and*.test.ts;wiki/,skills/, the MCP README and examples;What does this PR do?
One contract per agent tool: each tool is defined once in
@pascal-app/core/agent-tools, runs one core operation, and is registered by the MCP and by the hosted chat from the same object.add_wall,add_level,create_stair,place_items,create_room,furnish_room,search_assetsand the fourteen room and floor tools (divide_zone,merge_zones,move_zone,rotate_zone,duplicate_zone,delete_zone,set_zone_intent,lock_outside_faces,cut_floor_opening,remove_floor_opening,set_floor_foundation,set_room_floor_construction,rebase_floor_reference,create_mezzanine).create_roofkeeps its MCP definition for now.create_wall→add_wall,create_level→add_level,place_item→place_items,create_stair_between_levels→create_stair, andcut_openingis removed (add_door/add_window).achieved(created, updated, deleted, read against a copy taken before the call) and where the write went (writeTarget).add_doorandadd_windowreturn one declared result.add_objectrequires areason: what the object stands in for, checked before its script runs.verify_scenelists authored objects with their reason, and a plain wall-sized box or floor plate is refused (use_walls,use_slab).apply_patchrefuses an update the node would drop or change (unknown_field), a material the library lacks, naming the nearest of the target's kind (unknown_material), and a material a set preset hides (shadowed_field).nullclears a field.find_by_typerefuses a missing level;add_levelon an empty scene starts a site, building and level;create_projectsaves its first draft;duplicate_levellists at most 40 new ids and counts the rest (newNodeIdsOmitted): a floor copy is hundreds of ids the model reads back on every call;save_scenewith noidorprojectIdsaves to the project the session is bound to, where it was refused "Call create_project first".add_doortakesopeningKind: 'opening'for a passage with no leaf;apply_patch's checks register from the module that owns what they protect (patch-guards.ts,honest-patch-guard.ts); the honest-update check answers as before;add_window's say that glass strips and carved panels go incode;search_assets,place_itemsandfurnish_room.No schema change, so old scenes load as before.
place_itemstakes atargetNodeIdper item: a wall face (withy, the height of its bottom), a ceiling (hung flush), another item or an authored object, or a room, slab or level floor. Per-item refusals:height_required,item_too_tall,unsupported_host,host_not_found,host_not_on_level. An unknown catalog id is refused (asset_not_found), whereplace_itemplaced a 0.5 m placeholder.create_stairtakescreate_stair_between_levels'railingMode,materialPreset,nameand every opening control, under the same names. With none given, it cuts the editor tool's own opening. With any given, the opening follows the climb (main centred it at z + length/2 even on a turned flight). New refusals:slab_not_foundandceiling_not_found.library:preset-…).Draft: it stays a draft until the slice is verified (live checks and your QA).
How to test
bun install && bun run ci: lint, skills validation, type checks, tests and build pass.bun run --cwd packages/mcp build) and connect a client tonode packages/mcp/dist/bin/pascal-mcp.js.tools/listlistsadd_wall,add_level,place_itemsandcreate_stairand all fourteen room and floor tools, and lists none ofcreate_wall,create_level,place_item,create_stair_between_levelsorcut_opening.divide_zoneon it: two rooms, one undo step. Callset_floor_foundation: the slab rises.add_wall {start:[0,0], end:[5,0]}returnsachieved: { created: { wall: 1 } }.add_objectwithoutreasonreturnsreason_requiredand nothing is compiled.add_doorwithopeningKind: 'opening'on a wall builds a door node with no leaf.6b.
place_itemswith a wall astargetNodeIdand ayhangs the item on the face; with an unknown asset id it returnsasset_not_found.create_stairwithrailingMode: 'none'and anopeningWidthbuilds a stair without railings, owning an opening of that width.apply_patchupdating a door withmaterial: { color: '#343b3c' }returnsunknown_fieldnamingmaterial.color;{ material: { properties: { color: '#343b3c' } } }applies.place_itemsa 2.3 m bathtub into a 1.5 m bathroom: refusedtoo_large_for_roomwith both sizes. Place a cabinet in front of the bathroom door: refusedblocks_door, naming the door and a spot that clears it.Screenshots / screen recording
N/A: no visual change. Duplicate level's new placement is a behaviour; step 8 shows it.
Checklist
bun devbun checkto verify)mainbranch🤖 Generated with Claude Code
Note
High Risk
Breaking MCP tool names and broad changes to scene mutation, placement, rooms, stairs, and patch validation affect all agent surfaces; regressions could corrupt scenes or mislead agents with wrong results.
Overview
One contract and one core operation per agent tool, so MCP and chat run the same logic. MCP clients get breaking renames (
add_wall,add_level,place_items,create_stair;cut_openingremoved) plus room/floor tools,create_room,furnish_room, andsearch_assetswired throughAGENT_OPERATIONS.New and tightened write behavior:
add_objectrequires areason, refuses plain wall-sized boxes and floor plates, and stores hints for Pascal-native types;place_itemsandfurnish_roomshare catalog rules, hosted placement, and door/room-fit refusals;create_roomplaces openings on the correct level and reconciles derived slab/ceiling ids;create_stairowns floor openings with the old between-levels options;add_levelcan bootstrap an empty scene;duplicate_levelfills freed floor indices and caps returnednewNodeIds.achievedChanges,honestNodePatch, andrequireMaterialRefkeep results honest;applySceneChanges/mergeSceneChangesandapplyAgentOutcome(+afterReconcile) match store ordering and derived structure.Tests: large declarative
__fixtures__tables per tool (with optionalcheckcallbacks andcatalogin context) driveoperations.test.tsand dedicated suites likeadd-object.test.ts.Reviewed by Cursor Bugbot for commit ac1b35d. Bugbot is set up for automated code reviews on this repo. Configure here.