Skip to content

core, mcp: the shared tools foundation, one definition per agent tool - #1007

Open
AxiomeCG wants to merge 37 commits into
pascalorg:mainfrom
AxiomeCG:slice/s1-one-contract
Open

AxiomeCG wants to merge 37 commits into
pascalorg:mainfrom
AxiomeCG:slice/s1-one-contract

Conversation

@AxiomeCG

@AxiomeCG AxiomeCG commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

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_stair sizes its flight with main's planStairCreation, as the editor's stair tool does (risers of at most 18 cm, a run from the design targets).
  • Main's measure_stair and fit_stair sit beside it in the shared contracts.
  • create_stair_between_levels stays removed: its options live on create_stair.
  • The tool-count line main dropped from read-tool-annotations.test.ts stays dropped.

What changes for a user (an agent on the MCP or in the chat):

  • The MCP's write tools take the chat's names: create_wall → add_wall, create_level → add_level, place_item → place_items, create_stair_between_levels → create_stair. cut_opening is gone: add_door / add_window cover it, and add_door with openingKind: 'opening' makes a passage with no leaf.
  • save_scene never writes a session's scene over a project it wasn't loaded from; with no id or projectId it saves to the project the session is bound to.
  • A write that would silently do nothing is refused, naming the field. For example, apply_patch with material.color names material.properties.color; it also refuses an unknown material, naming the nearest, and a material a preset hides.
  • place_items hangs items on walls, ceilings and other items, and create_stair keeps the railing, preset, name and opening options of the tool it replaces.
  • Duplicate level (an agent's, and the editor's Duplicate) fills a freed floor instead of pushing every floor up.
  • A wall behind a porch or a terrace (a room with no ceiling that opens to the outside) is exterior on that face: the front door faces the porch, and finishes and facades follow. A kitchen open to a terrace, or a courtyard walled on every side, stays inside.
  • A door on an outside wall faces out, for agents and by hand: add_door turns 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_items refuses 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.
  • A new level is born with the storey height (2.5 m): in the editor's store, the hosted MCP's default scene and a new project.
  • Divide, merge, delete and an outdoor room's outline name a room boundary with no wall as the editor labels it: "a separator (the editor's Separator: a room boundary with no wall)". The agent then says what the person finds in the Scene panel, not "an open divider".

How to test:

  1. bun install && bun run ci on this branch.
  2. Build and connect the MCP (packages/mcp/README.md), then follow steps 2–8 of "How to test" below.
  3. With pascalorg/private-editor#843, follow its QA in the chat.

Known flaky on main: the quality check can fail on one timing test, core's space-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):

