An MCP server that lets an LLM design buildable LEGO models by emitting validated LDraw files — not by clicking around in a CAD app.
A 1,024-piece fortress city designed entirely through these tools — 1,576 verified stud connections, zero floating parts, zero collisions, validated at every stage. The model is examples/fortress_city.ldr; open it in Studio.
Drop it into Claude Desktop, ask Claude "build me a small red house on a tan baseplate," and the LLM gets 55 semantic tools, 6 prompts, and 5 reference resources covering: real LDraw catalog (24,009 parts), buildability checks (no floating / no collisions / no overlaps), connection-aware bonding, builder mode for piece-by-piece assembly, persistent projects, autosave, inline render previews in the chat, and a debug toolkit (render_validation, inspect_part, collision_detail, describe_errors). The output is a real .ldr / .mpd file you can open in BrickLink Studio or LeoCAD.
Status: early days, but the foundation is now load-bearing. The pipeline runs end-to-end — install, talk to Claude, get an exportable
.ldr— and the connection model is enforced for the entire 24k-part catalog: every placement reports what it actually clutches, strict mode rejects physically impossible positions, and the validator's ground truth is real stud↔receiver mating (in both directions — hanging parts under overhangs is a first-class connection). Structured architecture (walls, towers, gates, roofs — the fortress above) builds reliably through the helpers.Designing LEGO models with an LLM is still hard. The model has to reason about 3D geometry, brick connectivity, stagger / bond patterns, color, and proportion, and keep all that consistent across dozens of tool calls. Organic shapes, curves, and studs-not-on-top techniques are not yet within reach of the shipped tools (the connection model is stud-only — pins, clips, and brackets are Phase 2). The semantic toolkit is what makes any of this possible; getting it to be fluent is the actual research problem.
The renderer, the catalog, the validation/feedback loop, the import/export path, and the CLI are the parts that are working well today.
- Why this exists
- The non-negotiable rules
- Requirements
- Install & configure Claude Desktop
- Inline preview modes
- Coordinate convention
- The tools, grouped
- Prompts (slash menu)
- Resources
- Run as a standalone agent
- Standalone CLI:
render/bom/instructions - Development
- Known limitations
- License
Letting an LLM drive a CAD app via screen automation fails — too many micro-actions, no spatial ground truth, errors compound. LegoMCP gives the LLM a semantic API: every action is a typed function call, every change is validated against real LEGO connection rules, every state is reversible. The CAD app becomes the viewer; the MCP server is the source of truth.
The toolchain enforces these so what the LLM exports is actually buildable:
- Connection rule — every part either sits on the ground (y ≈ 0) or has at least one stud-receptor mating with a part directly above or below. Both directions are real connections: sitting on studs below or hanging from receivers above (building downward is fine). Merely resting on another brick without stud alignment — e.g. half a stud off, or a 1-wide brick centered on a 2-wide one — is NOT a connection and is reported as
floating. Enforced for every part in the catalog, not a hardcoded subset. - Anchored rule — connected islands must reach the ground via a chain of stud connections (in any direction). A "tower" whose bricks touch each other but the whole thing levitates is reported as
unanchored. - No overlap — bricks must not occupy the same AABB.
add_part(strict=True)rejects overlapping and unconnected placements at insertion time, and everyadd_part/move_part/rotate_partresult carries aconnectivityreport +warningsso problems surface immediately, not at the nextvalidate_model. - Real geometry — AABBs, stud positions, and dimensions come from actually-parsed LDraw
.datfiles (recursively through subparts andstug-NxNstud-group primitives). A "Brick 2x4" really is 80 LDU × 40 LDU × 24 LDU with 8 studs at the correct positions.
- macOS (tested) or Linux. Windows should work via WSL; not tested.
uv— Python toolchain manager.brew install uv(macOS) orcurl -LsSf https://astral.sh/uv/install.sh | sh.- ~150 MB disk for the LDraw parts library (one-time download).
- Claude Desktop for the inline-preview workflow. The standalone agent works without it but needs an
ANTHROPIC_API_KEY.
# 1. Clone & install
git clone <this repo> LegoMCP && cd LegoMCP
uv tool install --from . lego-mcp
# 2. Download the full LDraw parts library (~135 MB, one-time, a few minutes)
lego-mcp install-library
# 3. Wire into Claude Desktop. Edit:
# ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"lego": {
"command": "/Users/<you>/.local/bin/lego-mcp",
"env": {
"LEGO_MCP_INLINE_MODE": "file_url"
}
}
}
}Replace <you> with your username. Quit Claude Desktop entirely (⌘Q) and reopen so it spawns the new server.
Verify in any chat: type / — you should see a lego group with the start, build, from_plans, from_image, rescue, and techniques prompts.
~/Library/Application Support/lego_mcp/renders/
Every render_model / render_progress / render_validation call writes <timestamp>_<model>.png (history preserved) and overwrites latest.png. Override with LEGO_MCP_RENDERS_DIR env var.
Open the latest at any time:
open "$HOME/Library/Application Support/lego_mcp/renders/latest.png"Preview.app auto-refreshes on file change, so leave it open for live updates.
Set via LEGO_MCP_INLINE_MODE in the Claude Desktop config's env block.
| Mode | What lands in the chat text | Bytes per render | Use when |
|---|---|---|---|
data_uri (default) |
 |
