HZPlan
Turn a photo, measurements, or a description into an editable 2D floor plan — name rooms, place objects, and view it in 3D.
What it can do
- Create Plan: Use this when the user wants to start a brand-new house plan from scratch. Creates an empty private plan owned by the current user and returns its plan_id; follow up with create_storey, t
- List Plans: Use this when you need to find a plan_id or show the user what plans they have. Lists the current user's private plans with their ids and titles. Read-only.
- Request Image Upload: Get a one-time upload link for a floor-plan image when you can't attach it directly or the file is too large to pass inline. Works for ANY client (Claude, ChatGPT, etc.). Quota-f
What data it sees
Do you need an account
No: the server works without sign-in
Turn a photo, measurements, or a description into an editable 2D floor plan — name rooms, place objects, and view it in 3D. Long description: HZplan is a house-layout assistant. Describe a home or upload a floor-plan photo and HZplan builds a structured, editable plan: storeys, walls, openings, rooms, and objects with real metric dimensions. Ask it to add a wall, place a desk, name a room, or publish a shareable view. Plans render to SVG, an interactive web view, and a 3D walkthrough.
How to connect
How to connect HZPlan to Claude
The server address to paste into Claude settings:
https://hzplan--goncharov-artemv.run.tools
- Open Claude (claude.ai or the desktop app).
- Go to Settings → Connectors.
- Click Add custom connector.
- Paste the server address copied below into Remote MCP server URL and click Add.
- In a chat, click + → Connectors and switch the new connector on.
Custom connectors are available on Free, Pro, Max, Team and Enterprise plans (Free is limited to one). On Team and Enterprise an organization Owner adds the connector first under Organization settings → Connectors.
Authorization
After you click Add, a sign-in window for the service opens. Sign in with your own account and approve access. Claude never sees your password.
How to connect HZPlan to ChatGPT
The server address to paste into ChatGPT settings:
https://hzplan--goncharov-artemv.run.tools
- Open ChatGPT in a browser (chatgpt.com). A Plus, Pro, Business, Enterprise or Edu plan is required.
- Turn on developer mode once: Settings → Apps → Advanced settings → Developer mode.
- Open Settings → Connectors and click Create.
- Fill in the form: Name (anything), Description (one line about what the service does), MCP server URL (copy it below).
- Under Authentication choose OAuth if the service requires sign-in, otherwise None. Click Create.
- In a new chat open + → Apps/Connectors and enable the connector.
OpenAI has renamed this section before (Connectors → Apps/Plugins). If the label differs, search settings for "developer mode". On Business/Enterprise workspaces an admin must allow custom connectors first.
Authorization
On first use ChatGPT opens the service's sign-in window. Sign in and approve access.
How to connect HZPlan to Cursor
The server address to paste into Cursor settings:
https://hzplan--goncharov-artemv.run.tools
Fastest: click Open in Cursor below and confirm the prompt.
Manually:
- In Cursor open Settings → Cursor Settings → MCP and click Add new global MCP server.
- Paste the JSON copied below into
~/.cursor/mcp.json(per project:.cursor/mcp.jsonin the repo root). - Save the file. The server appears in the MCP list; authorize it there if asked.
Authorization
If the service needs sign-in, an authorize button appears next to the server in the MCP list.
Server tool list (31)
Raw names from tools/list. Only developers need these.
| create_plan | Use this when the user wants to start a brand-new house plan from scratch. Creates an empty private plan owned by the current user and returns its plan_id; follow up with create_storey, then add walls/openings/objects via upsert_entity. To build a plan from a floor-plan image instead, use split_floor_plan + convert_floor_to_plan. |
| list_plans | Use this when you need to find a plan_id or show the user what plans they have. Lists the current user's private plans with their ids and titles. Read-only. |
| request_image_upload | Get a one-time upload link for a floor-plan image when you can't attach it directly or the file is too large to pass inline. Works for ANY client (Claude, ChatGPT, etc.). Quota-free. FLOW 1. Call this (no args) → { upload_id, upload_url }. 2. Show the user the upload_url and ask them to open it and drop their floor-plan image (one tap). 3. Then call split_floor_plan with image_ref: { upload_id }. If they haven't finished you'll get code 'upload_pending' — wait a moment and retry. 4. Reuse the SAME upload_id for convert_floor_to_plan — one upload covers split + convert. ChatGPT: prefer attaching the file (pass `image`). But if that keeps failing or the file is too large, FALL BACK to this upload link — the user just opens it and drops the image, and you only ever pass back image_ref:{ upload_id } (no base64). LAST-RESORT FALLBACK: if the user can't or won't use the link, tell them they can create the plan directly in the web app: https://hzplan.uk/plans/new/from-image — never leave them with no way forward. |
| send_feedback | Send the user's feedback to the HZplan developers. Use when a tool failed, behaved unexpectedly, was confusing, or the user has a complaint, suggestion, or praise. Quota-free. CONSENT — REQUIRED: first draft the message, show the user the EXACT text you will send, and get their explicit agreement. Never send feedback the user hasn't seen and approved, and never include anything they didn't agree to share. |
| split_floor_plan | Step 1 of 2 in the image → hzplan pipeline. Detects how many distinct floor plans are present on a single uploaded image (residential plans frequently show ground + first + sometimes basement on one page) and returns one bounding box + preview crop per detected floor. Quota-free. WHEN TO CALL Call this ONCE per image the user wants to convert into an hzplan. If it returns ok:true, USE the floors[] result — show previews to the user, ask which to import, then call convert_floor_to_plan for each chosen floor. DO NOT retry a successful call. DO NOT issue duplicate calls back-to-back. WHAT TO PASS image: the user's uploaded image (ChatGPT fills download_url + file_id automatically; the model never has to base64-encode anything). Other clients can use `raw_url` (data: URI or http URL) or `raw_base64` instead — non-ChatGPT only. FALLBACK if the image is hard to pass (ChatGPT base64 failing, file too large): call request_image_upload, have the user drop the image on the link, then pass image_ref:{ upload_id }. As a last resort, point the user to the web app: https://hzplan.uk/plans/new/from-image WHAT YOU GET BACK ON SUCCESS { ok: true, floors: [ { floor_id, suggested_name, bbox_px, source } ] } PLUS one preview image per floor attached as image content (in the same order as floors[]). - floor_id: opaque string. PASS THIS to convert_floor_to_plan to convert that specific floor. - suggested_name: 'ground' / 'first' / 'second' / 'third' / 'fourth' or 'floor N' for >5 floors. Use as the default storey_name unless the user overrides. - bbox_px: [x1, y1, x2, y2] location of the floor in the source image's pixel space. - the attached preview images let the user confirm which floor is which; floors[][i] corresponds to attached image i. NEXT STEPS AFTER A SUCCESSFUL CALL 1. Show the user the attached floor preview images (one per entry in floors[], same order). 2. Ask which floors to import and confirm storey names. 3. For each chosen floor, call conv |
| convert_floor_to_plan | Step 2 of 2 in the image → hzplan pipeline. Runs full inference + conversion on ONE floor (picked by floor_id from a prior split_floor_plan response) and either mints a new hzplan (plan_id omitted) or appends the floor as a new storey to an existing plan (plan_id given). Burns 1 unit of `plan_from_image_monthly_*` quota per call. WHEN TO CALL After split_floor_plan returned successfully, the user has seen the previews, and they've told you which floor(s) to import. Call this ONCE per chosen floor. For a 3-floor plan the user wants fully imported, that's 1 split_floor_plan call + 3 convert_floor_to_plan calls. DO NOT retry a successful call. DO NOT call this before split_floor_plan. WHAT TO PASS image: the SAME image used in split_floor_plan (ChatGPT auto-fills again from the user's upload). floor_id: the floor_id from the split_floor_plan response. storey_name: human label for this storey, e.g. 'ground', 'first', 'kitchen floor'. Default to the suggested_name from split_floor_plan if the user doesn't say otherwise. plan_id: OMIT on the first conversion (mints a brand-new plan). PASS the previous call's plan_id on all subsequent conversions so all floors land on ONE plan as separate storeys. plan_title: only relevant when plan_id is omitted — title for the new plan. WHAT YOU GET BACK ON SUCCESS { ok: true, plan_id, storey_id, quota, summary } - plan_id: pass this to the NEXT convert_floor_to_plan call if there are more floors to import. - storey_id: the storey id assigned inside the plan ('s1', 's2', ...). - quota: current usage info. - summary: diagnostic counts (walls, openings, mm_per_px) — usually fine to ignore. NEXT STEPS - After all chosen floors are converted, tell the user the plan is ready. Render it with render_storey_widget(plan_id, storey_id) to show the result. - The user can then refine the plan with upsert_entity / delete_entity / set_object_feature_side etc. — those operate on walls / openings / objects inside the stor |
| rescale_storey | Multiplies every length / size / offset on `storey_id` by `factor`. Wall thicknesses, image bboxes, angles, and other absolute / unitless values are NOT touched. Runs the geometry engine after the rescale and refuses to commit if the storey becomes infeasible. WHEN TO CALL After convert_floor_to_plan if the AI inspects the image and decides the plan's scale is wrong (door-width heuristic is typically ±20% off). Also usable on hand-authored plans where the user wants to scale a storey up/down. Call ONCE per scale change — if the call succeeds, the plan is updated; if it fails (solver_infeasible / solver_ambiguous), the plan is unchanged. WHAT TO PASS plan_id: the plan owning the storey. storey_id: the storey to rescale (e.g. 's1', 's2'). factor: number in [0.1, 10]. Compute as `target_real_world_length / current_plan_length`. Example: AI sees a wall on the image and thinks it's 4000 mm in real life; the plan currently reports that wall as 3500 mm; factor = 4000 / 3500 ≈ 1.143. WHAT YOU GET BACK ON SUCCESS { ok: true, plan_id, storey_id, content_version, scaled_count: { length_mm, size_mm, offset }, storey_size_mm: [w, h] } - scaled_count reports how many of each predicate were touched (sanity check the factor was applied to walls + openings). - storey_size_mm is the new outer dimensions in mm. ERROR CODES plan_not_found / storey_not_found: bad ids. invalid_factor: factor out of [0.1, 10]. solver_infeasible: rescaled storey is overconstrained (e.g. a fixed-size object no longer fits). Plan was NOT modified. Try a different factor or fix the object first. solver_ambiguous: rescaled storey is underconstrained. Add missing offsets and retry, or revert. solver_error: the geometry engine itself failed; surface to user. Do NOT retry on success. Compose multiple rescales by calling once per storey. |
| list_illustrations | List accessible system illustrations and the current user's private illustrations. Use this tool for illustration discovery; do not use MCP resources for library data. |
| get_illustration | Get one illustration, including its stored SVG markup. Use this tool for illustration data; MCP resources are widget HTML only. |
| set_object_illustration | Attach, replace, or clear an illustration on an existing object while preserving the object's other assertions automatically. Prefer this over upsert_entity when you only want to change illustration_id. |
| set_object_feature_side | Update only the directional `feature_side` on an existing object (top/bottom/left/right, or null to clear) while preserving every other assertion — kind, size_mm, offsets, illustration_id, model_3d, etc. ALWAYS prefer this over upsert_entity when changing only the facing direction; upsert_entity replaces the whole assertion list and will silently drop attachments like model_3d. |
| set_object_height | Update only the `height_mm` on an existing object (positive integer of millimetres, or null to clear and fall back to the kind default) while preserving every other assertion — kind, size_mm, offsets, illustration_id, model_3d, feature_side, etc. ALWAYS prefer this over upsert_entity when changing only the rendered height; upsert_entity replaces the whole assertion list and will silently drop attachments like model_3d. Use this when a generated 3D model looks too tall or too short in the 3D walk — the 3D generation parameter only changes the GLB's baked proportions; the renderer scales to whatever this assertion (or the kind default) says. |
| upsert_illustration | Create or update a private SVG illustration in the current user's library. Omit illustration_id to create a new illustration (name and svg_markup are then required); pass illustration_id to update an existing one (only the fields you include change, the rest are preserved). render_svg_preview only previews — this tool persists. |
| delete_illustration | Archive one private illustration from the current user's library. |
| get_plan | Get the stored top-level summary for one plan. This returns summary data, not the full plan document. |
| create_storey | Create a new empty storey in an existing plan and return its current storey state. After create_storey, immediately call upsert_entity with type=storey and size_mm(width_mm, depth_mm) so the geometry engine has a fixed storey rectangle. Then place outer walls relative to storey anchors like s1.left, s1.right, s1.top, and s1.bottom. |
| get_storey_plan | Get bounded structured detail for one storey. Use storey_id from get_plan when possible. |
| name_space_at_point | Create or update a semantic space name by dropping an interior pin on a solved storey. Use this for named floor areas such as bathroom, bedroom, hall, kitchen, or garage after walls and openings already define the enclosure. This does not author new geometry. Instead it stores a semantic pin and binds the name onto the derived free-space region that contains that point. The point must fall inside exactly one derived free-space region. Provide space_id to rename or move an existing named space; omit it to create a new one. |
| get_plan_topology | Get the plan's spatial room topology. Returns one node per derived free-space region (the rooms shown in the UI) with its storey, an interior `center` point, area, and bound `name`/`kind` (null until named); and edges for the openings/doorways connecting rooms. To name a room, pass that node's `center` as the (x_mm, y_mm) to name_space_at_point. |
| list_room_objects | List current objects in one room. |
| get_object | Get one object with its current constraints and solved placement metadata. Use set_object_illustration for illustration-only changes; use this before upsert_entity when you need to preserve existing assertions during broader object edits. |
| render_svg_preview | Render one illustration SVG preview in the ChatGPT widget. Preview only; this does not save anything. Use upsert_illustration to persist SVG markup. |
| render_storey_widget | Render one storey in the ChatGPT widget. |
| render_named_room_widget | Render one named room from a storey in the ChatGPT widget. |
| render_plan_topology_widget | Render the whole-plan topology graph in the ChatGPT widget. |
| get_authoring_guide | Call this first when authoring or editing plans if you need the supported predicates, recommended sequencing, or the MCP usage rule that resources are widget shells only while tools handle all reads and writes. |
| upsert_entity | Create or update one authored entity in the current plan head. Supported phase-1 entity types are storey, wall, opening, and object. Assertions are entity-local: do not repeat entity_id in args. Use tools for all data reads and writes; MCP resources are widget HTML only and are never required for CRUD. When changing only an existing object's illustration link, prefer set_object_illustration instead of rebuilding the full assertion list manually. Storey workflow: after create_storey, upsert the storey with size_mm(width_mm, depth_mm) before adding structural entities. Only walls and openings create or divide rooms. Use name_space_at_point(...) to name enclosed floor areas such as bathroom, bedroom, hall, kitchen, or garage after walls and openings define them. Do not create those spaces as objects. Objects are furniture, fixtures, and equipment such as cupboards, sofas, beds, tables, toilets, sinks, and tubs; they may align to walls or surrounding spaces but they do not replace partition walls. Some furniture is directional: use feature_side(top|bottom|left|right) as the canonical rendered-facing predicate when orientation matters. When updating an existing entity, fetch it first and resend every assertion you want to keep. Storey predicates: ceiling_height_mm(number_mm), eye_height_mm(number_mm), size_mm(width_mm, depth_mm). Wall predicates: on_storey(storey_id), kind(value), axis(horizontal|vertical), angle_deg(number), thickness_mm(number_mm), length_mm(number_mm), height_mm(number_mm), offset(anchor, reference_anchor, delta_mm). Offsets constrain solved rectangle edges, not wall centerlines, so think in left, right, top, and bottom edges. When you want a gap between two entities, offset the closest pair of edges that represents that gap. Outer walls should usually be positioned relative to storey anchors, for example offset(left, s1.left, 0) or offset(bottom, s1.bottom, 0). For interior wall corners, place the first wall from storey anchors, then place the closin |
| upsert_entities | Apply many entity creations/updates to one plan in a single call. Intended for blast-then-repair authoring when importing from a floor plan image: emit the whole plan (storey size + all walls + openings + objects) as one batch, read the per-item and global solver report, then resubmit a follow-up batch that only contains the corrections. Items are applied in order against the same in-memory document; if any item fails to apply OR the solver reports infeasible/ambiguous/overlapping storeys, nothing is persisted and the full per-item + per-storey report is returned so you can fix and retry. Prefer create_storey first (so s1 exists) and pass entity_id on items you want to update instead of create. |
| delete_entity | Delete one wall, opening, or object from the current plan head by its entity id — this is the single tool for removing in-storey entities. To remove an entire floor and everything on it, use delete_storey instead. Deletion fails if another entity still depends on this one (for example an opening still hosting on a wall, or an offset(...) anchored to it); the response then lists the blocking entity ids so you can re-host or re-anchor them, then retry. |
| delete_storey | Use this when the user wants to remove an entire floor. Deletes one storey and everything on it — its rooms, spaces, objects, walls, and openings — from the current plan head. This is destructive and is not reversible through the API. It fails only if an entity on another storey still references something on this one; the response then lists the blocking entity ids so they can be re-anchored or deleted first, then retry. |
| delete_plan | Use this when the user wants to permanently delete an entire plan. Removes the plan and all of its storeys, along with any uploaded floor-plan images and public snapshots tied to it. This is destructive and cannot be undone. Prefer this over deleting storeys one by one when the whole plan should go; confirm with the user first because there is no recovery. |