Agent By hand
divide_zone Room pill Divide room ✓
merge_zones Select the separator between two rooms, Delete ✓
move_zone, rotate_zone, duplicate_zone Room pill Move, Rotate, Duplicate (by hand: 45° steps about the room) ✓
delete_zone Room pill, room panel or site tree trash, Delete key ✓
set_zone_intent Room panel (name, built-on, floor height, floor/ceiling on-off) and paint (finishes, regions, wall material) ✓
lock_outside_faces Room panel Keep outside dimensions (one room at a time) ✓
cut_floor_opening Room panel Cut opening partial: no multi-level shaft, no level-only rectangle, cutsPrimary not settable
remove_floor_opening Floor-opening panel delete ✓
set_floor_foundation Base plate panel (thickness, on the ground / raised, foundation height and finish) ✓
set_room_floor_construction Room panel floor thickness, floor-height drag ✓
rebase_floor_reference — missing (no editor path writes the plate's reference elevation)
create_mezzanine Room panel + Add mezzanine ✓
add_door with openingKind: 'opening' Door panel Type: Door / Opening / Garage ✓
add_door facing out on an outside wall The door tool places it facing out whichever face is hovered (R flips) ✓
place_items on a wall, a ceiling, an item The item tool's wall, ceiling and on-item placement ✓
place_items refusing a floor item in a door's way or too large for its room — missing: the editor checks collisions only, no door or room-fit check
create_stair options Stair panel: railing, auto cutout, opening offset; rename in the tree; paint partial: opening width/length/centre/rotation, cutting only one of floor/ceiling, and the destination slab are agent-only
duplicate_level filling a freed floor Site panel or level selector Duplicate level (same builder) ✓ (copying below, and naming at creation, are agent-only)
add_object (a scripted object, with a reason) — missing to create one by hand; an existing one's parameters are editable

Missing or partial before this PR: rebase_floor_reference, cut_floor_opening's shaft, create_stair's opening controls and add_object already existed on main's MCP; this PR brings them to the chat too. New in this PR: the place_items door and room-fit refusals. Decided (2026-10-06): the place_items door 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):

  • Sessions: packages/mcp/src/tools/scene-lifecycle/save-scene.ts (save_scene never writes a session's scene over a project it wasn't loaded from, and saves to the session's project when given no target) and create-project.ts (a session bound to another project starts the new one empty, and saves its first draft).
  • Honest patches: packages/core/src/agent-operations/node-patch.ts, material-refs.ts, packages/mcp/src/tools/apply-patch.ts, patch-guards.ts.
  • Item hosts: packages/core/src/agent-operations/place-items.ts, agent-tools/place-items.ts.
  • Stair options: packages/core/src/agent-operations/create-stair.ts, agent-tools/stairs.ts.
  • Freed floor: packages/core/src/building/level-duplication.ts.
  • Small fixes:
    • 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.ts and building/wall-openings.ts: openingKind.

What to skim (tools moved onto one definition, registered by the MCP and the chat):

  • Contracts: packages/core/src/agent-tools/{walls,levels,stairs,place-items,create-room,furnish-room,assets,room-structure,verify-scene}.ts.
  • Operations moved out of the MCP: packages/core/src/agent-operations/{add-wall,add-level,create-room,furnish-room,room-structure,search-assets,wall-opening}.ts.
  • Registration:
    • packages/mcp/src/tools/shared-tools.ts;
    • the shrunken room-tools.ts, structure-tools.ts and construction-tools.ts;
    • the deleted create-wall.ts, create-level.ts, place-item.ts and cut-opening.ts.
Before (MCP) Now (MCP and chat)
create_wall add_wall
create_level add_level
place_item place_items (with targetNodeId for walls, ceilings, items)
create_stair_between_levels create_stair (same options)
cut_opening add_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.ts and refusal.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*;
  • every __fixtures__/ and *.test.ts;
  • wiki/, skills/, the MCP README and examples;
  • 714c4ea, comment-only: comments keep the failure that motivated the code, without internal run names, codes or dates.

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.

  • Shared tools: 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, 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_roof keeps its MCP definition for now.
  • Breaking for MCP clients: create_wall → add_wall, create_level → add_level, place_item → place_items, create_stair_between_levels → create_stair, and cut_opening is removed (add_door / add_window).
  • Writes say what happened: a mutating shared tool returns achieved (created, updated, deleted, read against a copy taken before the call) and where the write went (writeTarget). add_door and add_window return one declared result.
  • add_object requires a reason: what the object stands in for, checked before its script runs. verify_scene lists authored objects with their reason, and a plain wall-sized box or floor plate is refused (use_walls, use_slab).
  • Honest patches: apply_patch refuses 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). null clears a field.
  • Fixes:
    • the room and floor tools run without the editor's scene store (a regression test; main already fixed the crash);
    • no listed input schema holds a tuple, so Claude Code keeps every tool;
    • find_by_type refuses a missing level;
    • add_level on an empty scene starts a site, building and level;
    • create_project saves its first draft;
    • Duplicate level (agent and editor) fills a freed floor instead of pushing every floor up;
    • duplicate_level lists 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_scene with no id or projectId saves to the project the session is bound to, where it was refused "Call create_project first".
  • Also:
    • add_door takes openingKind: '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;
    • its description and add_window's say that glass strips and carved panels go in code;
    • the MCP takes the host's item library for search_assets, place_items and furnish_room.

No schema change, so old scenes load as before.

  • Nothing the old tools did is lost:
    • place_items takes a targetNodeId per item: a wall face (with y, 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), where place_item placed a 0.5 m placeholder.
    • create_stair takes create_stair_between_levels' railingMode, materialPreset, name and 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_found and ceiling_not_found.
  • Refusals name no tool S1 doesn't ship: an unknown material points to a flat colour (library:preset-…).

Draft: it stays a draft until the slice is verified (live checks and your QA).

How to test

  1. bun install && bun run ci: lint, skills validation, type checks, tests and build pass.
  2. Build the MCP (bun run --cwd packages/mcp build) and connect a client to node packages/mcp/dist/bin/pascal-mcp.js. tools/list lists add_wall, add_level, place_items and create_stair and all fourteen room and floor tools, and lists none of create_wall, create_level, place_item, create_stair_between_levels or cut_opening.
  3. Draw a room and call divide_zone on it: two rooms, one undo step. Call set_floor_foundation: the slab rises.
  4. add_wall {start:[0,0], end:[5,0]} returns achieved: { created: { wall: 1 } }.
  5. add_object without reason returns reason_required and nothing is compiled.
  6. add_door with openingKind: 'opening' on a wall builds a door node with no leaf.
    6b. place_items with a wall as targetNodeId and a y hangs the item on the face; with an unknown asset id it returns asset_not_found. create_stair with railingMode: 'none' and an openingWidth builds a stair without railings, owning an opening of that width.
  7. apply_patch updating a door with material: { color: '#343b3c' } returns unknown_field naming material.color; { material: { properties: { color: '#343b3c' } } } applies.
  8. In the editor, with three levels, delete the middle one and Duplicate the ground floor: the copy takes the freed floor and the top level keeps its index.
  9. Build a room and, in front of it, a porch room with no ceiling that opens to the outside; add a door on the wall between them: its outside faces the porch, and the porch side of that wall takes the outside finish.
  10. place_items a 2.3 m bathtub into a 1.5 m bathroom: refused too_large_for_room with both sizes. Place a cabinet in front of the bathroom door: refused blocks_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

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

🤖 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_opening removed) plus room/floor tools, create_room, furnish_room, and search_assets wired through AGENT_OPERATIONS.

