cadpilot 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. cadpilot-0.4.0/.gitattributes +7 -0
  2. cadpilot-0.4.0/.github/workflows/ci.yml +38 -0
  3. cadpilot-0.4.0/.github/workflows/publish.yml +32 -0
  4. cadpilot-0.4.0/.gitignore +34 -0
  5. cadpilot-0.4.0/.python-version +1 -0
  6. cadpilot-0.4.0/AGENTS.md +190 -0
  7. cadpilot-0.4.0/CHANGELOG.md +204 -0
  8. cadpilot-0.4.0/LICENSE +22 -0
  9. cadpilot-0.4.0/PKG-INFO +173 -0
  10. cadpilot-0.4.0/README.md +147 -0
  11. cadpilot-0.4.0/README.zh-CN.md +147 -0
  12. cadpilot-0.4.0/addon/CADPilot/Init.py +1 -0
  13. cadpilot-0.4.0/addon/CADPilot/InitGui.py +62 -0
  14. cadpilot-0.4.0/addon/CADPilot/rpc_server/__init__.py +2 -0
  15. cadpilot-0.4.0/addon/CADPilot/rpc_server/assembly_ops.py +512 -0
  16. cadpilot-0.4.0/addon/CADPilot/rpc_server/commands.py +192 -0
  17. cadpilot-0.4.0/addon/CADPilot/rpc_server/feature_ops.py +660 -0
  18. cadpilot-0.4.0/addon/CADPilot/rpc_server/geometry_query.py +598 -0
  19. cadpilot-0.4.0/addon/CADPilot/rpc_server/gui_dispatch.py +208 -0
  20. cadpilot-0.4.0/addon/CADPilot/rpc_server/ip_filter.py +73 -0
  21. cadpilot-0.4.0/addon/CADPilot/rpc_server/joint_ops.py +421 -0
  22. cadpilot-0.4.0/addon/CADPilot/rpc_server/object_factory.py +36 -0
  23. cadpilot-0.4.0/addon/CADPilot/rpc_server/property_mapper.py +140 -0
  24. cadpilot-0.4.0/addon/CADPilot/rpc_server/rpc_server.py +1051 -0
  25. cadpilot-0.4.0/addon/CADPilot/rpc_server/serialize.py +108 -0
  26. cadpilot-0.4.0/addon/CADPilot/rpc_server/settings.py +42 -0
  27. cadpilot-0.4.0/addon/CADPilot/rpc_server/sketcher_ops.py +471 -0
  28. cadpilot-0.4.0/addon/CADPilot/rpc_server/trim_ops.py +47 -0
  29. cadpilot-0.4.0/addon/CADPilot/rpc_server/view_manager.py +148 -0
  30. cadpilot-0.4.0/docs/DESIGN.md +154 -0
  31. cadpilot-0.4.0/docs/DESIGN.zh-CN.md +154 -0
  32. cadpilot-0.4.0/examples/ModernBicycle.FCStd +0 -0
  33. cadpilot-0.4.0/pyproject.toml +72 -0
  34. cadpilot-0.4.0/src/cadpilot/__init__.py +0 -0
  35. cadpilot-0.4.0/src/cadpilot/assembly_state.py +192 -0
  36. cadpilot-0.4.0/src/cadpilot/freecad_client.py +319 -0
  37. cadpilot-0.4.0/src/cadpilot/guidance.py +192 -0
  38. cadpilot-0.4.0/src/cadpilot/operations/__init__.py +73 -0
  39. cadpilot-0.4.0/src/cadpilot/operations/assembly.py +219 -0
  40. cadpilot-0.4.0/src/cadpilot/operations/core.py +1102 -0
  41. cadpilot-0.4.0/src/cadpilot/pattern_store.py +127 -0
  42. cadpilot-0.4.0/src/cadpilot/prompt_text.py +131 -0
  43. cadpilot-0.4.0/src/cadpilot/py.typed +0 -0
  44. cadpilot-0.4.0/src/cadpilot/responses.py +25 -0
  45. cadpilot-0.4.0/src/cadpilot/server.py +948 -0
  46. cadpilot-0.4.0/src/cadpilot/server_state.py +26 -0
  47. cadpilot-0.4.0/src/cadpilot/session_state.py +253 -0
  48. cadpilot-0.4.0/src/cadpilot/tool_docs.py +266 -0
  49. cadpilot-0.4.0/tests/conftest.py +404 -0
  50. cadpilot-0.4.0/tests/live_sketch_verify.py +192 -0
  51. cadpilot-0.4.0/tests/test_assembly.py +181 -0
  52. cadpilot-0.4.0/tests/test_assembly_session.py +242 -0
  53. cadpilot-0.4.0/tests/test_assembly_state.py +146 -0
  54. cadpilot-0.4.0/tests/test_cad_features.py +99 -0
  55. cadpilot-0.4.0/tests/test_cad_sessions.py +435 -0
  56. cadpilot-0.4.0/tests/test_client_reconnect.py +155 -0
  57. cadpilot-0.4.0/tests/test_connectivity_audit.py +117 -0
  58. cadpilot-0.4.0/tests/test_geometry_sensing.py +67 -0
  59. cadpilot-0.4.0/tests/test_guidance.py +126 -0
  60. cadpilot-0.4.0/tests/test_operation_help.py +66 -0
  61. cadpilot-0.4.0/tests/test_operations.py +381 -0
  62. cadpilot-0.4.0/tests/test_pattern_store.py +52 -0
  63. cadpilot-0.4.0/tests/test_prompt_text.py +21 -0
  64. cadpilot-0.4.0/tests/test_responses.py +54 -0
  65. cadpilot-0.4.0/tests/test_server_state.py +29 -0
  66. cadpilot-0.4.0/tests/test_session_state.py +106 -0
  67. cadpilot-0.4.0/tests/test_sketch_features.py +143 -0
  68. cadpilot-0.4.0/uv.lock +613 -0
