PlanPilotMCP
# PlanPilotMCP
**Draw architectural plans, 3D models and BIM files in FreeCAD just by asking Claude.**
**PlanPilotMCP** is an [MCP](https://modelcontextprotocol.io) server that drives a **running** FreeCAD (GUI) through its native Python API. Units: millimetres.
<p align="center">
<img src="docs/assets/plan.png" alt="Colour floor plan of a 108 m² house drawn by PlanPilotMCP in FreeCAD: walls, partitions, doors, windows, furniture, room labels with areas and exterior dimensions" width="720">
<br><sub>Real output: a 12 × 9 m house drawn by tool calls (walls, partitions, doors, windows, furniture, rooms with areas, exterior dimensions), exported from a TechDraw sheet and coloured for the README.</sub>
</p>
> "Draw a 10 × 8 m house with a partition in the middle and an entrance door on the south side, fill the walls, dimension the outside, then put it on an A3 sheet at 1:100 and export the PDF."
| | |
|---|---|
| **2D plan** | walls (merged, no doubled lines), partitions, doors and windows linked to their wall, stairs, layers, editing (move, copy, rotate, mirror, offset, trim, extend) |
| **Annotation** | automatic dimensions, texts, rooms with areas, hatching, solid fill (poché) |
| **Layout** | TechDraw sheets (A4–A1, scale, title block), PDF / SVG / DXF export |
| **3D** | walls extruded from the plan with openings cut through, slab, flat or gable roof, stairs, isometric view |
| **BIM** | levels (one plan each), marks and properties on doors, windows, rooms and wall types, quantity schedules (CSV), IFC4 export |
## Architecture
```mermaid
flowchart LR
C["Claude Code"] -- "stdio (MCP)" --> S["server/<br/>MCP tools<br/>Python ≥ 3.11"]
S -- "TCP 127.0.0.1:9876<br/>JSON lines" --> B["bridge/<br/>inside FreeCAD"]
B -- "queue + 5 ms QTimer" --> G["Qt GUI thread"]
G --> T["tools/<br/>handlers"]
T --> D["FreeCADBackend<br/>Draft · Part · TechDraw"]
D --> F[("FreeCAD<br/>document")]
```
The `tools/` layers, testable without FreeCAD thanks to a fake backend:
```mermaid
flowchart TB
H["Handlers<br/>geometry · architecture2d · modify · dimensions · text · hatch · fill<br/>stair_tools · model3d · levels · bim_props · schedules · ifc_export · sheets"]
P["Ports<br/>DrawingBackend · ModelStore · SolidBuilder · SceneReader · SceneEditor · SheetBackend"]
G["Pure geometry<br/>geometry2d · shapes · polyunion · openings · build3d · stairs · bim_model"]
A["FreeCAD adapter<br/>freecad_backend · techdraw_backend"]
X["Fake backend<br/>tests/fakes.py"]
H --> P
H --> G
P -.implemented by.-> A
P -.implemented by.-> X
```
More in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
## MCP tools (77)
Full parameter reference: [`docs/TOOLS.md`](docs/TOOLS.md).
| Family | Tools |
|---|---|
| Primitives | `ping` `create_rectangle` `create_line` `create_polyline` `create_circle` `create_arc` |
| 2D architecture | `create_wall_2d` `create_partition_2d` `create_wall_loop` `merge_walls` `create_door_2d` `create_window_2d` `create_stair` |
| Reading | `get_document` `get_objects` `get_selected_objects` `get_object_geometry` `get_layers` `get_bounds` |
| Editing | `move_object` `rotate_object` `copy_object` `delete_object` `mirror` `offset` `set_geometry` `trim` `extend` |
| Layers | `create_layer` `set_layer` `set_visibility` `set_line_width` `set_line_style` |
| Dimensions | `dimension_horizontal` `dimension_vertical` `dimension_aligned` `dimension_linear` `dimension_angle` `dimension_chain` `dimension_exterior` `dimension_room` |
| Texts | `create_text` `create_note` `create_room_label` `get_texts` `update_text` |
| Hatch and fill | `create_hatch` `create_fill` `fill_walls` |
| 3D | `create_walls_3d` `create_slab` `create_roof` `create_stairs_3d` `clear_3d` `set_view` |
| Levels | `create_level` `set_active_level` `get_levels` `copy_level` |
| BIM | `set_properties` `set_wall_type` `get_wall_types` `get_schedule` `export_schedule` `export_ifc` |
| Sheets and export | `create_drawing_page` `get_sheet` `set_page_size` `set_orientation` `set_scale` `add_plan_view` `position_view` `add_title_block` `update_title_block` `export_pdf` `export_svg` `export_dxf` |
Standard layers (French names, created on first use):
| Layer | Content |
|---|---|
| `murs` · `cloisons` | walls · partitions |
| `ouvertures` | doors and windows (one block each) |
| `escaliers` | stairs |
| `cotations` · `textes` | dimensions · room names, areas, notes |
| `hachures` · `poche` | hatching · solid fill |
| `mobilier` | furniture |
| `murs_3d` `dalles_3d` `toiture_3d` `escaliers_3d` | 3D volumes (kept out of the sheet and the DXF) |
Annotations, hatching and fill are real geometry (lines + vector text), so they print in the PDF.
## Installation
1. `uv sync`
2. In FreeCAD (Python console: View ▸ Panels ▸ Python console):
```python
import runpy; runpy.run_path("/path/to/planpilotmcp/bridge/start_bridge.py")
```
The Report view prints `PlanPilotMCP bridge listening on 127.0.0.1:9876`. Once per FreeCAD session.
3. Register the server in Claude Code:
```
claude mcp add planpilot -- uv run --directory /path/to/planpilotmcp python -m server.main
```
Change the port with `FREECAD_MCP_PORT` (on both sides). Exports with an explicit `path` are confined to the document folder, `~/Documents` and `FREECAD_MCP_EXPORT_DIR` (set it on the FreeCAD side to allow another folder), and never replace an existing file unless `overwrite=true`. Optionally set `FREECAD_MCP_TOKEN` on both sides: the bridge then rejects requests without the right token. After editing the code, re-run step 2 in FreeCAD (the bridge hot-reloads) and restart Claude Code.
Try it: "draw a 10000 × 8000 mm rectangle". It appears immediately, and Ctrl+Z undoes it.
The IFC export needs `ifcopenshell`, which FreeCAD ships with; `uv sync` also installs it for the tests.
## Typical run
1. **Draw:** `create_wall_loop`, `create_partition_2d`, `create_door_2d`, `create_window_2d`, `create_stair`. Walls fuse where they touch. Place doors and windows after the walls.
2. **Annotate:** `create_room_label`, `dimension_exterior`, `dimension_room` (they take `scale` = the print scale denominator), then `fill_walls` for the solid poché of the cut walls. Do the fill and hatching after the openings are placed.
3. **3D (optional):** `create_walls_3d`, `create_slab`, `create_roof`, `create_stairs_3d` build volumes at the active level's elevation and turn the view to isometric. `clear_3d` removes them.
4. **Levels (optional):** `create_level` or `copy_level` for more storeys. Each level has its own plan; every tool works on the **active** level (`set_active_level`).
5. **BIM (optional):** `set_properties` (marks, heights, materials) and `set_wall_type`, then `get_schedule` / `export_schedule` for quantities and `export_ifc` for the IFC file.
6. **Sheet:** `create_drawing_page` (`size="A3"`, `scale="1:100"`, title block fields), `add_plan_view` (read its `warning`), then `export_pdf` / `export_dxf`. Save your FreeCAD file first: exports land next to it.
Sheet creation and export run TechDraw in FreeCAD's event loop and can take several seconds.
## Known limits
- The 3D, the BIM model, hatching and fill are **read from or drawn over the plan**: after editing the plan, run them again (`create_walls_3d`, `fill_walls`, `export_ifc`). A door added after a hatch does not clear it.
- A wall already cut open by a door is no longer a closed outline and is not merged: merge the walls first, then place the openings.
- The gable roof is a prism over the bounding box of the house (no hip roof, no L-shaped roof). Stairs are straight, quarter-turn or half-turn only; their 3D steps are solid blocks and do not cut the slab above.
- Levels cannot be deleted or renamed yet; door/window marks (P1, F1…) are numbered per level; the sheet and the DXF show the active level only.
- The solid fill is drawn as very dense pen-thick lines, not as a true filled area (the DXF has lines, not a HATCH); on the sheet the grey tone is striped.
- A long handler freezes the FreeCAD window while it runs (a warning is logged above 5 s).
- The IFC file was checked by reading it back with `ifcopenshell`, not in an external BIM viewer.
## Tests
```
uv run pytest # 698 tests: handlers on a fake backend, protocol, TCP, 3D, BIM, IFC read back
uv run pytest -m freecad # integration, needs FreeCAD importable
```
Full check against a running FreeCAD (bridge started as in Installation): draws a house through the bridge and asserts walls, openings, stairs, fill, 3D volumes, levels, schedules, IFC, sheet and PDF.
```
uv run python scripts/e2e_freecad.py --pdf /tmp/plan.pdf
```
## Project layout
```
bridge/ protocol, dispatcher, TCP server, Qt GUI-thread executor, launcher
server/ MCP tool declarations + bridge client
tools/ validation, pure geometry, ports, FreeCAD adapter, handlers
tests/ unit tests + fake backend
scripts/ end-to-end check against a running FreeCAD
docs/ architecture, tool reference
```
Adding a tool = write a handler + `registry.register(...)` in a `register_*_tools` function wired in `bridge/launcher.py` + a declaration in `server/main.py` (a test checks that both sides stay in step).
## License
[MIT](LICENSE) © 2026 stv-devl
TDQS
Scored across 77 tools
Most tools target a distinct resource and action, but there are overlapping clusters: seven dimension tools, three fill/hatch tools, and three wall creation tools. The detailed descriptions disambiguate them, though an agent could still misselect between dimension_variant or fill_walls vs create_hatch.
All tools use a consistent verb_noun snake_case pattern, including dimensional variants (dimension_vertical, dimension_aligned) and level/export names. The few single-verb tools (mirror, offset, trim, extend) are conventional imperative commands and do not break the pattern.
77 tools is far beyond the well-scoped 3-15 range and the 25+ threshold, making the surface heavy. The broad architectural/BIM domain justifies some size, but this is still an extreme count that risks overwhelming agents and consuming context.
The tool surface covers the full lifecycle for the domain: create/get/update/delete for objects and levels, walls, doors/windows, stairs, 3D extrusion, schedules, and many export formats. Minor gaps exist (no delete_level, no door/window width update), but these can be worked around with existing tools.