New and tightened write behavior: add_object requires a reason, refuses plain wall-sized boxes and floor plates, and stores hints for Pascal-native types; place_items and furnish_room share catalog rules, hosted placement, and door/room-fit refusals; create_room places openings on the correct level and reconciles derived slab/ceiling ids; create_stair owns floor openings with the old between-levels options; add_level can bootstrap an empty scene; duplicate_level fills freed floor indices and caps returned newNodeIds. achievedChanges, honestNodePatch, and requireMaterialRef keep results honest; applySceneChanges / mergeSceneChanges and applyAgentOutcome (+ afterReconcile) match store ordering and derived structure.

Tests: large declarative __fixtures__ tables per tool (with optional check callbacks and catalog in context) drive operations.test.ts and dedicated suites like add-object.test.ts.

Reviewed by Cursor Bugbot for commit ac1b35d. Bugbot is set up for automated code reviews on this repo. Configure here.

AxiomeCG and others added 7 commits October 6, 2026 17:19
…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>
@pascal

pascal Bot commented Oct 6, 2026

Copy link
Copy Markdown

What this PR does

Every agent tool now has one definition. Each tool is declared once in @pascal-app/core/agent-tools and paired with one operation in agent-operations, and the MCP registers from that list instead of keeping its own implementations: room-tools.ts loses 694 lines, structure-tools.ts 415, construction-tools.ts 299, and four MCP-only tools (create-wall, create-level, cut-opening, place-item) are deleted outright. For MCP clients that is a breaking rename to add_wall, add_level, place_items and create_stair, with cut_opening replaced by add_door / add_window.

Riding along with the move: mutating tools answer with achieved counts and where the write went, add_object requires a reason and checks it before the script compiles, wall-sized boxes and floor plates are refused in favour of walls and slabs, and apply_patch refuses an update the node would drop, shadow, or not find in the material library. Smaller fixes: duplicating a level fills a freed floor index, single-type tuples are rewritten so clients reading the 2020-12 dialect keep every tool, create_project saves its first draft, and find_by_type refuses a missing level. 116 files, +6,146 / -2,620.