~16 KB | You want inline previews to work in any MCP client (web, Desktop, future clients). |
file_url (recommended for Claude Desktop) |
 |
~150 bytes | You're on Claude Desktop, which renders local file URLs. ~200× cheaper. |
none |
nothing | 0 | You only want the LLM-vision MCPImage block; no human preview in chat. |
The MCPImage block (Claude's vision input) is always present regardless of mode — only the human-facing preview text changes.
- Right-handed, -Y is up.
- 1 stud = 20 LDU wide. 1 plate = 8 LDU tall. 1 brick = 24 LDU tall (= 3 plates).
- Part origins are at the center of the bottom face.
- A baseplate (3811) at y=0 has its top face at y=-4. A brick sitting on it has its bottom face at y=-4.
- Stack a brick on top of another by subtracting 24 from y.
- A "Brick A x B" has its longer axis along +X. So a 2x4 brick is 80 LDU (X) × 40 LDU (Z) × 24 LDU tall.
Rotations are named, not matrices: identity, rot90y, rot180y, rot270y, rot90x, rot90z.
55 tools total. The recommended order of preference when the LLM is building: high-level helpers → connection-guaranteed placement (find_valid_placements + add_part_at_placement, place_on_top) → raw add_part only as a debug fallback. Each call shows the most useful arguments; full signatures are in the tool docstrings.
| Tool | What it does |
|---|---|
create_model(name, include_manual=True) |
Reset to a fresh empty model. The result carries getting_started — the building manual (coordinates, connection rules, workflow, tool order) — so the LLM gets the rules at the exact moment a build begins. The same manual ships as MCP server instructions at connection time. |
list_parts(limit, subassembly) |
List current parts (optionally filtered to one subassembly). |
bill_of_materials(subassembly, bricklink=False) |
The orderable parts list: quantities per (part_id, color) with part + color names, sorted most-needed first. bricklink=True also returns a BrickLink Wanted List XML (bricklink_xml) you can upload to order; colors without a verified BrickLink mapping are flagged in bricklink_warnings rather than guessed. |
| Tool | What it does |
|---|---|
add_part(part_id, color, x, y, z, rotation, strict=False) |
Explicit placement. strict=True rejects overlap and unconnected placements at insertion. Result includes connectivity (mated parts, studs engaged, grounded) + warnings. |
remove_part(instance_id) |
Delete by id. |
move_part(instance_id, x, y, z) |
Reposition. Reports connectivity at the new position. |
rotate_part(instance_id, rotation) |
Change orientation. Reports connectivity in the new orientation. |
These encode real LEGO masonry/architecture techniques so the LLM doesn't have to compute stagger/corners/seams by hand. Strict-grid by default; reject off-grid coords.
build_wall_segment(..., bond="running") — each row's seams shift by half a brick from the row below.
| Tool | What it does |
|---|---|
build_wall_segment(x0, z0, x1, z1, height_rows, color, bond, base_y, palette) |
Straight wall with running / stretcher / stack bond. End-fills with shorter bricks; tracks seams so adjacent rows don't share them. |
build_room(x_min, z_min, x_max, z_max, height_rows, color, bond, base_y) |
Rectangular hollow room. 4 staggered walls + 2x2 corner columns. |
build_perimeter(points, height_rows, color, bond, base_y) |
Closed orthogonal outline (any rectilinear footprint from a plan/image). Alternates corner ownership by row. |
build_wall_with_openings(x0, z0, x1, z1, height_rows, color, openings=[...]) |
Straight wall with rectangular / round-arch / lancet openings. |
build_stepped_gable_roof(...) / build_stepped_pyramid_roof(...) |
Connector-aware roofs. |
build_floor(x_min, z_min, x_max, z_max, y, color, part_id) |
Tile a rectangle with plates. |
build_corner(x, z, height_rows, color, brick_part) |
One-corner column (for hand-built rooms). |
place_on_top(base_id, new_part_id, color, stud_offset_x, stud_offset_z, rotation) |
Stud-grid stacking from a known anchor. |
place_next_to(reference_id, new_part_id, color, side, stud_offset, rotation) |
Flush placement (north/south/east/west of an existing part). |
repeat_pattern(part_id, count, dx, dy, dz, ...) |
Array of identical parts along a line. |
| Tool | What it does |
|---|---|
search_parts(query, limit=20) |
Keyword search over the 24k catalog. AND-of-tokens; use single concrete words ("arch", "wheel", "window"). |
get_part_info(part_id) |
Dimensions, stud count, bbox_local, AND for slopes an orientation block (high_edge, low_edge, plain-text summary) so you don't have to guess after rotation. |
list_colors() |
The 22 named colors + LDraw IDs. |
parts_that_mount_on(part_id, limit=20) |
Reverse mount search. Footprint-bucketed index over the full catalog; first call builds index (~1s), then queries are ~ms. |
| Tool | What it does |
|---|---|
find_connections(a, b, full_nesting_only=False, min_studs_matched=1) |
Every valid placement of B-on-A and A-on-B. |
find_valid_placements(part_id, near_part_id, limit=20) |
World-coordinate placements that REALLY connect to an existing instance — on top and hanging underneath, pre-filtered against collisions with the rest of the model, sorted by studs engaged. Each entry carries a token. |
add_part_at_placement(token, color) |
Place a part at a find_valid_placements result — strict-checked, zero coordinate math. The preferred way to attach a single part. |
suggest_next_brick_for_wall(subassembly) |
Heuristic next-brick suggester (Phase 1 stub). |
| Tool | What it does |
|---|---|
set_current_subassembly(name) |
Tag subsequent additions with a group name. |
list_subassemblies() |
Names + counts. |
remove_subassembly(name) |
Delete every part with the tag. |
clone_subassembly(src, dst, x_offset, y_offset, z_offset) |
Duplicate a built component. |
mirror_subassembly(src, dst, axis, plane_offset) |
Bilateral symmetry — build half a cathedral, mirror it. |
analyze_assembly_ports(subassembly) |
Exposed studs/receivers on a built module. |
find_subassembly_connections(movable, target) |
Where one module can connect to another. |
move_subassembly(name, dx, dy, dz) |
Translate a whole group. |
| Tool | What it does |
|---|---|
validate_model(max_errors=200, check_support=True) |
The 9-field report: parts, connections, collisions, unknown_parts, floating, unanchored, grid_alignment_errors, vertical_seam_score, wall_bond_quality. Errors include suggestion strings. |
inspect_part(instance_id, neighbor_studs=2) |
Focused diagnostic for ONE part: position, AABB, rotation, subassembly, validation flags, who it collides with (with overlap region + smallest separation), who it's connected to, who's nearby in XZ. |
collision_detail(part_a, part_b) |
For a flagged pair: overlap AABB, volume, smallest separating translation per axis, one-line "Move part X by Y LDU on Z to clear" suggestion. |
describe_errors(max_errors=10) |
Bulk-enriches every validate_model error with inspect_part / collision_detail context in one round-trip. |
All four return [markdown_preview, summary_dict, MCPImage] — the human sees the inline image, the LLM sees the image via its vision channel.
| Tool | What it does |
|---|---|
render_model(width=800, height=600, color_mode="model", hidden_edges=True) |
Isometric PNG. color_mode: "model" (actual colors), "instance" (one color per piece, for debugging), "row" (color by brick course), "rotation" (color by orientation). |
render_progress(width, height, color_mode, hidden_edges) |
Builder-mode render: built parts solid, unbuilt as ghosts. |
render_validation(width=900, height=700) |
Color-codes each part by validation status: green = ok, red = collision, orange = floating, purple = unanchored, yellow = off-grid, gray = unknown part_id. The one-shot "where are my problems?" view. |
view_latest_render() |
Re-show the most recent render without re-rendering. |
render_validation() on a clean build_room: every part is green — no collisions, nothing floating, nothing off-grid. When something is wrong, the offending parts switch color (red = collision, orange = floating, purple = unanchored, yellow = off-grid, gray = unknown part_id) so you can spot problems at a glance.
import_ldr("10030-1.mpd") followed by render_model(...) on the Imperial Star Destroyer (set 10030) — 9,361 parts across 135 sub-modules, ~7,290 of them carrying non-canonical rotation matrices (the angled hull plating). Import: 0.09 s.
✨ Current renderer. Reads the real triangle/quad geometry inside each .dat file and projects it directly. Bridge superstructure, three engine exhaust rings at the rear, antenna dishes, angled hull plating — all in true shape. Same parsing/transform pipeline that import_ldr uses, just hooked up to the catalog's actual mesh data.
Earlier draft of the renderer, kept around because it looks funny
🪨 The first cut of the renderer drew each part as its world-space bounding cuboid. The silhouette and scale are correct — every part is positioned and rotated faithfully, you can clearly see the loader walked the 135-sub-module hierarchy correctly — but slopes, curves, and engine details all became chunky stacks of grey blocks. Useful while debugging the loader / transform pipeline. It looks like a Minecraft Star Destroyer.
Builder mode isn't trained to design anything this complex yet — semantic tools for greebling, wing-plate panel boundaries, and curved-hull techniques are future work. But the import + render path holds up at this scale today: you can load any OMR model, inspect it, run validation on it, and start chipping away.
LDraw model © Roland Dahl (rolandd on the LDraw OMR), redistributable under CCAL 2.0. Downloaded from library.ldraw.org/library/omr/10030-1.mpd. The Imperial Star Destroyer is a trademark of Lucasfilm / The Walt Disney Company.
Three tiers — pick the right one:
| Tier | Tool | Use when |
|---|---|---|
| Checkpoint (in-memory, instant) | save_checkpoint(name) / restore_checkpoint(name) / list_checkpoints() |
Before a risky change. Lost on restart. |
| Project (on-disk, named) | save_project(name) / load_project(name) / list_projects() |
A meaningful intermediate you want to keep. Multi-block MPD + JSON notes. Survives restart. |
| Autosave (automatic) | autosave_status() / restore_autosave() |
Every 25 mutations, the model is dumped to _autosave.mpd. After a crash, call restore_autosave() to pick up where you left off. |
Walk a finished target model piece by piece (for a human or robot to physically assemble).
render_progress() mid-build: the bottom row of the target room is placed (solid red), the rest of the target shows as faded ghosts so you know what's still to come.
| Tool | What it does |
|---|---|
start_builder_session() |
Snapshot becomes the target; built set clears. After this, new add_part calls add to target, not built. |
end_builder_session(mark_all_built=True) |
Exit builder mode. |
builder_status() |
Quick snapshot: total / built / remaining / next_up part / blocked count / complete? |
next_unbuilt_step(limit=1) |
The next placeable unbuilt parts (only treats already-built parts as supporters). |
mark_built(id) / mark_built_batch(ids) / unmark_built(id) / reset_build_progress() |
Walk forward / back / start over. |
plan_build_sequence(subassembly, max_steps=50, start_after=0) |
Compile the whole target into a paged step list. |
next_build_step(subassembly, built_count=0) |
Same as above but per-call (compatible with plan_build_sequence). |
render_progress() |
See built-solid + unbuilt-ghost in one image. |
For workflows where Claude is reading a plan / photo and needs to remember dimensions across many turns.
| Tool | What it does |
|---|---|
add_note(key, text) / get_note(key) / list_notes() / remove_note(key) |
Sticky observations keyed by short string. Notes travel with the project (saved by save_project). |
| Tool | What it does |
|---|---|
export_ldr(path) / export_mpd(path) |
Write to disk. Relative paths land in the renders dir (so it works under Claude Desktop's read-only cwd). |
import_ldr(path) |
Load an .ldr or .mpd. Multi-block MPDs are flattened with subassembly tags preserved. |
| Tool | What it does |
|---|---|
undo() / redo() |
O(1) operation-based. Last 500 mutations. |
Invoke from Claude Desktop's / menu. Each one seeds the conversation with the right context.
| Prompt | What it does |
|---|---|
start |
Open this first in a new session. Full one-page reference: every tool, the build/validate/render loop, the three save tiers, builder mode. |
build (goal, scale) |
Generic "build X" prompt with the workflow + techniques. |
from_plans (subject) |
Multimodal cathedral-style build: upload plans, decompose into subassemblies, build half + mirror. |
from_image |
Build from an uploaded reference photo. |
rescue (path) |
Load an existing model, clean it up, fix collisions. |
techniques |
Print the LEGO techniques cheat sheet (running/English bond, SNOT, MILS, etc). |
Read-only references the LLM can fetch with resources/read.
| URI | Content |
|---|---|
lego://techniques |
LEGO building techniques cheat sheet |
lego://coords |
LDraw coordinate convention reference |
lego://workflow |
The build/validate/render loop |
lego://connections |
How stud connections work — the rules behind floating/unanchored |
lego://model/current |
Live state of the current model (JSON) |
uv sync --extra agent
export ANTHROPIC_API_KEY=sk-ant-...
uv run python -m lego_mcp.agent "build me a 16x12 stud room, 5 bricks high"The agent spawns lego-mcp as a subprocess, fetches the tool list over MCP JSON-RPC, hands the tools to Claude via the Anthropic SDK, and loops until Claude stops calling tools. Final model exported to ./agent_build.mpd and rendered.
Three one-shot commands that work on an .ldr / .mpd file from the shell — no MCP server, no Claude Desktop:
lego-mcp render <file>— render to PNG (below).lego-mcp bom <file>— the orderable parts list (below).lego-mcp instructions <file>— step-by-step build order (below).
For when you want to render an .ldr / .mpd file from the shell — previewing an OMR download, batching renders for a website, generating a turntable, or just sanity-checking a model someone sent you — without spinning up the MCP server or opening Claude Desktop.
lego-mcp render path/to/model.mpd # writes model.png next to the input
lego-mcp render model.mpd -o /tmp/out.png -w 1600 -H 1100 # explicit output + dimensions
lego-mcp render model.mpd --color-mode instance # one color per piece (debug)
lego-mcp render model.mpd --hidden-edges # show internal contact faces as dotted lines
lego-mcp render model.mpd --view-angle 90 # camera 90° around the Y-axis
lego-mcp render model.mpd --turntable 24 # 24 frames sweeping 360° → model_000.png … model_023.png
lego-mcp render model.mpd --background "#0a0a0a" # dark background
lego-mcp render model.mpd \
--watermark "ACME © 2026" \
--watermark-position bottom-right \
--watermark-color "#ffffff" \
--watermark-opacity 200
lego-mcp render --help # full helpAll flags:
| Flag | Default | What it does |
|---|---|---|
<input> (positional) |
— | Path to an .ldr or .mpd file. |
-o, --output PATH |
<input-stem>.png next to input |
Output PNG path. |
-w, --width N |
1200 |
Image width in pixels. |
-H, --height N |
900 |
Image height in pixels. (-h is reserved for help.) |
--color-mode {model,instance,row,rotation} |
model |
model = actual colors; instance = one color per piece; row = color by brick course; rotation = color by orientation. |
--hidden-edges |
off | Draw fully-covered contact faces as dotted guide lines. |
--view-angle DEG |
0 |
Camera azimuth (yaw around Y), pitch constant. Use multiples of 90 for presentation renders — they show the other corners with the same classic iso look. Angles near 45/135/225/315 face the model almost head-on, and orthographic face-on views have no horizontal depth offset, so they read as flat/"sheared" (correct, but hard on the eyes). |
--turntable N |
— | Instead of one image, write N frames sweeping 360°. Each frame goes to <stem>_NNN.png. Overrides --view-angle. |
--background #RRGGBB |
#f5f5f8 |
Image background color. Accepts #fff shorthand. |
--watermark TEXT |
— | Overlay text on the rendered image. |
--watermark-position {top-left,top-right,bottom-left,bottom-right} |
bottom-right |
Where to draw the watermark. |
--watermark-color #RRGGBB |
#282828 |
Watermark text color. |
--watermark-opacity N |
140 |
Watermark alpha (0 transparent .. 255 opaque). |
--watermark-size N |
18 |
Watermark font size in px. |
--watermark-margin N |
14 |
Watermark distance from image edges, in px. |
-q, --quiet |
off | Suppress progress lines on stdout. |
The watermark draws with a TTF font when one is available (Helvetica / Arial / DejaVu probed in order), falling back to PIL's bitmap default on minimal systems so it never hard-fails.
Aggregates a model into an orderable parts list: one row per (part_id, color), sorted most-needed first, with a piece total. The same data the bill_of_materials MCP tool returns in-chat.
lego-mcp bom model.ldr # human-readable table (default)
lego-mcp bom model.ldr -f csv -o parts.csv # spreadsheet
lego-mcp bom model.ldr -f json # structured (scripting)
lego-mcp bom model.ldr -f bricklink -o wanted.xml # BrickLink Wanted List to upload & order| Flag | Default | What it does |
|---|---|---|
<input> (positional) |
— | Path to an .ldr / .mpd file. |
-f, --format {table,csv,json,bricklink} |
table |
Output format. bricklink writes a Wanted List XML you upload at bricklink.com. |
-o, --output FILE |
stdout | Write to a file instead of printing. |
The BrickLink export maps the colors it can verify and flags the rest as warnings (printed to stderr, and left as a comment in the XML) rather than guessing a wrong color id — so double-check any flagged lots before ordering.
Compiles a finished model into a support-respecting assembly order: baseplates and ground parts first, then every part once something it rests on is already placed. (In-chat, the plan_build_sequence / next_build_step tools and the interactive builder mode — start_builder_session → next_unbuilt_step → mark_built → render_progress — do the same.)
lego-mcp instructions model.ldr # full numbered build order
lego-mcp instructions model.ldr --max 20 # first 20 steps
lego-mcp instructions model.ldr --start 20 --max 20 # next page
lego-mcp instructions model.ldr --json # full step payloads (positions, supports)# Generate a hero image for a model with a tagline
lego-mcp render hero.mpd -o hero.png -w 1920 -H 1080 \
--watermark "Built with LegoMCP" --watermark-opacity 90
# Compare two color modes side-by-side (re-run with different --color-mode)
lego-mcp render m.mpd -o m_model.png --color-mode model
lego-mcp render m.mpd -o m_instance.png --color-mode instance
# Spin a 36-frame turntable for an animation
lego-mcp render m.mpd --turntable 36 -o /tmp/spin.png
# → /tmp/spin_000.png … /tmp/spin_035.png — feed into ffmpeg / a GIF tooluv sync --extra dev --extra agent
uv run pytest # 128 tests including a real MCP stdio handshakeArchitecture and decisions live in NOTES.md.
src/lego_mcp/
├── server.py # FastMCP tools + state + tool dispatch
├── parts.py # LDraw .dat parser + catalog + index cache
├── render.py # Built-in isometric renderer w/ studs + sub-faces
├── helpers.py # build_wall / build_room / build_floor / place_on_top / ...
├── connections.py # Stud-receptor mating combinatorics
├── connection_graph.py # Floating + unanchored detection via BFS
├── mount_index.py # Reverse mount search (footprint-bucketed)
├── connectors.py # Typed connector model (Phase 1: stud-only)
├── assembly_ports.py # Module-level exposed-port analysis
├── build_steps.py # Build sequencing (whole + partial-vs-target)
├── prompts.py # 6 prompts + 4 resources
├── agent.py # Standalone Claude-driven agent
└── __main__.py # `lego-mcp` CLI: server / install-library / --help
tests/ # 128 tests, pytest
- Slope renderer is AABB. Slopes draw as cuboids. Use
get_part_info(slope_id)'sorientationblock to reason about slope direction — that's the bigger reasoning win. True wedge geometry is a planned follow-up. - Non-canonical-rotation parts are drawn as oriented cuboids — the matrix is preserved, the box rotates correctly, but those parts don't participate in stud-mating connection analysis (the grid only makes sense axis-aligned). Fine for imported display, intentionally limited for new construction.
- Search is keyword-AND. "vehicle base car chassis" returns 0; "chassis" alone returns lots. Workaround: use single concrete words. Semantic search is on the roadmap.
- Connection model is stud-only. Pins / hinges / axles / clips need
!LDCAD CONNparsing (LDCad-extended LDraw metadata). Phase 2. mount_indexranking is footprint-naive. Often surfaces minifig-leg variants for small targets because they happen to share the footprint. A "kind=" filter (brick/plate/tile vs accessory) would help.- No physics. A structurally-unsound model that's geometrically connected still passes validation.
See NOTES.md for the full decision log + deferred items.
MIT. See LICENSE.