@@ -0,0 +1,7 @@
1
+ # Normalize line endings: LF in the repository, platform-native on checkout
2
+ * text=auto
3
+
4
+ # Never convert binary assets
5
+ *.png binary
6
+ *.gif binary
7
+ *.FCStd binary
@@ -0,0 +1,38 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, windows-latest]
14
+ python-version: ["3.12", "3.13"]
15
+ runs-on: ${{ matrix.os }}
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v5
22
+ with:
23
+ enable-cache: true
24
+
25
+ - name: Set up Python
26
+ run: uv python install ${{ matrix.python-version }}
27
+
28
+ - name: Install dependencies
29
+ run: uv sync --python ${{ matrix.python-version }}
30
+
31
+ - name: Lint
32
+ run: uv run --no-sync ruff check .
33
+
34
+ - name: Format check
35
+ run: uv run --no-sync ruff format --check .
36
+
37
+ - name: Test
38
+ run: uv run --no-sync pytest -q
@@ -0,0 +1,32 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - name: Install uv
13
+ uses: astral-sh/setup-uv@v5
14
+ - name: Run tests
15
+ run: |
16
+ uv sync
17
+ uv run --no-sync pytest -q
18
+
19
+ publish:
20
+ needs: test
21
+ runs-on: ubuntu-latest
22
+ environment: pypi
23
+ permissions:
24
+ id-token: write # required for PyPI Trusted Publishing (OIDC)
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - name: Install uv
28
+ uses: astral-sh/setup-uv@v5
29
+ - name: Build
30
+ run: uv build
31
+ - name: Publish to PyPI
32
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,34 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ .venv/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+
10
+ # FreeCAD working files (demos / backups)
11
+ *.FCStd
12
+ *.FCBak
13
+ *.FCStd1
14
+
15
+ # Local plans / skills
16
+ docs/superpowers/
17
+
18
+ # OS / editor
19
+ .DS_Store
20
+ Thumbs.db
21
+ *.swp
22
+ *.swo
23
+ *~
24
+
25
+ # IDE
26
+ .idea/
27
+ .vscode/
28
+ .zcode/
29
+
30
+ # Local secrets
31
+ .env
32
+
33
+ # Allow the demo model in examples/
34
+ !examples/ModernBicycle.FCStd
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,190 @@
1
+ # CADPilot — Workspace Guide
2
+
3
+ ## Purpose
4
+
5
+ MCP (Model Context Protocol) server that lets AI clients (Claude Desktop, LangChain, etc.) control FreeCAD remotely. Two main components communicate over XML-RPC:
6
+
7
+ 1. **MCP server** (`src/cadpilot/`) — Python package run via `uvx cadpilot` or `uv run cadpilot`. Speaks MCP to the AI client and XML-RPC to FreeCAD.
8
+ 2. **FreeCAD addon** (`addon/CADPilot/`) — Installed into FreeCAD's `Mod/` directory. Hosts the XML-RPC server inside the FreeCAD process and dispatches all document/GUI work onto the main thread.
9
+
10
+ ## Directory Layout
11
+
12
+ ```
13
+ src/cadpilot/ # MCP server package (published to PyPI)
14
+ server.py # FastMCP tool definitions & CLI entry point (main())
15
+ freecad_client.py # XML-RPC client proxy to FreeCAD addon
16
+ operations/core.py # Tool operation implementations (one function per tool)
17
+ operations/__init__.py # Re-exports all operations
18
+ responses.py # ToolResponse type alias, text/json/screenshot helpers
19
+ server_state.py # ServerState dataclass (connection, host, screenshot flags)
20
+ session_state.py # ModelingSession/Step dataclasses, JSON persistence, current-session registry
21
+ assembly_state.py # AssemblySession/AssemblyStep dataclasses, JSON persistence (~/.cadpilot/assembly/), precomputed-undo rollback
22
+ operations/assembly.py # assembly_session tool: spec validation → RPC assembly_op → session recording
23
+ pattern_store.py # Pattern memory (reusable workflows), keyword retrieval
24
+ guidance.py # Lightweight next-step suggestions & risk heuristics (incl. primitive_without_sketch / absolute_placement / assembly-mode steer)
25
+ prompt_text.py # ASSET_CREATION_STRATEGY prompt template
26
+ tool_docs.py # Long per-operation reference docs served by the operation_help tool
27
+
28
+ addon/CADPilot/ # FreeCAD workbench addon (copied to FreeCAD's Mod/ dir)
29
+ InitGui.py # Workbench registration, toolbar/menu, auto-start
30
+ Init.py # Path setup
31
+ rpc_server/
32
+ rpc_server.py # FreeCADRPC class — XML-RPC handler, server start/stop
33
+ gui_dispatch.py # dispatch_to_gui() — queues work onto the GUI thread
34
+ commands.py # FreeCAD Command classes for toolbar buttons
35
+ object_factory.py # create_object_gui() — object creation logic
36
+ property_mapper.py # set_object_property() — recursive property assignment
37
+ serialize.py # serialize_object() — object → dict for RPC responses
38
+ view_manager.py # save_active_screenshot() — camera/view screenshot logic
39
+ geometry_query.py # Read-only Shape queries — measure/topology/interference
40
+ assembly_ops.py # Anchor-based assembly — anchors (auto/explicit), assemble (mates), verify_assembly
41
+ joint_ops.py # Persistent-joint assembly — FreeCAD 1.1 Assembly WB lifecycle (Link wrap, joints, preSolve+solve, rollback, verify+gap profile)
42
+ trim_ops.py # Declarative priority trimming — non-destructive baked cut in loser-link frame
43
+ feature_ops.py # Parametric feature creation (boolean/fillet/sketch/pad/...) + selectors
44
+ sketcher_ops.py # Constrained-sketch builder (geometry/constraints/solver diagnostics)
45
+ ip_filter.py # FilteredXMLRPCServer — IP/CIDR allowlist
46
+ settings.py # JSON settings persistence (auto-start, remote, allowed IPs)
47
+
48
+ examples/ # Usage examples (adk/agent.py, langchain/react.py)
49
+ tests/ # pytest suite for the MCP server side (fake XML-RPC connection)
50
+ assets/ # Demo GIFs and images for README
51
+ ```
52
+
53
+ ## Build & Run
54
+
55
+ ```bash
56
+ # Install dependencies (requires Python ≥3.12, uv)
57
+ uv sync
58
+
59
+ # Run MCP server locally (developer mode)
60
+ uv run cadpilot # connects to FreeCAD on localhost:9875
61
+ uv run cadpilot --with-screenshots # attach screenshots by default (multimodal models)
62
+ uv run cadpilot --only-text-feedback # never return screenshots (hard guarantee)
63
+ uv run cadpilot --host 192.168.1.100 # connect to remote FreeCAD
64
+
65
+ # Publish to PyPI (via hatchling)
66
+ uv build
67
+
68
+ # Run tests (MCP server side only; the addon needs a live FreeCAD)
69
+ uv run pytest
70
+ ```
71
+
72
+ The FreeCAD addon must be installed separately — copy `addon/CADPilot/` into FreeCAD's `Mod/` directory and restart FreeCAD.
73
+
74
+ ## Addon Hot-Reload (No Restart)
75
+
76
+ During development you can reload the addon **without restarting FreeCAD**:
77
+
78
+ 1. Copy the updated addon files to the live `Mod/` directory:
79
+ ```bash
80
+ cp -rf "H:/My_Software/FreeCAD-MCP/addon/CADPilot/." \
81
+ "C:/Users/intel/AppData/Roaming/FreeCAD/v1-1/Mod/CADPilot/"
82
+ ```
83
+
84
+ 2. In FreeCAD's Python console (or via `execute_code`) run:
85
+ ```python
86
+ import sys, importlib
87
+ import rpc_server.rpc_server as rs_old
88
+
89
+ print("stop:", rs_old.stop_rpc_server())
90
+
91
+ from PySide import QtCore # or PySide6 / PySide2
92
+
93
+
94
+ def _start(rs):
95
+ result = rs.start_rpc_server(9875)
96
+ print("start:", result)
97
+ if "still stopping" in str(result):
98
+ # Previous stop is still draining; retry instead of giving up
99
+ # (giving up here leaves a half-restarted server: socket dead,
100
+ # no heartbeat, every GUI-dispatched call hangs).
101
+ QtCore.QTimer.singleShot(4000, lambda: _start(rs))
102
+
103
+
104
+ def restart():
105
+ for sub in [
106
+ "ip_filter",
107
+ "settings",
108
+ "gui_dispatch",
109
+ "object_factory",
110
+ "property_mapper",
111
+ "serialize",
112
+ "view_manager",
113
+ "commands",
114
+ "geometry_query",
115
+ "assembly_ops",
116
+ "trim_ops",
117
+ "joint_ops",
118
+ "sketcher_ops",
119
+ "feature_ops",
120
+ ]:
121
+ name = f"rpc_server.{sub}"
122
+ if name in sys.modules:
123
+ importlib.reload(sys.modules[name])
124
+ rs = importlib.reload(rs_old)
125
+ _start(rs)
126
+
127
+
128
+ # Deferred restart: the in-flight XML-RPC request blocks shutdown drain,
129
+ # so we wait 4s for server_close() to finish before re-binding the port.
130
+ QtCore.QTimer.singleShot(4000, restart)
131
+ ```
132
+
133
+ 3. Wait ~8 seconds before issuing the next MCP call so the new server is ready.
134
+
135
+ 4. If GUI-dispatched calls (e.g. `execute_code`) hang after the reload but
136
+ `ping` still answers, the heartbeat/waker chain died during the race.
137
+ Repair via `execute_code_async` (its worker runs without GUI dispatch):
138
+ ```python
139
+ import rpc_server.gui_dispatch as gd
140
+ from PySide import QtCore
141
+ import FreeCADGui
142
+
143
+ def repair():
144
+ while not gd._rpc_request_queue.empty():
145
+ t = gd._rpc_request_queue.get()
146
+ if t is not gd._SHUTDOWN:
147
+ t()
148
+ QtCore.QTimer.singleShot(500, gd.process_gui_tasks)
149
+
150
+ QtCore.QTimer.singleShot(0, FreeCADGui.getMainWindow(), repair)
151
+ ```
152
+
153
+ > **Why deferred?** `stop_rpc_server()` calls `shutdown()` which blocks until the current request drains, and `server_close()` must release the socket before `start_rpc_server()` can bind again. Running the restart synchronously inside the same `execute_code` call deadlocks because the request itself is blocking shutdown. The `QTimer` deferral runs on FreeCAD's main GUI thread after the RPC call returns.
154
+
155
+ ## Architecture & Key Constraints
156
+
157
+ - **GUI thread rule**: All FreeCAD document/GUI operations MUST run on FreeCAD's main (GUI) thread. The addon uses `dispatch_to_gui()` to queue lambdas and a `QTimer` waker to process them. Never call FreeCAD APIs directly from the RPC server thread.
158
+ - **Two-process model**: The MCP server and FreeCAD run in separate processes. They communicate exclusively via XML-RPC on port 9875. The MCP server never imports FreeCAD.
159
+ - **MCP version compatibility**: `server.py` imports `FastMCP` from `mcp.server.fastmcp` (1.x) with a fallback to `MCPServer` from `mcp.server.mcpserver` (2.x). Keep both paths working.
160
+ - **Timeouts**: Default XML-RPC transport timeout is 150s. `execute_code` has a 90s GUI-thread timeout; use `execute_code_async` for longer operations.
161
+ - **Reconnect**: `FreeCADConnection._invoke` rebuilds the XML-RPC proxy and retries once on dead-connection errors (FreeCAD/addon restart). Socket timeouts are NOT retried — the op may still be executing server-side.
162
+ - **Screenshot handling**: Screenshots are OPTIONAL and off by default. Per-call `with_screenshot` params opt in; `--with-screenshots` makes them default-on; `--only-text-feedback` is a hard off that overrides everything (see `ServerState.resolve_screenshot`). Screenshots are base64 PNG via temp files. Mutation tools (create/edit/delete/execute_operations) capture the screenshot inline in the same RPC dispatch; the client falls back to a second `get_active_screenshot` call against old addons. When no explicit size is given, the long edge is capped at 768px (`DEFAULT_MAX_DIM` in `view_manager.py`). Some view types (TechDraw, Spreadsheet) don't support screenshots — `get_active_screenshot` returns `None` in those cases.
163
+ - **Async tasks**: `execute_code_async` returns a `task_id`; status, `task_print()` output, and tracebacks are kept in a bounded in-memory registry (`_async_tasks`, FIFO max 50) and polled via `get_task_result`. `sys.stdout` is never redirected for background tasks (process-wide race).
164
+ - **Unified cad() tool**: All mutations go through `cad(operation=...)` (nsforge math()-style dispatcher) to keep the tool list small. Steps: the operation functions in `operations/core.py` remain the implementation; `cad_operation` dispatches and records session steps. Scope is modeling-only — FEM analysis, the parts library, and `reload_document` were removed in v0.2. Feature ops (boolean/fillet/chamfer/loft/sweep/mirror/pattern, plus Sketcher/PartDesign: variables/sketch/pad/pocket/revolution/groove/thickness/draft since v0.3, plus datum_plane/hull since v0.4) share the single RPC `create_feature` and pass params as a spec dict (obj_name = base, obj_properties = params). For `sketch`/`variables`/`datum_plane`/`hull`, obj_name names the NEW object and `spec["base"]` carries it (see `CAD_NO_BASE_OPERATIONS`). Sketches are built atomically in `sketcher_ops.py` (geometry + constraints in one transaction, solver runs immediately); results carry `dof`/`fully_constrained`/`warnings` via `describe_feature`, failures roll back with solver diagnostics. Thickness/draft support both the FreeCAD ≥1.1 LinkSub `Base` layout and the ≤1.0 `Faces` property (probe `PropertiesList`).
165
+ - **Sketch mode details** (live-verified on FreeCAD 1.1.3): sketch `external: [[obj, "EdgeN"|"VertexN"], ...]` adds external geometry (GeoIds from -3 in list order, start/end points ONLY — `mid`/`center` on external ids fails at solve with MalformedConstraints, so `_check_external_point_refs` rejects it up front); out-of-body targets are auto-bridged via `PartDesign::SubShapeBinder` (`_external_binder`, idempotent `Ext_<obj>_<elem>`) because PartDesign rejects external geometry outside the sketch's body, and `addExternal` takes `(str, str)`. `datum_plane` attaches a PartDesign::Plane to an origin plane (`body.Origin.OriginFeatures` Role lookup) or an existing face (FlatFace + optional offset); sketches attach via `plane={"datum": name}`. `hull` = visual hull: intersect 2-3 view-profile sketches extruded along their sketch normals (extent = union bbox projected per-normal ±`margin`, default max(1mm, 5% of diagonal)); result is a STATIC `Part::Feature` (no proxy — survives document reload), same-name re-run replaces the Shape in place (iterate), multiple solids → largest wins, empty intersection → RuntimeError. v1 limits: view sketches at the global origin, one closed outer profile per view. **Attachment fusion**: pad/pocket on a sketch attached to a solid's face (directly or through a datum plane) operate on THAT solid via attachment — pocket cuts it, pad fuses into it; do NOT pad-then-boolean-cut (the pad already contains the base solid). **execute_code + openTransaction**: always pair with try/except → `doc.abortTransaction()` — a leaked transaction leaves broken objects that poison later recomputes (null shapes).
166
+ - **Geometry sensing**: `measure_geometry`/`get_topology`/`check_interference` are read-only Shape queries (addon `geometry_query.py`), dispatched to the GUI thread without a transaction. Values are rounded to 4 significant digits; topology lists are size-sorted and paginated (`limit`/`offset`).
167
+ - **Assembly toolchain**: `get_anchors`/`set_anchors`/`assemble`/`verify_assembly` (addon `assembly_ops.py`) give the model data-driven spatial awareness — no screenshots required. Anchors are named points+directions: auto-derived per object (`bbox_center/min/max`, `com`, `axis_mid/start/end` from the dominant cylindrical face, `face0-2_center` from the largest planar faces) plus explicit named ones stored as JSON in an `App::PropertyString` named `MCP_Anchors` in LOCAL coords (they follow Placement). `set_anchors(coord_frame="global")` converts via `obj.Placement.inverse()` at write time — use it whenever the source coordinates are global, because many objects carry non-identity Placements. `assemble` takes a mate list `{obj, anchor, target, target_anchor, mode: center|touch|axis, offset}`, applies each mate in one FreeCAD transaction, then RE-RESOLVES the anchor post-move to report per-mate residuals (mm + deg) and aborts on `tolerance` violation; partial commit via `commit_if` when at least one mate passed. `verify_assembly` audits the whole document: floating objects (nearest-neighbour distance via bbox prefilter + `distToShape`), interferences (common volume for bbox-overlapping pairs), and explicit anchor-pair checks with per-check tolerance. PRECISION RULE: `_r()`/`_vec()` rounding (4 sig digits) applies ONLY at the report boundary — `_resolve_anchor`/`_auto_anchor_map` must return RAW `FreeCAD.Vector`s for math (rounding at ~1000mm coords = 0.1mm granularity → false residuals). `verify_assembly` also builds a union-find **contact graph** from pairs already scanned (exact `distToShape` ≤ `_CONTACT_TOLERANCE` = 0.5mm, or common volume > threshold) and reports connected components: the largest is the main assembly, the rest are `islands` (with `gap_mm`/`nearest_main`).
168
+ - **Connectivity auto-audit**: after every committed `cad()` mutation, `cad_operation` re-runs the read-only `verify_assembly` audit and appends a "⚠ Connectivity" warning (formatted by `_format_connectivity_warning`) to both the tool response and the recorded step's `result_summary`; `detect_risks` surfaces it as a `disconnected_islands` risk in `session_status`. Guards: skipped when `object_count > _AUTO_AUDIT_MAX_OBJECTS` (300), when the addon is old (no `islands` key), or globally via `--no-auto-audit` (`ServerState.auto_audit`). Audit failures never block the mutation.
169
+ - **Assembly mode (`assembly_session` tool)**: independent state machine for mate-based assembly with PERSISTENT joints (FreeCAD 1.1 native Assembly workbench — `Assembly::AssemblyObject` with `Type="Assembly"`, `App::Link` components that own Placements, `JointObject.Joint` joints in the JointGroup). Ops: start(ground) / add_component / mate / solve / unmate / rollback(to_step) / verify / status / complete. Mate refs: `{"part", face|anchor|point}` — resolved to `(link, ["FaceN", "VertexM"])` where the vertex sets the mate landing point (GUI click semantics). `preSolve` (matchJCS) runs before the final single `solve(True)` — skipping it lands mates with faces perpendicular; repeated solve passes corrupt storePrev state. Frames: `link.Shape` is GLOBAL, joint references/`findPlacement` are part-LOCAL; residuals are geometric truth (`fa.distToShape(fb)` + normal angle), never JCS math. `trim={"winner":...}` bakes a non-destructive cut in the loser-link local frame and re-points the link; rollback deletes joints/cuts, re-points links, restores pre-mate placements (precomputed per-step undo, merged by `assembly_state.plan_rollback`). Gap profiles sample the a-face UV grid and measure perpendicular lift from the mate plane (overhang ≠ gap).
170
+ - **Modeling sessions**: `session_state.py` binds a session to one document. Every committed mutation runs inside a FreeCAD transaction (addon `_run_op_with_screenshot` wraps `doc.openTransaction`), so `session_rollback` = `doc.undo()` × N + log truncation; removed steps sit in a redo buffer until a new step (mirrors FreeCAD redo semantics). `execute_code` steps are non-atomic and block rollback unless forced. Sessions/patterns persist under `$CADPILOT_HOME` (default `~/.cadpilot/`). Mutation results carry an `objects` fingerprint (sorted object names) used to detect state drift after rollback.
171
+ - **Knowledge hierarchy**: prompts instruct ① model's own knowledge → ② `recall_patterns` → ③ `inspect_freecad`; successful approaches are stored via `save_pattern`/`session_complete`.
172
+ - **Name sanitization**: FreeCAD sanitizes document/object names (spaces → underscores, deduplication). RPC handlers return the *actual* name from FreeCAD, not the requested name.
173
+
174
+ ## Coding Conventions
175
+
176
+ - Python 3.12+ (uses `X | None` union syntax, `type` alias).
177
+ - Logging: use `logging.getLogger("CADPilotserver")` in both MCP server and addon code.
178
+ - Tool operations: each tool has a dedicated `_operation` function in `operations/core.py` that takes a `FreeCADConnection` as its first arg and returns a `ToolResponse` (`list[TextContent | ImageContent]`).
179
+ - Addon code uses `FreeCAD.Console.PrintMessage/PrintError/PrintWarning` for FreeCAD's Report View.
180
+ - Settings persisted as JSON via `cadpilot_settings.json` (in FreeCAD's user data dir).
181
+ - **Docstring budget (prompt economy)**: every `@mcp.tool()` docstring is injected into the AI client's context on `tools/list`. Keep docstrings to a 1-3 line summary + brief Args — long parameter/semantics references go in `src/cadpilot/tool_docs.py` (`CAD_OP_DOCS`) and are served on demand via the `operation_help` tool. `tests/test_operation_help.py::test_tool_docstring_budget` enforces a total budget (< 14,000 chars across all tools); it fails if docstrings creep back up.
182
+
183
+ ## Adding a New MCP Tool
184
+
185
+ 1. Add the operation function in `src/cadpilot/operations/core.py`.
186
+ 2. Export it from `src/cadpilot/operations/__init__.py`.
187
+ 3. Add the `@mcp.tool()` handler in `src/cadpilot/server.py` that calls the operation.
188
+ 4. Add the corresponding RPC method in `addon/CADPilot/rpc_server/rpc_server.py` (on the `FreeCADRPC` class).
189
+ 5. Add the client proxy method in `src/cadpilot/freecad_client.py`.
190
+ 6. If the tool touches the document/GUI, dispatch via `dispatch_to_gui()` in the addon.
@@ -0,0 +1,204 @@
1
+ # Changelog
2
+
3
+ ## v0.4.0 (2026-07-30)
4
+
5
+ ### Renamed to CADPilot
6
+
7
+ The project has been renamed from `freecad-mcp` to **CADPilot** (AI pilots FreeCAD). All identifiers updated:
8
+
9
+ - PyPI package: `freecad-mcp` → `cadpilot`
10
+ - Python module: `freecad_mcp` → `cadpilot`
11
+ - CLI command: `uvx freecad-mcp` → `uvx cadpilot`
12
+ - FreeCAD addon directory: `FreeCADMCP` → `CADPilot`
13
+ - Workbench name: "FreeCAD MCP" → "CADPilot"
14
+ - Environment variable: `FREECAD_MCP_HOME` → `CADPILOT_HOME`
15
+ - Data directory: `~/.freecad-mcp` → `~/.cadpilot`
16
+ - Settings file: `freecad_mcp_settings.json` → `cadpilot_settings.json`
17
+ - Logger name: `FreeCADMCPserver` → `CADPilot`
18
+
19
+ ### New features (since v0.3.0)
20
+
21
+ - `datum_plane` and `hull` feature operations in `cad()`
22
+ - Assembly session with persistent joints (FreeCAD 1.1 Assembly workbench)
23
+ - Connectivity auto-audit after every `cad()` mutation
24
+ - Declarative priority trimming (`trim={"winner": ...}`)
25
+
26
+ ### Fixed
27
+
28
+ - **`execute_code` namespace pollution**: user code now runs in a copy of the
29
+ addon module's globals (same as `execute_code_async`) — assignments can no
30
+ longer corrupt the RPC server's own namespace across calls.
31
+ - **Assembly state robustness** (`assembly_state.py`): `load()` returns `None`
32
+ on missing/corrupt/structurally-invalid session files instead of raising
33
+ (consistent with `session_state.load_session`); `save()` is now atomic
34
+ (tmp + replace) so a failed write can't truncate a saved session; the
35
+ `_current` registry is guarded by a lock.
36
+ - **`assembly_session` RPC error handling** (`operations/assembly.py`):
37
+ `start` / `add_component` / `mate` / `unmate` / `rollback` now check the
38
+ addon result for `{"success": false}` before consuming fields — a failed
39
+ RPC returns the addon's error message and records nothing (previously
40
+ crashed with `KeyError` after a half-mutated state). `mate` no longer
41
+ crashes when the result carries trim data but the call passed no `trim`.
42
+ - **Dev tooling**: the ruff config parsed invalidly (`[tool.ruff.format]
43
+ line-length`), silently disabling all lint/format runs; fixed and the whole
44
+ tree re-linted/reformatted (136 findings resolved).
45
+
46
+ ## v0.3.0
47
+
48
+ ### New features
49
+
50
+ - **Constrained sketches + PartDesign** — eight new `cad()` feature ops enabling
51
+ the full "variables → sketch → solid → dress-up" parametric chain:
52
+ - `variables` — create/update a Spreadsheet parameter table (`cells: {"A1": [alias, value]}`,
53
+ idempotent); everything downstream binds to it via `=Spreadsheet.alias`.
54
+ - `sketch` — atomic constrained sketch (`Sketcher::SketchObject` inside a
55
+ PartDesign Body, auto-created when absent). `geometry` (line/arc/circle/bspline/point;
56
+ list order = GeoId) + `constraints` (coincident, horizontal, vertical, tangent,
57
+ perpendicular, parallel, equal, symmetric, distance, distance_x, distance_y,
58
+ radius, angle) are applied in one transaction and solved immediately.
59
+ Point references are `[geo_id, "start"|"end"|"center"|"mid"]`; `[-1, *]` is the
60
+ origin. `plane`: "XY"/"XZ"/"YZ" (+`offset`) or `{"face": [obj, "FaceN"]}`.
61
+ Dimensional values accept `=expressions`. Results report `dof` /
62
+ `fully_constrained`; under-constrained sketches succeed with a warning,
63
+ conflicting/failed ones roll back with solver diagnostics
64
+ (`ConflictingConstraints`, `RedundantConstraints`).
65
+ - `pad` / `pocket` — PartDesign extrusion from a sketch (closed profile
66
+ enforced), `length`/`reversed`/`midplane`.
67
+ - `revolution` / `groove` — PartDesign revolve, `axis` ("X"/"Y"/"Z" sketch axes
68
+ or `{"edge": [obj, "EdgeN"]}`) and `angle`.
69
+ - `thickness` / `draft` — dress-up ops; FreeCAD ≥1.1 LinkSub `Base` and
70
+ ≤1.0 `Faces` property layouts both supported. `pull_direction` takes
71
+ `{"edge": [obj, "EdgeN"]}`.
72
+ - Live verification script: `scripts/live_sketch_verify.py` (runs against a live
73
+ FreeCAD; builds a parametric bracket and checks volumes, expression
74
+ propagation, failure diagnostics, and undo).
75
+
76
+ ## v0.2.0 (2026-07-30)
77
+
78
+ ### Breaking changes
79
+
80
+ - Four standalone tools are merged into a single unified **`cad()`** tool
81
+ (nsforge `math()`-style dispatcher, reduces resident tool context):
82
+ - `create_object` → `cad(operation="create_object", doc_name, obj_type, obj_name, ...)`
83
+ - `edit_object` → `cad(operation="edit_object", ...)`
84
+ - `delete_object` → `cad(operation="delete_object", ...)`
85
+ - `execute_operations` → `cad(operation="batch", ops=[...])`
86
+ - **Removed** (modeling-only scope):
87
+ - `run_fem_analysis` and all `Fem::` object creation support
88
+ (addon `fem_executor.py` gone; no CalculiX/Gmsh dependency)
89
+ - `get_parts_list` and `insert_part_from_library` (addon `parts_library.py` gone)
90
+ - `reload_document`
91
+ - **RPC response format**: `get_objects`, `get_object`, and `list_documents` now
92
+ return `{"success": true, "objects"/"object"/"documents": ...}` instead of bare
93
+ lists/dicts. The MCP client (`freecad_client.py`) handles the conversion
94
+ transparently, so MCP tool callers see the same data — but direct XML-RPC
95
+ consumers must adapt.
96
+ - Clients calling the removed/merged tools must switch to `cad()`.
97
+
98
+ ### New features
99
+
100
+ - **Modeling sessions**: `session_start` / `session_status` / `session_get_steps` /
101
+ `session_rollback` / `session_redo` / `session_add_note` / `session_pause` /
102
+ `session_resume` / `session_list` / `session_complete`.
103
+ - Every committed mutation runs inside a FreeCAD transaction; `session_rollback`
104
+ maps to native `doc.undo()` with log truncation, `session_redo` mirrors FreeCAD
105
+ redo semantics (a new step clears the redo buffer).
106
+ - `execute_code` steps are non-atomic and block rollback unless `force=True`.
107
+ - Sessions persist as JSON under `$CADPILOT_HOME/sessions/` (default
108
+ `~/.cadpilot/sessions/`).
109
+ - **Pattern memory**: `save_pattern` / `recall_patterns` — successful workflows can be
110
+ stored and retrieved by keyword search (CJK-safe substring matching).
111
+ `session_complete` can archive a session as a pattern.
112
+ - **Runtime introspection**: `inspect_freecad` — inspect an object's properties/methods
113
+ or a dotted-name API docstring without leaving the session.
114
+
115
+ ### Fixed
116
+
117
+ - **Double coordinate transform in geometry queries**: FreeCAD Shapes carry the
118
+ object's Placement as their internal location, so `BoundBox`, `CenterOfMass`,
119
+ `Vertex.Point`, `Face.Surface` (Axis/Center) and `Face.normalAt` already return
120
+ GLOBAL coordinates. `measure_geometry` / `get_topology` / `get_positioning_info`
121
+ applied `obj.Placement` a second time, returning wrong positions/normals/axes for
122
+ any moved or rotated object (e.g. a rotated fuselage reported its bbox along -Z).
123
+ All manual placement transforms removed; `placement.rotation.angle_deg` now
124
+ actually reports degrees (was radians).
125
+ - **`align_shapes` radian/degree bug**: `Face.getAngle()` returns radians but
126
+ `FreeCAD.Rotation(axis, angle)` expects degrees — touch/axis modes rotated by a
127
+ far-too-small angle. Fixed with `math.degrees()`.
128
+ - **Expression binding**: in `cad()` create/edit `obj_properties`, string values starting with `=` are bound via the ExpressionEngine (`obj.setExpression`) instead of assigned literally — Spreadsheet-driven parametric design without new tools.
129
+ - **Feature operations**: `cad()` gains `boolean`/`fillet`/`chamfer`/`loft`/`sweep`/`mirror`/`pattern` — parametric FreeCAD objects (transactional, rollback-able), with edge/face selectors ("all" / indices / names) fed by `get_topology`.
130
+ - **Geometry sensing**: `measure_geometry` (volume/area/bbox/center of mass/validity), `get_topology` (paginated faces/edges/vertices with semantic info for selection), `check_interference` (distance + common volume) — quantitative feedback after each modeling step.
131
+ - **Spatial positioning** (the hardest problem in AI-driven CAD assembly):
132
+ - `cad(operation="move")` — relative translate/rotate on top of current Placement
133
+ (solves ~80% of positioning needs without manual coordinate math).
134
+ - `get_positioning_info` — global-coordinate spatial data for a specific face/edge/vertex
135
+ (center, normal, axis, radius, start/end points — all transformed by the object's Placement).
136
+ - `align_shapes` — move an object so one of its elements aligns with a target element on
137
+ another object. Modes: `"touch"` (face-to-face contact, normals opposed), `"center"`
138
+ (center-to-center), `"axis"` (cylindrical axis alignment). Optional `offset` for gap/overlap.
139
+ - **Global coordinates everywhere**: `measure_geometry` now returns bounding box and center of
140
+ mass in global coordinates (applies Placement transform). `get_topology` face/edge/vertex
141
+ entries now include global-coordinate data: face `radius`/`axis` (cylindrical/conical/spherical),
142
+ edge `start`/`end` vertices and `radius`/`axis` (circular), vertex global position.
143
+ - **Guidance**: mutation responses include lightweight `display_text` suggestions and
144
+ risk warnings (state drift after rollback, non-atomic steps, document closed).
145
+
146
+ ### Bug fixes
147
+
148
+ - **Consistent edge schema**: closed (full-circle) edges in `get_topology` /
149
+ `get_positioning_info` now always carry an `end` point (equal to `start`) —
150
+ previously the key was absent for single-vertex edges, breaking callers that
151
+ iterate `start`/`end` uniformly.
152
+ - **`align_shapes` silent offset**: `offset` is only meaningful in `touch` mode;
153
+ passing a non-zero offset in `center`/`axis` mode now returns a `warning`
154
+ field instead of silently ignoring it.
155
+ - **Dead code**: removed an always-overwritten placement computation in
156
+ `_build_move` (`feature_ops.py`).
157
+ - **Null shape serialization**: `serialize_shape` now checks `shape.isNull()` in
158
+ addition to `shape is None`, preventing `AttributeError` on objects whose Shape
159
+ property exists but is a null OCCT handle.
160
+ - **Mirror feature type**: `_build_mirror` now tries `Part::Mirroring` first and
161
+ falls back to `Part::Mirror` only on type-not-found errors, with a clear
162
+ `ValueError` if neither exists — no more silent `TypeError` on FreeCAD builds
163
+ that only ship one of the two.
164
+ - **Thread safety**: `_now()` in `session_state.py` wrapped with a `threading.Lock`
165
+ to prevent rare timestamp collisions in concurrent session operations.
166
+ - **Object name normalization**: `_normalize_object_names()` handles both string
167
+ and dict elements from different RPC code paths, fixing `objects_after`
168
+ fingerprint mismatches in session steps.
169
+ - **Interference threshold**: `check_interference` common-volume threshold raised
170
+ from `1e-7` to `1e-4` mm³ to avoid false positives from floating-point noise.
171
+ - **Face normals**: `get_topology` now computes normals for ALL face types (not
172
+ just `Plane`), using `face.normalAt()` at the face center.
173
+
174
+ ### Improvements
175
+
176
+ - **Screenshot policy**: screenshots are opt-in per call (`with_screenshot`).
177
+ Precedence: `--only-text-feedback` (hard off) > per-call `with_screenshot` >
178
+ `--with-screenshots` (default-on). Screenshots are capped at 768px on the long edge
179
+ by default to save tokens.
180
+ - **Inline screenshots**: `execute_code` and `create_document` now capture
181
+ screenshots in the same GUI dispatch (single RPC round trip) instead of a
182
+ separate `get_active_screenshot` call, halving latency for screenshot-enabled
183
+ workflows.
184
+ - **Stability**: read-only RPCs (`get_objects`, `get_object`, `list_documents`) now
185
+ dispatch onto the FreeCAD GUI thread; the XML-RPC client retries once on
186
+ recoverable connection errors.
187
+ - **Merged RPC**: mutations and their optional screenshot are fetched in a single
188
+ XML-RPC round trip; mutation results include an `objects` fingerprint (sorted
189
+ object names) used for drift detection.
190
+ - **Batch single-recompute**: `cad(operation="batch", ...)` now skips per-object
191
+ `doc.recompute()` and performs a single recompute after all ops, significantly
192
+ faster for large batches.
193
+ - **Boolean multi-tool**: `cad(operation="boolean", tool=["Obj1","Obj2",...])`
194
+ now accepts a list of tool objects — they are fused into a temporary compound
195
+ before the boolean operation, enabling multi-body cuts/fuses in one step.
196
+ - **ViewObject serialization**: extended with `DisplayMode`, `LineColor`,
197
+ `PointSize`, `LineWidth`, and `DrawStyle` properties for richer visual feedback.
198
+ - **Compatibility**: the new MCP server falls back gracefully against older addons
199
+ (single-shot screenshot RPC, optional params); older clients keep working against
200
+ the new addon.
201
+ - Tests: pytest suite (119 tests) covering responses, operations, reconnect, session
202
+ state, pattern store, guidance, cad() dispatch, name normalization, spatial
203
+ positioning (move, get_positioning_info, align_shapes), and global-coordinate
204
+ topology queries.
cadpilot-0.4.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shirokuma (k tanaka)
4
+ Copyright (c) 2026 LBurny
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.