File Change What changed
packages/core/src/agent-tools/index.ts modified New contracts exported and listed in AGENT_TOOL_CONTRACTS
packages/core/src/agent-tools/walls.ts added add_wall contract
packages/core/src/agent-tools/levels.ts modified add_level contract; levelTarget exported, verify_scene moved out
packages/core/src/agent-tools/place-items.ts added place_items contract, up to 64 items per call
packages/core/src/agent-tools/stairs.ts added create_stair contract
packages/core/src/agent-tools/create-room.ts added create_room contract, doors and windows declared per polygon edge
packages/core/src/agent-tools/furnish-room.ts added furnish_room contract
packages/core/src/agent-tools/assets.ts added search_assets contract
packages/core/src/agent-tools/room-structure.ts added The fourteen room and floor tool contracts
packages/core/src/agent-tools/verify-scene.ts added verify_scene contract, now reporting authored objects
packages/core/src/agent-tools/wall-openings.ts modified add_door takes openingKind: 'opening'; descriptions send glass strips and carved panels to code
packages/core/src/agent-tools/add-object.ts modified reason is part of the contract
packages/core/src/agent-tools/{achieved,write-target}.ts added Shared achieved output shape and writeTarget with the "not saved to any project" note
packages/core/src/agent-operations/index.ts modified New operations registered in AGENT_OPERATIONS
packages/core/src/agent-operations/{add-wall,add-level,create-stair,place-items}.ts added The operations behind the renamed tools; add_level starts site, building and level on an empty scene
packages/core/src/agent-operations/create-room.ts added One wall per edge, reuse of a wall already running there, declared openings
packages/core/src/agent-operations/furnish-room.ts added Placement with door clear zones, item collisions, every skip stated
packages/core/src/agent-operations/room-structure.ts added The fourteen room and floor operations, with no editor scene store
packages/core/src/agent-operations/search-assets.ts added Search over the catalog the host supplies
packages/core/src/agent-operations/achieved.ts added Created, updated and deleted counted against a copy taken before the call
packages/core/src/agent-operations/apply-outcome.ts added Applies an outcome through a host, including the reconcile passes
packages/core/src/agent-operations/apply-changes.ts modified Updates, then creates, then deletes, matching the store's order
packages/core/src/agent-operations/node-patch.ts added Patch honesty: unknown_field, shadowed_field, field_required, null clears
packages/core/src/agent-operations/material-refs.ts added unknown_material, naming the nearest material for the target's kind
packages/core/src/agent-operations/scene-measure.ts added Registry of families a scene measure counts per floor
packages/core/src/agent-operations/wall-opening.ts added One door and window placement path for every surface
packages/core/src/agent-operations/add-object.ts modified Reason required before the script runs; use_walls / use_slab refusals
packages/core/src/agent-operations/verify-scene.ts modified Lists authored objects with their reason
packages/core/src/agent-operations/find-by-type.ts modified Refuses a level that is not there instead of answering empty
packages/core/src/agent-operations/types.ts modified AgentContext gains the host catalog and checkpoint
packages/core/src/building/level-duplication.ts modified A copy fills a freed floor index instead of pushing every floor up
packages/core/src/building/wall-openings.ts modified openingKind carried into the door node
packages/core/src/systems/wall/wall-topology.ts modified WALL_MIN_LENGTH exported
packages/core/src/contracts/reference-inventory.ts modified Metadata inventory: reason described, stale MCP keys dropped
packages/mcp/src/tools/index.ts modified Registration switches to the shared tools; catalog and script hosts passed as one object
packages/mcp/src/tools/shared-tools.ts modified Registers each shared contract, adding achieved, live-sync and catalog-backed calls
packages/mcp/src/tools/room-tools.ts modified 694 lines of room and floor logic removed, now core
packages/mcp/src/tools/structure-tools.ts modified Same for the structure tools; annotations and output shape stay here
packages/mcp/src/tools/construction-tools.ts modified Construction tool bodies moved to core
packages/mcp/src/tools/{create-wall,create-level,cut-opening,place-item}.ts and their tests removed Replaced by the shared tools (8 files)
packages/mcp/src/tools/apply-patch.ts modified Runs the core patch check
packages/mcp/src/tools/patch-guards.ts modified New refusal codes for dishonest updates
packages/mcp/src/tools/asset-catalog.ts modified AssetCatalog the host provides, read once, built-in list as the fallback
packages/mcp/src/server.ts modified Server option for the host's item library
packages/mcp/src/resources/{index,catalog-items}.ts modified Resources read the same catalog (2 files)
packages/mcp/src/tools/live-sync.ts modified Results carry the project a write reached, or the unsaved note
packages/mcp/src/tools/normalize-schema-dialect.ts modified Single-type tuples rewritten as sized arrays for 2020-12 clients
packages/mcp/src/tools/scene-lifecycle/create-project.ts modified Saves a first draft so another session can load the scene
packages/mcp/src/tools/{redo,undo}.ts modified Report the current live-sync state when nothing moved (2 files)
34 test and fixture files added / modified Case tables for the shared tools, plus regressions: room tools without the editor store, duplicate level, patch honesty, tool annotations
20 remaining files modified MCP entry re-export, README, examples, prompts and agent guide, the two skills and two wiki pages, tool-annotation policy and justifications, all on the new tool names

Good place to start: the two registries, packages/core/src/agent-tools/index.ts and packages/core/src/agent-operations/index.ts, then packages/mcp/src/tools/shared-tools.ts to see how the MCP turns a contract into a registered tool. Per the description, the branch is still a draft while place_items hosting and the create_stair options land.

AxiomeCG and others added 5 commits October 6, 2026 18:24
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>
@AxiomeCG AxiomeCG changed the title core, mcp: one contract per agent tool, shared with the hosted chat core, mcp: the shared tools foundation, one definition per agent tool Oct 6, 2026
AxiomeCG and others added 6 commits October 6, 2026 19:03
… 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>
AxiomeCG and others added 9 commits October 6, 2026 20:59
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>
AxiomeCG and others added 3 commits October 6, 2026 21:34
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>
AxiomeCG and others added 2 commits October 6, 2026 22:11
… 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>
@AxiomeCG
AxiomeCG marked this pull request as ready for review October 6, 2026 20:24

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/agent-operations/create-stair.ts
Comment thread packages/core/src/agent-operations/add-object.ts
AxiomeCG and others added 2 commits October 6, 2026 22:40
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>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/agent-operations/create-stair.ts Outdated
Comment thread packages/mcp/src/tools/shared-tools.ts
… 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>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/agent-operations/create-stair.ts
Comment thread packages/mcp/src/tools/scene-lifecycle/create-project.ts
AxiomeCG and others added 2 commits October 6, 2026 23:32
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ac1b35d. Configure here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant