{
  "markdown": "# AnkusDrive\n\n<!-- mcp-name: io.github.gchen19/ankusdrive -->\n<!-- ↑ Ownership proof for the official MCP Registry (#201): the registry reads it\n     from the README *as uploaded to PyPI*, so it must survive every release and\n     match `name` in server.json. tests/test_mcp_registry.py holds both to that. -->\n\n<!-- Absolute raw.githubusercontent URLs, not repo-relative paths: this README is\n     also the PyPI project page, which resolves relative links against pypi.org.\n     PNG rather than the SVG because raw.githubusercontent serves SVG as\n     text/plain, so browsers refuse to render it as an image. The <picture> gives\n     GitHub a dark variant; PyPI strips <source> and falls through to <img>. -->\n<p align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/gchen19/AnkusDrive/main/logo/wordmark/ankusdrive-wordmark-dark-1280.png\">\n    <img src=\"https://raw.githubusercontent.com/gchen19/AnkusDrive/main/logo/wordmark/ankusdrive-wordmark-1280.png\" alt=\"AnkusDrive — align generative intent with the CAD kernel\" width=\"640\">\n  </picture>\n</p>\n\nA CLI + MCP server that drives [FreeCAD](https://www.freecad.org/) through its Python API so LLMs (and humans at a terminal) can design mechanical parts and run FEM simulations without clicking through the GUI.\n\n## Why\n\nFreeCAD exposes almost everything it does through a Python API — create documents, build sketches, extrude solids, mesh them, run CalculiX/Elmer FEM solves, read back stress/displacement fields. But that API lives inside FreeCAD's embedded Python (`freecadcmd`), which is awkward to call from anywhere else. AnkusDrive wraps it behind two surfaces:\n\n- **CLI** — one-shot commands (`ankusdrive run script.py`, `ankusdrive box --w 10 --d 20 --h 5 -o part.FCStd`) for scripts, CI, and quick iteration.\n- **MCP server** — 280+ structured tools (`new_document`, `add_primitive`, `boolean_op`, `pad`, `add_gear`, `fem_new_analysis`, `fem_run`, `fem_results`) so an LLM agent can model, inspect, and simulate iteratively. Beyond core CAD/FEM this now spans a broad **simulation surface** (thermal, CFD/CHT, EM, acoustics, FSI, injection molding, granular/DEM, optics, multibody) and a **design-control layer** (item/part numbers, recipes, variant families, lifecycle/revision, ECO change orders, versioned interfaces).\n- **Multi-agent orchestration** — a host-side reference layer that lets a *team* of agents partition one product into components, build them in parallel, and merge the pieces back together with the joints actually fitting (see [Multi-agent design](#multi-agent-design)).\n\n## Target environment\n\n- FreeCAD 1.1.x. The `freecadcmd` binary is auto-discovered per-OS (macOS `.app` bundle, Linux `/usr/bin` etc., **Windows** `C:\\Program Files\\FreeCAD 1.1\\bin\\freecadcmd.exe` — version-globbed); override via `$ANKUSDRIVE_FREECADCMD` or rely on PATH. Run `ankusdrive doctor` to see exactly what resolved.\n- Bundled Python, `ccx` (CalculiX), and `gmsh` already ship **inside every FreeCAD install** — the macOS `.app`, the Linux package, and the Windows `bin\\` — so core CAD + structural FEM work on all three with no extra install.\n- Host-side rendering needs `Pillow` and `numpy`; both are installed by AnkusDrive as regular pip deps.\n- **One optional exception:** drawing **PDF/SVG** export (`export_drawing`) renders inside FreeCAD's *bundled* Python, so it needs `reportlab` + `svglib` installed **there** — see [Drawing export (PDF/SVG)](#drawing-export-pdfsvg). DXF export and everything else leave FreeCAD's Python untouched.\n\n## Setup\n\nAnkusDrive is a `pip`-installable package; FreeCAD itself is the only thing you\ninstall separately. The host-side dependencies (`mcp`, `Pillow`, `numpy`) come\nalong with the install. `freecadcmd` is launched as a subprocess and uses its\nown bundled Python — AnkusDrive doesn't touch it.\n\n```bash\n# 1. Install FreeCAD 1.1.x from https://www.freecad.org/\n#    (macOS: drag to /Applications; Linux: distro package or AppImage;\n#     Windows: run the installer — default C:\\Program Files\\FreeCAD 1.1)\n\n# 2. Install AnkusDrive. Pick one:\npipx install ankusdrive                                       # from PyPI — isolated app, `ankusdrive` on PATH\npip install ankusdrive                                        # or into an env you manage yourself\n# unreleased main, or for development from a clone:\npipx install git+https://github.com/gchen19/AnkusDrive.git\ngit clone https://github.com/gchen19/AnkusDrive.git && cd AnkusDrive\npython3 -m venv .venv && .venv/bin/pip install -e .         # `.venv/bin/ankusdrive`\n\n# 3. Smoke-test that the worker can reach FreeCAD, and see the full setup report\nankusdrive ping        # → ping=pong freecad=1.1.1\nankusdrive doctor      # per-item FreeCAD + solver checklist with the exact fix each\n```\n\n> **On Windows, don't follow the block above by hand** — there is one scripted path\n> that does all of it including the MCP registration:\n> [Windows quickstart (PowerShell)](#windows-quickstart-powershell).\n\nAnkusDrive is published on PyPI at\n[pypi.org/project/ankusdrive](https://pypi.org/project/ankusdrive/); the\ndistribution roadmap beyond it (marketplace listings, hosted transport) is\ntracked in [epic #303](https://github.com/gchen19/AnkusDrive/issues/303); the\noriginal phase plan is kept as a design record at\n[`docs/archive/PUBLISHING_PLAN.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/archive/PUBLISHING_PLAN.md).\n\n### Windows quickstart (PowerShell)\n\nWindows is a first-class target (core CAD + CalculiX FEM run natively against a stock\nFreeCAD 1.1 install), and the whole core install is one script — venv, pinned\ndependencies, `doctor`, and the MCP registration line with **resolved absolute paths**:\n\n```powershell\n# 1. Install FreeCAD 1.1.x from https://www.freecad.org/ (default C:\\Program Files\\FreeCAD 1.1).\n#    Nothing needs to go on PATH — AnkusDrive globs the versioned install dir itself.\n\n# 2. Clone and run the core installer. Windows PowerShell 5.1 is enough; no admin needed.\ngit clone https://github.com/gchen19/AnkusDrive.git\ncd AnkusDrive\npowershell -ExecutionPolicy Bypass -File scripts\\install-core.ps1\n```\n\nThat creates `.venv`, installs AnkusDrive with the pins that matter (notably `mcp<2` —\n`mcp` 2.x installs cleanly and then breaks `ankusdrive mcp`), verifies the resolved\n`mcp`/`numpy`/`Pillow`, runs `ankusdrive doctor` + `ankusdrive ping`, completes a real MCP\nstdio handshake, and finally prints your registration block. Useful switches:\n`-Python 'C:\\Program Files\\Python313\\python.exe'` to pick an interpreter,\n`-Extras mbd,fluids` for the pip-wheel solver families, `-Persist` to write the FreeCAD\npath into `%APPDATA%\\ankusdrive\\config.toml` (MCP hosts launch with a minimal\nenvironment, so a `$env:` set in your terminal will **not** reach them).\n\n**3. Register it with your MCP host.** The script prints these with your real paths\nfilled in — a GUI host doesn't inherit your shell `PATH`, so the absolute path matters:\n\n```powershell\n# Claude Code\nclaude mcp add ankusdrive -- C:\\Users\\you\\AnkusDrive\\.venv\\Scripts\\ankusdrive.exe mcp\n\n# Claude Desktop: %APPDATA%\\Claude\\claude_desktop_config.json\n#   { \"mcpServers\": { \"ankusdrive\": {\n#       \"command\": \"C:\\\\Users\\\\you\\\\AnkusDrive\\\\.venv\\\\Scripts\\\\ankusdrive.exe\",\n#       \"args\": [\"mcp\"] } } }\n```\n\nThen restart the host; you should see the `ankusdrive__*` tools appear.\n\nSupported Python: **3.10 – 3.14** (3.14 verified end-to-end on Windows 11 —\n`pip install`, MCP stdio handshake, and `ankusdrive ping` → `freecad=1.1.1`). The script\nchecks your interpreter *before* pip runs, so a too-new CPython says so instead of\nfailing inside the resolver.\n\nOptional solvers (SU2, Elmer, PrusaSlicer, WSL-backed OpenFOAM) come afterwards via\n`scripts\\install-solvers.ps1`. Full per-solver reality, the test suite, and the WSL2\nroute: [`docs/WINDOWS.md`](docs/WINDOWS.md).\n\n### Ubuntu 24.04+ / containers (apt has no FreeCAD)\n\nFreeCAD was **dropped from Ubuntu 24.04's `universe` repo**, so `apt install\nfreecad` finds no candidate there, and upstream's snap/flatpak both fail in a\ncontainer or sandboxed agent environment (no snapd session, no FUSE). The path\nthat works everywhere is the official **AppImage, extracted**:\n\n```bash\nscripts/install-freecad-appimage.sh          # or: scripts/install-solvers.sh freecad\n```\n\nIt downloads the pinned release AppImage, checks its SHA-256, unpacks it with\n`--appimage-extract` (a userspace squashfs unpack — **no FUSE, no root, no\nsnapd**, which is why it works in a container), symlinks `freecadcmd`, `freecad`,\n`ccx` and `gmsh` into `/usr/local/bin`, and then **live-verifies** the result with\n`ankusdrive ping` plus a real CalculiX solve (`ankusdrive fem cantilever`). Without a\nwritable `/opt` it installs to `~/.local/opt/freecad` instead; `--prefix` /\n`--bindir` override both, `--appimage FILE` reuses a download you already have.\n\nThe symlink step is optional: AnkusDrive also probes\n`/opt/freecad/squashfs-root/usr/bin` (and `~/.local/opt/freecad*/…`) directly, so\na hand-extracted AppImage in either prefix is auto-discovered. `ccx` and `gmsh`\nride along inside the AppImage, so structural FEM works off this one download.\n\n### Telling AnkusDrive where FreeCAD lives\n\nAnkusDrive auto-discovers `freecadcmd` in this order: `$ANKUSDRIVE_FREECADCMD`,\nthen `shutil.which(...)` on PATH (trying `freecadcmd`, `FreeCADCmd`, and\n`freecad.cmd`), then a **per-OS** list of standard install locations:\n\n| OS | Auto-discovered locations (newest version wins) |\n|---|---|\n| macOS | `/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd` |\n| Linux | `/usr/bin`, `/usr/local/bin`, `/snap/bin/freecad.cmd`, extracted AppImage under `/opt/freecad*/squashfs-root/usr/bin` or `~/.local/opt/freecad*/…`, `~/.local/bin` |\n| Windows | `C:\\Program Files\\FreeCAD *\\bin\\freecadcmd.exe` (version-globbed), `C:\\Program Files (x86)\\…`, `%LOCALAPPDATA%\\Programs\\FreeCAD *\\bin\\…` |\n\nSo a stock installer on any of the three needs **no configuration**. For a\nnon-default install, point AnkusDrive at the binary directly:\n\n```bash\nexport ANKUSDRIVE_FREECADCMD=/path/to/freecadcmd            # macOS/Linux\n```\n```powershell\n$env:ANKUSDRIVE_FREECADCMD = \"D:\\Apps\\FreeCAD\\bin\\freecadcmd.exe\"   # Windows\n```\n\n`ankusdrive doctor` prints which of the three layers (env / PATH / auto) actually\nresolved FreeCAD, plus every candidate it checked — the fastest way to debug a\n\"FreeCAD not found\" on a new box.\n\n### Drawing export (PDF/SVG)\n\n`export_drawing` builds 2-D mechanical drawings (multi-view PDF/SVG/DXF with\ndimensions) entirely headless. **DXF** uses FreeCAD's own writer and needs\nnothing extra. **PDF and SVG** are composed and rasterised with `reportlab` +\n`svglib`, and because that runs inside the *worker* — FreeCAD's bundled Python,\nnot the host venv — the two packages must be installed into **FreeCAD's Python**:\n\n```bash\n# Resolve FreeCAD's bundled Python from freecadcmd itself (portable across the\n# macOS .app, a Linux distro package, and an extracted AppImage). freecadcmd\n# prints a startup banner after the script output, so match a marker line\n# rather than taking the last line:\nprintf 'import sys; print(\"DPREFIX=\"+sys.prefix)\\n' > /tmp/_fcprefix.py\nFREECAD_PREFIX=\"$(freecadcmd /tmp/_fcprefix.py 2>/dev/null | sed -n 's/^DPREFIX=//p')\"\nFREECAD_PY=\"$FREECAD_PREFIX/bin/python\"      # some builds: $FREECAD_PREFIX/bin/python3\n\n# Pin svglib<1.6 — newer svglib pulls rlPyCairo -> pycairo, a native build we\n# don't use (our drawings are line art, no gradients).\n\"$FREECAD_PY\" -m pip install reportlab \"svglib<1.6\"\n\n# Verify:\n\"$FREECAD_PY\" -c \"import reportlab, svglib; print('drawing export ready')\"\n```\n\nWithout this, `export_drawing` still produces `.dxf`; `.pdf`/`.svg` raise a clear\n`ModuleNotFoundError`. FreeCAD already bundles `Pillow` (reportlab needs it), so\nno separate install is required.\n\n### Wiring it into an MCP host\n\nThe MCP server speaks stdio. Point your host at the `ankusdrive` binary and\nlet it run the `mcp` subcommand.\n\n**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json`\n(macOS) or `%APPDATA%\\Claude\\claude_desktop_config.json` (Windows):\n\n```json\n{\n  \"mcpServers\": {\n    \"ankusdrive\": {\n      \"command\": \"ankusdrive\",\n      \"args\": [\"mcp\"]\n    }\n  }\n}\n```\n\nIf `ankusdrive` isn't on the host process's PATH, use an absolute path —\ne.g. `/Users/<you>/.local/bin/ankusdrive` (pipx default) or\n`/absolute/path/to/AnkusDrive/.venv/bin/ankusdrive` (clone+venv).\n\n**Claude Desktop, one click** — download `ankusdrive-<version>.mcpb` from the\n[latest release](https://github.com/gchen19/AnkusDrive/releases/latest) and open it.\nClaude Desktop sets up its Python environment with `uv`, so no `pipx` step is needed\n— FreeCAD 1.1 still is. The install dialog has one optional field, the FreeCAD\ncommand path, for a FreeCAD that auto-discovery cannot find.\n\n**Claude Code** — register once:\n\n```bash\nclaude mcp add ankusdrive -- ankusdrive mcp\n```\n\n**Other hosts (Cursor, Continue, custom MCP clients)** — same shape: stdio\ntransport, command = `ankusdrive`, args = `[\"mcp\"]`.\n\nAfter restarting the host, you should see 280+ `ankusdrive__*` tools become\navailable. If startup hangs or the host reports a closed connection, run\n`ankusdrive ping` directly — that exercises the same worker boot path with\ncleaner error messages.\n\n## Simulation solvers & review-video demos\n\nThe base install (FreeCAD + `pip install ankusdrive`) covers geometry, the analytic\noracles, and the MCP surface. The heavy simulation families each shell out to an\n**external solver**, discovered at runtime by [`ankusdrive/solvers.py`](https://github.com/gchen19/AnkusDrive/blob/main/ankusdrive/solvers.py)\n(`$ANKUSDRIVE_<SOLVER>_PATH` → `PATH` → standard install dirs). A family whose solver is\nabsent degrades to a clean `{ok: false, reason, install}` dict instead of crashing — check\nwhat currently resolves with **`ankusdrive doctor`** (cross-platform, no server boot needed),\nthe `solve_capabilities` MCP tool, or the install script's `list`. The install script\ninstalls the pip-wheel solvers and provisions the native ones —\n`scripts/install-solvers.sh` on Linux/macOS (apt/conda + source builds), and\n[`scripts/install-solvers.ps1`](https://github.com/gchen19/AnkusDrive/blob/main/scripts/install-solvers.ps1) on Windows (pip extras +\nportable SU2/Elmer/PrusaSlicer downloads; CalculiX auto-detected from FreeCAD's bundle).\n\n**Persistent config:** every `ANKUSDRIVE_*` path can instead live in\n`~/.config/ankusdrive/config.toml` (`%APPDATA%\\ankusdrive\\config.toml` on Windows;\n`ANKUSDRIVE_CONFIG` overrides): `freecadcmd = \"...\"` at top level, one lowercased key per\nsolver var under `[solvers]` (`su2_path`, `elmer_path`, `openfoam_bashrc`, ...). Env vars\nstill win when set; the file is the layer that survives an MCP host's minimal launch\nenvironment. `ankusdrive doctor` reports the file and which layer resolved each value.\n\n**Platform note:** the solver *discovery* layer is fully cross-platform (per-OS install\ndirs, Windows `PATHEXT`/`.exe`, env overrides), so `ankusdrive doctor` gives an honest report\non macOS/Linux/Windows. The **pip-wheel** families (MBD, topology, optics, fluids) install\nidentically everywhere. The **native-binary** families differ by OS — CalculiX ships inside\nevery FreeCAD install; SU2 and PrusaSlicer have good Windows/macOS binaries; Elmer has a\nportable Windows zip but no macOS binaries; the\n**OpenFOAM-backed** families (CFD, FSI, injection molding) still rely on a Linux shell +\nlinker glue and are Linux/WSL/Docker for now. See\n[`docs/WINDOWS.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/WINDOWS.md) and [`docs/MACOS.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/MACOS.md) for the full\nper-solver reality and setup on each OS.\n\nThe review-video demos under [`scratch/`](https://github.com/gchen19/AnkusDrive/tree/main/scratch) turn a solver result into a GIF a human\ncan watch — the **real exported geometry** in motion with the matching oracle overlaid on\nthe frame (written to `artifacts/`). Each needs its family's solver plus `matplotlib`, and\nthe CFD one needs `meshio` (on top of the base `numpy`/`Pillow`):\n\n```bash\npip install matplotlib meshio        # frame rendering + reading OpenFOAM's VTK output\n```\n\n| Review-video demo (`scratch/…`) | Solver it drives | Install |\n|---|---|---|\n| `dog_clutch_cad_sim.py` — rigid-body contact via `p.vhacd` | **PyBullet** (pip wheel) | `pip install 'ankusdrive[mbd]'` |\n| `meshing_gears_video.py` — MBD gear train | **PyBullet** (pip wheel) | `pip install 'ankusdrive[mbd]'` |\n| `modal_shape_video.py` — FEM modal shapes | **CalculiX** `ccx` (FreeCAD FEM) | `apt install calculix-ccx` (Linux); FreeCAD finds `ccx` on `PATH` |\n| `thermal_field_video.py` — transient thermal field | **Elmer** | `apt install elmerfem-csc`; ensure `ElmerSolver` on `PATH` (or set `ANKUSDRIVE_ELMER_PATH`) |\n| `cfd_field_video.py` — CFD field (lid-driven cavity) | **OpenFOAM** + `meshio` | OpenFOAM via apt/conda, then `source <install>/etc/bashrc` (or set `ANKUSDRIVE_OPENFOAM_BASHRC`); `pip install meshio` |\n\nAll of them also use FreeCAD for the geometry/meshing, so run each with the same\ninterpreter that launches the worker — e.g. `.venv/bin/python3 scratch/cfd_field_video.py`.\n\n### Optics\n\nTwo optics engines sit behind the MCP surface, in two licensing/runtime lanes:\n\n| Lane | Tools | Engine | Install |\n|---|---|---|---|\n| Sequential — lens design + optimization | `optics_lens_design`, `optics_lens_optimize`, `optics_raytrace` | **optiland** / rayoptics (MIT/BSD, in-process) | `pip install 'ankusdrive[optics]'` — or `scripts/install-solvers.sh optics` |\n| Non-sequential — tracing through STL solids | `optics_solid_trace` | **KrakenOS** (GPL-3.0, **out-of-process only**) | `pip install 'ankusdrive[optics_gpl]'` — or `scripts/install-solvers.sh optics_gpl` |\n\nThe sequential engines import in-process, so install the `optics` extra into the **same\ninterpreter that launches the worker** (like the other wheels). The non-sequential engine\nis GPL-3.0 and is therefore **never imported by AnkusDrive** — it runs in a separate\nsubprocess ([`ankusdrive/optics_gpl_runner.py`](https://github.com/gchen19/AnkusDrive/blob/main/ankusdrive/optics_gpl_runner.py)), the same\narm's-length boundary used for the GPL Elmer/OpenFOAM binaries. The worker locates a\nPython that can import KrakenOS automatically (from where the wheel is installed); override\nwith `ANKUSDRIVE_OPTICS_GPL_PYTHON=/path/to/python`. Because of that isolation the GPL extra\nis **opt-in**: the no-argument `install-solvers.sh` run installs only the permissive\nextras and prints how to add `optics_gpl`. Rendered examples for both lanes (lens layout,\nspot diagram, optimization, prism TIR, and a ball-lens spherical-aberration study) live in\n[`examples/optics_gallery/`](https://github.com/gchen19/AnkusDrive/tree/main/examples/optics_gallery) — regenerate with\n`.venv/bin/python examples/optics_gallery.py` (and `…_3d.py`, `optics_ball_lens.py`), or\nbootstrap everything in one shot (installs both lanes, then renders every figure):\n\n```bash\nscripts/install-solvers.sh --optics-gallery\n```\n\n## Architecture sketch\n\n```\n ┌────────────┐      ┌────────────┐      ┌──────────────────────┐\n │  MCP host  │ ───► │ AnkusDrive   │ ───► │  freecadcmd worker   │\n │  (Claude)  │      │ (Python)   │ IPC  │  (long-lived Python) │\n └────────────┘      └────────────┘      └──────────────────────┘\n       ▲                    ▲                        │\n       │                    │                        ▼\n       └── CLI user ────────┘               .FCStd / .inp / .vtk\n```\n\nKey decision: **long-lived worker with JSON-over-stdin/stdout**, not subprocess-per-call. FreeCAD startup is ~1–2s; re-paying that per tool call is unacceptable for an interactive agent. The worker is a small Python loop launched under `freecadcmd`, reading commands, dispatching to handlers, returning structured results (including object IDs so follow-up calls can reference created geometry).\n\n## FreeCAD API surface we care about\n\nNotes gathered from the scripting docs and the FEM Python tutorial:\n\n**Core (App):**\n- `App.newDocument(name)` / `App.ActiveDocument` / `doc.recompute()` / `doc.save(path)`\n- `doc.addObject(\"Part::Box\", \"name\")` — typed object creation; properties set after (`box.Height = 5`)\n- `doc.supportedTypes()` for introspection; `obj.TypeId`, `obj.isDerivedFrom(\"Part::Feature\")`\n\n**Modeling:**\n- `Part` — `makeBox`, `makeCylinder`, `makeSphere`, boolean `cut/common/fuse`, fillets, lofts (OpenCASCADE under the hood)\n- `Draft` — 2D primitives, `move`, arrays\n- `Sketcher` + `PartDesign` — parametric sketch-driven solids (most \"real\" mechanical design happens here)\n- `FreeCAD.Vector`, `Placement` for positioning\n\n**FEM (`ObjectsFem` + `femtools`):**\n- `ObjectsFem.makeAnalysis(doc, \"Analysis\")` — container\n- `makeSolverCalculixCcxTools` / `makeSolverElmer` — solver objects with tunables (`GeometricalNonlinearity`, `ThermoMechSteadyState`, …)\n- `makeMaterialSolid` — assign `YoungsModulus`, `PoissonRatio`, `Density`\n- Constraints: `makeConstraintFixed`, `makeConstraintForce`, `makeConstraintPressure`, `makeConstraintDisplacement`, contact/tie/spring, thermal\n- Mesh: `makeMeshGmsh` + `femmesh.gmshtools.GmshTools(...).create_mesh()` (or Netgen)\n- Run: `femtools.ccxtools.FemToolsCcx().run()`\n- Results: iterate `analysis.Group` for `Fem::FemResultObject`; read `.DisplacementVectors`, stress fields\n\n**Headless invocation:**\n- `freecadcmd script.py` — runs script then exits\n- `freecadcmd` with no args — interactive Python REPL (what the worker will drive)\n- `--console`, `-M <moddir>`, `-P <pypath>`, `--pass <args>`, `FreeCAD.ConfigGet(...)` for env info\n- `FreeCADGui` is **not** available headless — keep design logic in `App`/`Part`/`Fem` only\n\n## How an agent reaches FreeCAD: three layers\n\nAnkusDrive exposes FreeCAD through three layers, each with a different audience\nand a different cost-of-use. Knowing which layer a feature lives in tells you\nhow to invoke it.\n\n### Layer 1 — typed MCP tools (the agent surface)\n\n280+ first-class MCP tools span the **core mechanical-design surface**, a broad\n**engineering-analysis / simulation surface**, and a **design-control (PLM)\nlayer**. They have validated parameters, structured returns, and stable handles\nfor chaining. This is the happy path — what an agent uses for things people do\nevery day.\n\n| Domain | What's covered |\n|---|---|\n| Document lifecycle | `new_document`, `open_document`, `save_document`, `list_documents`, `set_active_document`, `close_document`, `restart_worker` |\n| Geometry primitives | `add_primitive` (box/cyl/sphere), `boolean_op`, `export_shape` (STEP/IGES/BREP/STL) |\n| Selection (stable refs) | `list_faces`, `list_edges`, `query_faces`, `resolve_face`, `resolve_edge`, `register_handle`, `verify_feature` |\n| PartDesign | `make_body`, `make_datum_plane`, `make_sketch`, `add_sketch_geometry`, `add_sketch_constraint`, `add_sketch_external`, `close_sketch`, `pad`, `pocket`, `revolve`, `hole`, `loft`, `sweep`, `helix`, `partdesign_fillet`, `partdesign_chamfer`, `linear_pattern`, `polar_pattern`, `mirrored`, `thickness`, `draft` |\n| Direct modeling & feature ops | `fillet_edges`, `chamfer_edges`, `shell_solid`, `add_rib`, `engrave_text`, `oring_groove`, `transform`, `scale_shape`, `copy_shape` |\n| Parametric components | `add_gear`, `add_rack`, `add_sprocket`, `add_pulley`, `add_spring`, `add_fastener`, `add_bearing`, `add_thread`, `list_thread_options` |\n| Metrology & inspection | `measure_distance`, `measure_angle`, `bounding_box`, `check_shape`, `section_view`, `min_clearance`, `envelope_check`, `interference_check` |\n| Generic property access | `get_object`, `set_property` |\n| Functional intent & invariants | `annotate_face`, `list_face_roles`, `classify_face_sides`, `check_airtight_path`, `declare_intent`, `verify_intent` |\n| Performance contracts | `declare_performance`, `verify_performance` — a quantitative spec (\"Cd ≤ 0.30 at 30 m/s\", \"Δp ≤ 50 Pa\", \"first mode ≥ 200 Hz\") persisted on the part and re-proved after every edit, with a three-state verdict: a measurement whose uncertainty band straddles the limit is `indeterminate` (escalate), never a pass. The contract is consulted at the gates (#261): `merge_assembly`, `substitutability_check` and `component_contract_check` read the last recorded verdict, so an unmet spec blocks a merge and an *unverified* one is reported as its own outcome rather than passing silently |\n| Design-space studies (DOE) | `study_submit` — sweep recipe/tool parameters over a full grid or a Latin hypercube and keep the WHOLE search as a table, not just the last point. A response is any AnkusDrive tool + a metric path (including a whole `verify_performance` verdict, so points stay comparable across fidelity tiers); screening responses evaluate inline, solver responses fan out concurrently behind one collector job. Sampling is deterministic from `seed`, so re-submitting a crashed or widened study re-runs only the new points and reports the rest as cache hits |\n| Optimize to a spec | `optimize_submit` — vary bounded parameters until every constraint passes, then report whether it was **proven**. A bounded Nelder-Mead (derivative-free; there is no adjoint through a CFD solve) over the same objective/constraint mapping the contract layer uses, with a screen→solver fidelity ladder. Two rules come from the contract layer: an `indeterminate` constraint is a measurement problem, not a failed step (it neither attracts nor repels the search), and convergence is not proof — a margin narrower than its own uncertainty band is reported unproven, however tidily the simplex converged |\n| Assembly & interfaces | `make_assembly`, `add_part`, `list_assembly_parts`, `merge_assembly`, `publish_interface`, `interface_align_check`, `assembly_lock`, `assembly_lock_check`, `bom_extract` |\n| Drawings (TechDraw, headless) | `make_drawing_page`, `add_projection_group`, `add_section_view`, `add_thumbnail`, `add_dimension`, `add_annotation`, `add_feature_note`, `add_gdt_callout` (feature control frames), `set_title_block`, `fit_page`, `export_drawing` (PDF/SVG/DXF), plus completeness/legibility gates `drawing_gate`, `drawing_legibility` |\n| Inspection (first-article) | `balloon_drawing` (revision-stable balloon numbering), `inspection_plan` (characteristic list with a measurement method per row, by the gauge-maker's 10:1 rule), `fai_report` (AS9102-Form-3-*shaped* CSV/SVG/PDF — not a certified submission); `drawing_gate(require_ballooned=True)` makes a ballooned print a release requirement |\n| Release packages (vendor / RFQ) | `release_package` — the one-call deliverable bundle for an item at a revision: STEP + drawings (PDF/SVG/DXF) + recursive BOM + inspection package + a blake2b-checksummed manifest. Gated *before* anything is written: the item must be in a releasable lifecycle state (or `draft=True`, which watermarks every artifact PRELIMINARY), `drawing_gate` must pass for every included page, and the title block's part number / revision / material must match the items registry — a mismatch is a failure with a naming diff, never a silent fix. Byte-reproducible (the same revision re-releases to identical checksums), stamps the ECO into the manifest and the print, and `rfq=True` adds quantity breaks + the `cost_estimate` rollup while dropping internal-only artifacts |\n| Off-the-shelf parts (buyability) | `catalog_search` (what standard components exist, in which sizes and stocked lengths), `catalog_nearest` (snap a wanted size to a real one — asked for an M4×13 it answers 12 and 16), `catalog_check`, `standard_part_designate` (canonical designations: `ISO 4762 M4×12 A2`, `608-2RS`, `AS568-214 NBR70`, stamped on the part at creation), `designation_check`, `bom_extract(orderable=True)` (per-line stocked / not_stocked with alternatives) |\n| Visual feedback | `render_view`, `render_views` (8 preset views, multi-view sheets), `render_photoreal` / `render_photoreal_submit`, `render_capabilities` |\n| FEM (FreeCAD/CalculiX/Elmer) | `fem_new_analysis`, `fem_set_solver`, `fem_set_material`, `fem_set_nonlinear_material`, `fem_add_constraint` (fixed/force/pressure/displacement/temperature/heatflux/initial_temperature), `contact_setup`, `fem_mesh`, `fem_mesh_refinement`, `fem_modal`, `fem_buckling`, `fem_run`, `fem_results`, `fem_result_probe` (stress/disp/temp at a point or face), `fem_modal_results`, `fem_buckling_results`, `fem_thermal_results`, plus the legacy `fem_cantilever_demo` |\n| Engineering oracles & hand-calcs | machine elements (`gear_rating`, `bearing_life`, `belt_drive`, `spring_check`, `bolted_joint_check`, `press_fit_stress`, `seal_check`), structural (`beam_modal`, `beam_buckling`, `plate_check`, `hertz_contact`, `elastica_deflection`, `plastic_collapse`, `random_vibration`, `harmonic_response`), durability (`fatigue_check`, `fracture_check`, `creep_flag`, `wear_estimate`), thermal (`thermal_lumped`, `thermal_transient_1d`, `thermal_composite_wall`, `h_estimate`), tolerance/GD&T (`tolerance_stackup`, `fit_check`, `fit_class`, `gdt_check`) |\n| Simulation families (external solvers, async) | screens + full solves that shell out to OpenFOAM/Elmer/CalculiX/openEMS/YADE/KrakenOS, most via a submit→poll job pattern: thermal/CHT (`cht_channel_submit`, `cht_graetz_submit`, `thermal_transient_submit`, `thermal_radiation_submit`), CFD (`cfd_pipe_flow`, `cfd_body_drag`, `cfd_internal_flow_submit`, `cfd_external_flow_submit` — including the virtual wind tunnel: hand it a solid and get Cd/Cl/Cm from an integrated force, gated against the sphere drag curve; every steady solve carries a trust block (convergence, checkMesh, measured y+) and `cfd_mesh_independence_submit`/`grid_convergence` put a Richardson/GCI error band on geometry with no analytic twin), EM (`em_skin_depth`, `em_dc_resistance`, `em_field`, `em_conduction_submit`, `em_induction_submit`, `em_fullwave_submit`), acoustics (`acoustic_screen`, `acoustic_fem_submit`, `acoustic_radiation_submit`), FSI (`fsi_*`), molding (`molding_screen`, `molding_fill_submit`, `molding_warpage_submit`), granular/DEM (`granular_screen`, `dem_pack_submit`, `dem_flow_submit`), optics (`optics_lens_design`, `optics_lens_optimize`, `optics_raytrace`, `optics_solid_trace`), multibody (`mechanism_kinematics`, `mechanism_simulate_submit`), topology (`topology_optimize_submit`, `topology_to_solid`) |\n| Async jobs | `job_status`, `job_result`, `job_list` — poll/collect any `*_submit` long-running solve; `solve_capabilities` reports which solvers currently resolve |\n| Materials & fluids | `material_list`, `material_get`, `material_select`, `fluid_props` — mechanical-property / molding / CoolProp thermophysical corpora behind a typed lookup |\n| Sheet metal | `sheet_base` (base flange), `sheet_flange` / `sheet_tab` / `sheet_hem` (bends placed by stable edge tag), `sheet_unfold` (K-factor flat pattern + per-bend allowance/deduction, with the K in force and its source echoed into every result), `sheet_refold` (round-trip verification against the folded solid), `sheet_flat_export` (layered DXF — CUT / BEND_UP / BEND_DOWN, the file a laser/brake shop quotes from), `sheet_check` (min bend radius by material, min flange, hole-to-bend, refold collision) |\n| Manufacturing & Design-for-X | `dfm_check` (also runs the sheet-metal press-brake rules when handed a sheet part), `dfa_check`, `moldability_check`, `optics_moldability_check`, `pack_check`, `cost_estimate`, `slice_estimate`, `slice_gcode_submit`, `laminate_properties`, `drop_impact` |\n| CNC (machinability + machining time) | `cnc_machinability_check` (setups from the tool-approach census, undercuts, tool L/D, sharp/small internal corners, thin walls — pure geometry, no CAM engine), `cnc_time_estimate` (material-removal-rate model: removed volume / MRR plus finishing area, ±50 % against the flat table's ±100 %; feeds `cost_estimate(machine_time_hr=…)`) |\n| Tolerance ↔ cost | `tolerance_cost_check` (per-dimension IT grade, the cheapest process that holds it naturally, a relative cost index, and a flag when a dimension is tighter than the declared process can hold without a secondary operation), `suggest_loosening` (*the loosest tolerance that works* — greedy loosening, every step re-verified against `tolerance_stackup`'s cpk); `cost_estimate(tolerance_class=…)` puts the same curve in the rollup |\n| Design control / PLM | items & part numbers (`items_new`, `items_validate`, `items_resolve`, `items_check_manifest`), recipes (`recipe`, `recipe_list`, `recipe_schema`, `recipe_validate`), feature templates (`feature_instantiate`, `feature_list`, `feature_schema`, `feature_validate`), variant families (`family_materialize`, `family_validate`), lifecycle/revision (`lifecycle_transition`, `lifecycle_editable`, `lifecycle_classify_change`, `lifecycle_apply_change`), change control (`eco_create`, `eco_validate`, `change_impact`, `where_used`, `baseline_create`, `baseline_verify`), interface registry + substitutability (`get_interface`, `substitutability_check`), projects (`scaffold_project`, `project_validate`, `project_check_references`, `project_resolve_manifest`) |\n| Operations | `transaction_open`, `transaction_commit`, `transaction_abort` |\n\nAll tools return JSON; geometry-creating tools return a `handle` (e.g.\n`pad_1`) that subsequent calls reference. The heavy simulation families return\na `{ok: false, reason, install}` dict (rather than crashing) when their solver\nisn't installed — see [Simulation solvers](#simulation-solvers--review-video-demos).\n\n### Layer 2 — generic property reflection\n\nFor the long tail of \"I just need to tweak this one property\" without a\ndedicated tool:\n\n- **`get_object(handle)`** — dump every entry in `obj.PropertiesList` with\n  Quantities → float (mm/deg), Vectors → list, Placements → dict.\n- **`set_property(handle, name, value)`** — set any single property by name.\n\nUse this when a typed tool exists for the object kind but doesn't expose the\nexact property you need (e.g. `Refine` on a Pad, `Sections` ordering on a\nLoft, internal tunables on a CCX solver).\n\n### Layer 3 — `run_script` (the universal escape hatch)\n\nFor features that have **no first-class MCP tool at all** — e.g. Path\nworkbench (CAM toolpaths), Surface workbench, Arch/BIM, Spreadsheet,\nTechDraw dimensions, contact/spring FEM constraints, B-spline sketcher\noperations, expression-engine bindings, anything in a workbench AnkusDrive\ndoesn't wrap.\n\n```python\nrun_script(code='''\nimport Path\njob = Path.Job.Create(\"Job\", [_resolve(\"pad_1\")])\n__result__ = {\"job_name\": job.Name}\n''')\n```\n\nInside the script, the worker pre-injects: `App` / `FreeCAD`, `Part`,\n`ObjectsFem`, plus `_register(prefix, obj)` / `_resolve(handle)` /\n`_handles` so scripts can register new objects into the same handle\nregistry that typed tools use. Set `__result__ = ...` to a JSON-serializable\nvalue to return data; print statements go to /dev/null.\n\nThe escape hatch costs more (the agent has to write FreeCAD Python) but\nmakes the entire FreeCAD API reachable. The Phase 2 plan's \"After Phase 2\"\nsection calls out which run_script patterns deserve promotion to typed\ntools — that's how the surface grows over time.\n\n### What the CLI is (and isn't)\n\nThe CLI is **not the agent surface** — it's a human-debugging + transport\ntool. Seven subcommands:\n\n| Command | Purpose |\n|---|---|\n| `ankusdrive ping` / `version` | Health check — boot a worker, prove FreeCAD is reachable |\n| `ankusdrive box` / `cylinder` | Single-shot primitive → .FCStd (manual smoke tests) |\n| `ankusdrive export <in.FCStd> -o <out.step>` | Headless format conversion |\n| `ankusdrive run <script.py>` | Execute arbitrary FreeCAD Python in a live worker (set `__result__` to return JSON) |\n| `ankusdrive mcp` | **Start the MCP server over stdio** — this is how an MCP host launches AnkusDrive |\n| `ankusdrive fem cantilever` | Run the built-in canned demo |\n\nAgents do not invoke the CLI. They speak MCP via stdio after the host has\nlaunched `ankusdrive mcp`. The CLI's job is (a) to start that server and\n(b) to give a human a way to poke at the worker without writing an MCP\nclient.\n\n### Decision rule\n\n| Need | Use |\n|---|---|\n| Standard CAD/FEM operation | First-class MCP tool (Layer 1) |\n| Tool exists but I need property X | `get_object` / `set_property` (Layer 2) |\n| Workbench / API not wrapped at all | `run_script` (Layer 3) |\n| Smoke test from a shell, or stand up MCP | CLI |\n\n## Multi-agent design\n\nThe roadmap above is about deepening what *one* agent can do. The\n[`orchestration/`](https://github.com/gchen19/AnkusDrive/tree/main/orchestration) layer is about *many* agents sharing the\nwork: split a product into components and subassemblies, build those in\nparallel (each agent cold, seeing only its own contract slice), then merge the\nwhole back up with the joints actually fitting. The design is written up in\n[`docs/MULTI_AGENT.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/MULTI_AGENT.md); it targets **partition + merge**,\nnot shared co-editing of one live document (a single worker = one\n`App.ActiveDocument`, so concurrent mutation is a non-goal for now).\n\n**Concurrent agents on one MCP server — workspaces.** FastMCP runs sync tools in\na thread pool, so a host can have several tool calls in flight at once. The\nserver keeps a *pool* of named **workspaces**, each its own freecadcmd process\nwith its own `App.ActiveDocument` and handle registry. Each concurrent agent\nclaims its own workspace with `use_workspace(name)` at the start of its session;\n**handles and documents do not cross workspaces**. A client that never calls\n`use_workspace` sees the historical single-worker behavior byte-for-byte\n(everything routes to the `default` workspace). `Worker.call()` is internally\nserialized so two threads can never interleave the stdin/stdout protocol on one\nprocess. The pool is capped (`ANKUSDRIVE_MAX_WORKSPACES`, default 4) and idle\nworkspaces are reaped (`ANKUSDRIVE_WORKSPACE_IDLE_S`, default 900s) so abandoned\nsessions don't leak processes; `list_workspaces` / `close_workspace` manage it.\n\nThe split of responsibilities is deliberate:\n\n- **AnkusDrive ships the thin, tool-agnostic primitives** that make a merge\n  verifiable — `publish_interface` (declare a component's mating frames),\n  `merge_assembly` (combine component files into one assembly), and the\n  **gates** that decide whether a merge is sound: `interface_align_check`\n  (do published frames line up?), `interference_check` (do solids collide?),\n  `envelope_check` (does it fit its bounding budget?), plus an\n  `assembly_lock` / `assembly_lock_check` contract lockfile. These are real\n  MCP tools usable by any host.\n- **`orchestration/` is the host-side *reference* coordinator** — explicitly\n  **not** part of the `ankusdrive` package. Given a free-text brief it\n  `decompose`s it into a validated manifest, fans out one builder agent per\n  component, `merge_assembly`s them, reads the gates, and on failure\n  **renegotiates** — re-dispatching only the components implicated by the\n  failing gate — up to a round budget. It runs against a real Anthropic client\n  or a scripted stub (`ScriptedClient`) for free dry runs; the merge and gates\n  are real worker calls either way. Any host (Claude, another tool, a human)\n  can use it, replace it, or ignore it — the only contract that matters is the\n  manifest + the component files on disk.\n\nHow well partition+merge holds up is measured by a dedicated eval ladder\n(`tests/test_multiagent_m1.py` / `_m2.py`, runnable in CI) with hard-oracle\nmerge gates and a single-agent baseline — see\n[`tests/MULTI_AGENT_EVAL.md`](https://github.com/gchen19/AnkusDrive/blob/main/tests/MULTI_AGENT_EVAL.md). Early experiments have\npartition performing at or above the single-agent baseline on the harder toys.\n\n## Designs, not just parts — the design-control layer\n\nMulti-agent orchestration partitions *one* product across a team. A separate\naxis makes a *design* (not just a part) something you can parameterize, vary,\nand evolve under control — the mechanisms a PLM/PDM workflow expects, mapped\nonto AnkusDrive's deterministic, headless, git-diffable grain. The keystone\ninsight: **the build recipe is the feature tree; the parameters are its inputs;\nregeneration is re-running the recipe** — so AnkusDrive gets parametric regen and\nfamily tables without a live in-file expression engine. The full scoping and\nrationale is in [`docs/DESIGN_HIERARCHY.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/DESIGN_HIERARCHY.md); the\nagent-facing judgment lives in the [`design-modularly`](https://github.com/gchen19/AnkusDrive/tree/main/skills/design-modularly)\nskill.\n\n- **Parametric hierarchy** — *recipes* (`recipe`, `recipe_validate`) are named,\n  declared-input build templates (AnkusDrive's PowerCopy/UDF *and* its intra-part\n  parametric model); a *relations* DAG drives driven dimensions from master\n  parameters by formula (`pitch_d = module * teeth`, arithmetic only — no\n  iterative solve, no double-driving); *feature templates* (`feature_instantiate`)\n  graft reusable features onto reference geometry by name; the typed\n  [`units`](https://github.com/gchen19/AnkusDrive/blob/main/ankusdrive/units.py) layer rejects dimensionally-wrong inputs at the\n  door (`\"5 N\"` for a length is an error, not a silent mis-scale).\n- **Variant families** — `family_materialize` expands a row × column design\n  table into a set of variants deterministically, running the recipe per row and\n  allocating part numbers in table order.\n- **Identity & lifecycle** — *items* (`items_new`) give a part a stable\n  part-number identity decoupled from its file path; a *lifecycle* state machine\n  (`lifecycle_transition`: in_work → in_review → released → obsolete) enforces\n  released-immutability, and a Form/Fit/Function predicate decides revision bump\n  vs. new part number on a change.\n- **Change control** — *ECOs* (`eco_create`) are first-class change records;\n  `where_used` / `change_impact` compute blast radius over the dependency graph\n  before you commit; `baseline_create` / `baseline_verify` pin reproducible\n  snapshots.\n- **Versioned interfaces** — an interface-type registry (`get_interface`,\n  `nema17_face@1`-style named/versioned types) plus a Liskov\n  `substitutability_check` gate enforce Form/Fit/Function compatibility as code,\n  so a swapped part is verified to actually mate.\n- **Projects** — `scaffold_project` + `project_validate` /\n  `project_check_references` promote the multi-agent directory convention to a\n  first-class `project.json` (manifest-of-manifests) with a master/skeleton\n  single-source-of-truth slot and reference-integrity guards.\n\nLike the merge gates, these are thin, deterministic, mostly FreeCAD-free\nprimitives — the logic layers import and test without launching a worker.\n\n## Status\n\nPhase 3 closed 2026-05-10 (v0.3.0). The core mechanical-design surface from\nPhase 2 (2026-04-25) is intact; Phase 3 layered intent-encoding APIs on top of\nit. Since then the tool surface has grown from ~100 to **280+ tools** across\nseveral waves: a command-tier expansion (parametric components + direct feature\nops + metrology), the **multi-agent orchestration** layer, a broad\n**engineering-analysis + external-solver simulation surface** (thermal/CFD/CHT/\nEM/acoustics/FSI/molding/granular/optics/multibody), and a **design-control\n(PLM) layer** (items, recipes, variant families, lifecycle, ECO/change,\nversioned interfaces, projects).\n\n- **Worker + transport** — long-lived `freecadcmd` worker, newline-JSON over stdio with stdio hygiene (FreeCAD C++ chatter redirected off the protocol fd).\n- **CLI** — `ping`, `version`, `box`, `cylinder`, `export`, `run`, `mcp`, `fem cantilever`, plus top-level `--version`.\n- **MCP server** — FastMCP over stdio, 280+ typed tools across document lifecycle, primitives, selection (face/edge tags), full PartDesign (sketcher + pad/pocket/revolve/hole/loft/sweep/helix/fillet/chamfer/pattern/mirror/thickness/draft), direct-modeling feature ops, parametric components, metrology/inspection, generic property reflection, mass properties, assembly + interface gates, TechDraw (incl. headless PDF/SVG/DXF export, dimensions, gates), multi-view + photoreal rendering, FEM (static + modal + buckling + thermal + nonlinear + result-probe), the engineering-analysis oracles and external-solver simulation families (sync + async `*_submit`/`job_*`), the materials/fluids corpora, Design-for-X / manufacturing checks, the design-control (PLM) layer, and transactions.\n- **Command tiers 1–3** — 21 new tools: parametric components (`add_gear`, `add_rack`, `add_sprocket`, `add_pulley`, `add_spring`, `add_fastener`, `add_bearing`, `add_thread`), direct feature ops (`fillet_edges`, `chamfer_edges`, `shell_solid`, `add_rib`, `engrave_text`, `oring_groove`, `transform`, `scale_shape`, `copy_shape`), and metrology/inspection (`measure_distance`, `measure_angle`, `bounding_box`, `check_shape`, `section_view`, `min_clearance`).\n- **Multi-agent orchestration** — AnkusDrive ships the thin merge primitives + gates (`publish_interface`, `merge_assembly`, `interface_align_check`, `envelope_check`, `assembly_lock`/`_check`); the host-side reference coordinator (`orchestration/`) decomposes a brief, fans out per-component builders, merges, gates, and renegotiates. See [Multi-agent design](#multi-agent-design) and [`docs/MULTI_AGENT.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/MULTI_AGENT.md).\n- **Phase 3 intent-encoding additions** — `direction='into_body'|'away_from_body'` and `through='wall'|'body'` on pocket/hole (ray-cast wall depth handles hollow shells correctly); `intended_for='print'|'machine'|'drawing'` on hole drives ModelThread; `verify_feature` diffs actual-vs-expected volume change to catch silent failures; visibility hygiene at save hides consumed inputs; `register_handle` + `run_script` auto_register close the escape-hatch one-way trapdoor; `list_thread_options` surfaces the coupled ThreadType/ThreadSize enums dynamically; revolve has an OCCT pre-check that flags axis-coincident edges with an actionable error.\n- **Selection layer** — `list_faces` / `list_edges` / `query_faces` / `resolve_*` produce stable geometric tags that survive edits; FEM constraints take tags directly.\n- **Rendering** — host-side software rasterizer (`ankusdrive/render.py`) with per-pixel z-buffer (`render_view` / `render_views` return PNGs as MCP `ImageContent`), plus photoreal `render_photoreal` via the FreeCAD Render addon + an external renderer (POV-Ray / LuxCore / Appleseed / Cycles / OSPRay / PBRT). Support matrix, install, and limitations: [`docs/RENDERING.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/RENDERING.md).\n- **Simulation surface** — engineering-analysis oracles (machine elements, structural, durability, thermal, tolerance/GD&T) plus external-solver families that shell out to OpenFOAM / Elmer / CalculiX / openEMS / YADE / KrakenOS, discovered at runtime by [`ankusdrive/solvers.py`](https://github.com/gchen19/AnkusDrive/blob/main/ankusdrive/solvers.py) and degrading cleanly when absent. Long solves use an async submit→poll job pattern (`*_submit` + `job_status`/`job_result`/`job_list`). Catalog and result schemas: [`docs/SIMULATION_TOOLS.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/SIMULATION_TOOLS.md); proof harness: [`docs/SIMULATION_EXAMPLES.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/SIMULATION_EXAMPLES.md). Materials/fluids back these via `material_*` and `fluid_props` (mechanical-property / molding / CoolProp corpora).\n- **Design-control (PLM) layer** — recipes + a relations DAG (parametric regen), feature templates, variant families from a design table, item/part-number identity, a lifecycle/revision state machine, ECO change records with where-used/impact + baselines, a versioned interface registry + Liskov substitutability gate, and project containers with reference-integrity guards. Scoping + rationale: [`docs/DESIGN_HIERARCHY.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/DESIGN_HIERARCHY.md). See [Designs, not just parts](#designs-not-just-parts--the-design-control-layer).\n- **Tests** — ~980 test functions across ~90 files (worker / MCP / CLI / render / determinism / edit stability / negative paths / perf / multi-agent / simulation families / molding / PLM layer), runnable via `tests/run_all.sh` (Linux/macOS) or `tests/run_all.ps1` (Windows — single-interpreter, skips the Linux-only solver families; see [`docs/WINDOWS.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/WINDOWS.md)). Reliability harness (Layer A classification, B diff-detection, C agent-loop closure) is gated behind `RUN_RELIABILITY=1`; see [`tests/RELIABILITY.md`](https://github.com/gchen19/AnkusDrive/blob/main/tests/RELIABILITY.md).\n\nSee [`docs/ROADMAP.md`](https://github.com/gchen19/AnkusDrive/blob/main/docs/ROADMAP.md) for the Phase 1/2 per-slice record — a\nchangelog of how the surface above was built, frozen at the close of Phase 2. Open\nwork (FEM contact/spring/tie refinements, fully async `fem_run`, `feature_tree`\nintrospection, deeper external-solver integrations) lives on the\n[issue tracker](https://github.com/gchen19/AnkusDrive/issues), not in that file.\n\nThe committed showcase — every GIF, render, drawing and exported solid the docs point\nat, with the script that regenerates each one — is indexed in\n[`artifacts/README.md`](https://github.com/gchen19/AnkusDrive/blob/main/artifacts/README.md).\n\n## Open questions\n\n- **Error model**: FreeCAD raises plain Python exceptions from C++; worker catches and serializes them, but stack context across the JSON boundary is still lossy.\n- **Async / concurrency**: multi-doc shipped (`list_documents` / `set_active_document` / `close_document`), and the long-running external solvers run off the channel via the `*_submit` + `job_*` pattern, but the in-worker `fem_run` itself is still synchronous and blocks the MCP channel for the duration of a CalculiX/Elmer solve.\n- **macOS Gatekeeper / sandboxing**: `freecadcmd` launched from a non-interactive context may hit quarantine issues. This has never been confirmed either way — tracked in [#310](https://github.com/gchen19/AnkusDrive/issues/310), which will either document the fix or delete this caveat.\n\n## License\n\nLicensed under the [Apache License, Version 2.0](https://github.com/gchen19/AnkusDrive/blob/main/LICENSE). Contributions\nsubmitted to this project are licensed under the same terms (Apache 2.0\n§5: inbound = outbound), which means contributors retain copyright but\ngrant the project — and everyone downstream — a perpetual, irrevocable\nlicense to use their work, including a patent grant. The intent is to keep\nthe project welcoming to contributors while ensuring nobody can later\nre-proprietize what they contributed.\n\nThe *code* is Apache-2.0; the *name* is not. Apache-2.0 §6 grants no trademark\nrights, so the AnkusDrive word mark and the brand assets in\n[`logo/`](https://github.com/gchen19/AnkusDrive/tree/main/logo)\nare covered separately — see\n[`TRADEMARKS.md`](https://github.com/gchen19/AnkusDrive/blob/main/TRADEMARKS.md)\nfor what you may do without asking (referring to the project, compatibility\nclaims, redistribution, packaging, and forking all qualify) and\n[`NOTICE`](https://github.com/gchen19/AnkusDrive/blob/main/NOTICE) for the\nattribution a redistributor must carry.\n\n## References\n\n- [FreeCAD Scripting Basics](https://github.com/FreeCAD/FreeCAD-documentation/blob/main/wiki/FreeCAD_Scripting_Basics.md)\n- [Python scripting tutorial](https://github.com/FreeCAD/FreeCAD-documentation/blob/main/wiki/Python_scripting_tutorial.md)\n- [Start up and Configuration](https://github.com/FreeCAD/FreeCAD-documentation/blob/main/wiki/Start_up_and_Configuration.md)\n- [FEM Workbench](https://github.com/FreeCAD/FreeCAD-documentation/blob/main/wiki/FEM_Workbench.md)\n- [FEM Tutorial Python](https://github.com/FreeCAD/FreeCAD-documentation/blob/main/wiki/FEM_Tutorial_Python.md) — full cantilever example\n- [Model Context Protocol](https://modelcontextprotocol.io/)\n",
  "bytes": 52102,
  "sha": "b48b8adcd08deda884028a916932f74d24b6ebf58c857bc837e84f196b098814",
  "repo_slug": "gchen19/ankusdrive",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_gchen19_ankusdrive_cf63e1c2/readme"
}