The API
One JSON document describes a space. Every tool is a pure
function over it: nothing is stored here, and no call remembers a previous
one, so you carry the document from call to call and the same document
always gives the same answer.
This page is the reference. Setup is how you connect
a client to it; the front page is what it is for and when not
to use it. Every number below is read off the engine that serves this page,
so it cannot promise a limit the endpoint does not enforce.
The endpoint
Endpoint: POST /v1 all eighteen tools
POST /v1/core build, check, draw and export to DXF
POST /v1/elevation elevations and components
POST /v1/advisory the reads that suggest and never decide
Protocol: MCP, revision 2026-07-28 (Streamable HTTP). Clients on the
initialize-era revisions — 2025-11-25, 2025-06-18, 2025-03-26,
2024-11-05 — are served too: open with `initialize` and name
your revision in MCP-Protocol-Version, and none of 2026-07-28's
mirrored headers or _meta fields are asked of you.
A profile is the same server with a shorter tool list. It is a courtesy to
a client with a crowded context, never a permission: a key minted against one
reaches exactly the tools it names.
| /v1/core | edit_layout, list_operations, validate_layout, render_layout, draw_sheet, export_dxf, list_fixtures |
| /v1/elevation | project_elevation, list_components, project_section, suggest_sections, join_drawings |
| /v1/advisory | suggest_joins, suggest_rotation, check_rules, missing_dimensions, overhead_report, roof_report |
The tools
Eighteen, coarse and intention-shaped rather than one call per
object. edit_layout takes a batch of operations and answers with
the document and its findings; the advisory reads answer a question and
change nothing.
edit_layout | Apply operations to a layout document and get the updated document back. |
list_operations | Every operation edit_layout accepts, grouped by what it acts on. |
validate_layout | Typed, permanent error codes naming which object offended: overlapping objects, an opening too near a corner, a boundary that crosses itself. |
render_layout | The layout as SVG. |
project_elevation | The elevation of one side of the building, PROJECTED from the plan rather than drawn, which is why it cannot disagree with it. |
project_section | A section is the same projection as an elevation with the picture plane moved INSIDE the building. |
suggest_sections | Candidate cut lines with the evidence for choosing between them — how many walls each would cut, how many INTERNAL walls (counted apart: one says how much of the envelope the cut crosses, the other how many rooms it goes through), how many openings it would pass through as voids, which overhead members it would catch, and how much it would reveal beyond. |
join_drawings | A house is storeys, and a section is across all of them. |
draw_sheet | ONE sheet, TRUE TO SCALE: the SVG declares real paper millimetres and a viewBox of paper × scale, so a rule laid on the print agrees with the drawing rather than with whatever size a browser chose. |
export_dxf | The drawings draw_sheet puts on paper, as ONE DXF for CAD — AutoCAD, Vectorworks, Revit, a CNC bed. |
roof_report | A roof is OVER-DETERMINED: span, eaves, ridge and pitch are four numbers with one equation between them, so the plan plus any TWO of the heights fixes it. |
list_components | The component types an elevation can hang on an opening, with their defaults. |
list_fixtures | What can stand in a room and be drawn as what it is: kitchen units and appliances, sanitaryware, a stair, beds, sofas, wardrobes — each at a typical trade size, with its plan symbol, its front and side, and its SECTION profile through either axis, from one description. |
suggest_joins | Objects close enough and aligned enough to be joined, with the evidence for each. |
suggest_rotation | Answers twice over, because the two answers usually disagree: the angle with the most clearance around it, and the angle that squares it with its neighbours and the room's own walls. |
check_rules | Rules are YOUR parameters, checked by a generic engine. |
overhead_report | Every member ABOVE the plan's cut plane — a beam over, today — and whether its heights are known. |
missing_dimensions | Given a layout and the dimensions actually MEASURED on site, says whether the shape is determined by those readings, and if not, which readings to go and take. |
The document
Integer millimetres, and the origin at the bottom left
Every persisted coordinate, dimension and offset is a whole number of
millimetres. Not metres, not pixels, not floats. Rounding happens once, at
the boundary, on the way in, because floating-point drift in a layout that
round-trips through an API is a wall that no longer closes some months
later. The origin is bottom left and y is up, which is the surveying
convention and the one the person holding the tape is thinking in. SVG is
y-down, so the flip happens exactly once, at render, and never reaches
stored data.
The room is a closed ring
The boundary is a ring of shared vertices: segment i runs from
vertex i to vertex i+1. There is no closure tolerance and
no way to express an open boundary, because the structure has nowhere to
put a gap. Six independently-typed sides almost never close; a ring cannot
fail to.
A segment is straight or a single circular arc, and the arc is one signed
integer: how far the wall bows from its chord, in millimetres.
Positive bows into the room, negative out, and depth zero is the
straight case, so a curve flattening is not a discontinuity. Radius-driven
arcs were rejected for the opposite reason: a radius goes to infinity as a
curve straightens, and is almost never a whole number of millimetres.
Free point placement means a bowtie is expressible, so self-intersection
is a refusal (boundary_self_intersects, naming both segments)
rather than a structural impossibility. Arcs are capped at a semicircle.
Walls, and internal walls
The line you give is the internal face, the one a tape
measure lands on. The wall is thickened outward from it, so thickening never
moves the room or changes its area. A partition is the opposite and
deliberately so: it has a room on both sides, so neither face is privileged
and it straddles its line, half the thickness each way. A partition is a
segment and never a ring, which is what lets it exist without trading away
the closure guarantee above. Rooms, if you want them, are a read over
partitions rather than a second stored thing to keep in step.
What stands in it
- Objects — rectangles and circles, positioned, sized
and rotated. A column, a bench or a service station is an object marked
unmovable, never a hole cut in the room, so the boundary stays one ring
and validation treats it as what it is.
- Openings — doors and windows hosted on exactly one
wall or one partition, at an offset along it, with a hinge side and a
swing. On a bowed wall the jambs are found on the arc and the gap is an
annular sector, because a straight gap laid on an 18m radius leaves the
wall band by 33mm at each jamb.
- Components — what a door or window is lives
on the opening, as a type rather than as geometry. A six-over-six sash is
{ "type": "sash", "panes": [3, 2] }, about six tokens; the
same window as path data is around three hundred and can be wrong in ways
nothing will flag.
- Fixtures — a bath, a base unit, a WC, a stair: an
object carrying
fixture: { type }, drawn in plan, elevation
and section from one description at the trade size it is made in. Each
type ships its own placement rules, so a basin facing the tiles or a sofa
with its seat to the wall is reported by name with the rotation that turns
it.
- Zones — labelled polygons for the parts of the floor
that are not the room: bar, kitchen, servery, screen.
- Blocks — a grid of objects in one operation: rows,
columns, pitch, aisles, an optional bow with the objects turned to follow
it, and row-letter or sequential numbering. It expands to ordinary
objects, so an auditorium needs no second kind of document.
- Roof and rooflights — the plan already carries the
footprint, so a roof states form and heights and its outline is derived.
It is over-determined on purpose: span, eaves, ridge and pitch are four
numbers with one equation between them, so giving three that disagree is
roof_overdetermined with the discrepancy in millimetres,
never a silent choice of two.
Drawings are projected, never redrawn
Elevations and sections come out of the plan document. A
horizontal position on one is a reference to a plan feature, never a
number — opening:3.jamb.left, vertex:4.outer,
object:7.centre — so the plan and the section cannot disagree,
because the disagreement is inexpressible. Heights are given, never derived:
a cill height cannot be got from a plan, and a plausible 900 drawn on paper
is indistinguishable from a measured one forever after, so an opening with
no supplied height is refused by name.
A house is one ordinary document per storey against one shared datum, and
a joined section is a read over both: aligned from two fixed points you name,
with the junction checked rather than chosen. A drawing sits on an A-series
sheet at a true scale, and one too big for its cell is refused with both
numbers in the message. export_dxf hands the same drawings to
CAD as real entities on named layers, in model space, in millimetres.
What it asks rather than guesses
missing_dimensions answers a survey that does not determine
the shape with the readings to go and take, not with the number that would
close the ring. The same stance holds for a roof with two of its four
numbers, and for a height nobody measured. It could pick a plausible value
and must not: 40° drawn because 40 is typical is indistinguishable,
forever after, from 40° measured.
Limits
Every cap is a measured number — the cost of validating and rendering at
that size — rather than an opinion about rooms.
| 1,000,000 | World millimetres either way from the origin. A room 2mm wide and a room 900km wide are both refused rather than drawn. |
| 256 | Boundary vertices, so the same number of wall segments in the ring. |
| 128 | Partitions internal walls. A segment each, never a ring. |
| 128 | Openings doors and windows across the ring and the partitions. |
| 64 | Zones labelled polygons. |
| 64 | Zone vertices per zone. |
| 2,000 | Objects raised from 512 by measurement, not opinion. An eighty-seat auditorium is eighty of these. |
| 40 | Covers per object. |
| 26 | Sections one per letter. |
| 64 | Overhead beams and the like. |
| 64 | Rooflights hosted by the roof, not by a wall. |
| 1,000 | Wall thickness millimetres. |
| 1,000,000,000 | Ids the highest id an object may carry. |
| 16 | Labels characters. |
| 16 | Numbers characters in an object number. |
| 64 | References characters in a section reference. |
The operation vocabulary
Fifty-seven named operations, one list shared by the editor, the undo
stack, the API and edit_layout. There is nothing a pointer can
do in the editor that a caller cannot, because a gesture resolves to one of
these and this list is the API.
place_corner · remove_last_corner · close_room · reopen_room · move_corner · bow_wall · set_wall_length · add_door · add_window · remove_opening · slide_opening · set_opening_width · swap_door_hinge · swap_door_swing · swap_door_leaves · set_opening_component · set_roof · remove_roof · add_partition · remove_partition · restore_partition · set_partition_thickness · add_beam · remove_overhead · label_overhead · add_section · remove_section · place_zone_corner · remove_last_zone_corner · close_zone · reopen_zone · add_zone · move_zone_edge · label_zone · remove_zone · move_zone_corner · remove_corner · move_wall · split_wall · chamfer_corner · restore_ring · add_table · add_object_block · remove_objects · restore_objects · remove_table · move_table · set_table_size · set_table_occupancy · assign_table · set_table_shape · rotate_table · add_fixture · set_fixture · add_rooflight · remove_rooflight · start_over
Refusals
Fifty-four stable codes, returned as data
rather than thrown, each naming the offending segment, opening, zone or
object rather than saying “invalid layout”. message
is for a human reading logs and is not part of the contract; switch on
code. A shipped code is permanent: we add freely and
rename never. A refusal names everything wrong at once where it
can, not the next thing wrong.
ring_malformed · ring_too_short · coordinate_not_integer_mm · segment_zero_length · depth_not_integer_mm · arc_exceeds_semicircle · boundary_self_intersects · schema_version_unsupported · document_malformed · document_too_large · coordinate_out_of_bounds · id_duplicate · zone_malformed · opening_off_wall · partition_malformed · partition_outside_boundary · opening_exceeds_wall · opening_too_near_corner · object_malformed · object_out_of_bounds · objects_overlap · too_many_overlaps · determinacy_not_rectilinear · dimension_malformed · dimension_not_axial · view_unknown · height_missing · elevation_ordinate_unreferenced · elevation_arc_unsupported · component_type_unknown · fixture_faces_wall · fixture_off_wall · fixture_access_blocked · fixture_type_unknown · overhead_kind_unknown · section_ref_duplicate · section_ref_unknown · section_mark_unknown · section_extent · paper_unknown · sheet_overflows · roof_kind_unknown · roof_eaves_malformed · rooflight_off_roof · roof_overdetermined · scale_unusable · storeys_malformed · storeys_misaligned · storeys_views_differ · storeys_junction_mismatch · clearance_violation · rule_kind_unknown · rule_malformed · op_unknown
Validation cost is bounded as well as complete. Overlap is a sweep over a
spatial index rather than every object against every other, because an
O(n²) check is a denial of service handed to the caller; and the
overlap report is capped too, since n objects on one point overlap
in n²/2 pairs. NaN, Infinity, -0,
numbers past the safe integer range and rotations of a billion degrees all
survive typeof x === 'number' and are all refused at the
boundary.
Rendering
The renderer is a pure function of the parameters: byte-identical SVG for
identical input, on every platform, so a render can be diffed in CI. Nothing
reachable from a geometry path reads a clock or a random number, iteration
order is defined, and a title block's date is a string the caller supplies
rather than one the engine invents.
Colours arrive as a validated token map and an unknown token is refused
rather than ignored. On paper the engine draws the register it was built to:
solid ink poché, two tones, three line weights, one grotesque in
tracked capitals. A style is never an argument to a tool, because a style
that is an argument is a style every caller gets wrong differently.
What it is not
No levels, families, schedules, parametric constraint solvers, egress
analysis or clash detection, and there never will be: if a feature only
makes sense because a BIM package has it, it is out of scope. Editing is
single-editor by decision — undo is a bounded stack of inverse commands, not
a CRDT — and making it concurrent later is a rewrite of the mutation layer
we would rather discuss than do quietly. A section does not yet cut a
pitched roof, and half-hipped, mansard, gambrel and catslide roofs, dormers
and chimneys are refused by name rather than guessed at.
The engine's source is not public. This endpoint is the interface, not
the implementation, and it is free while it is a demand probe: it will be
priced eventually and there will be notice first.