docs: update FreeCAD MCP README to document dual-service architecture
This commit is contained in:
@@ -1,62 +1,104 @@
|
||||
# FreeCAD Robust MCP Server — Ansible Deployment
|
||||
|
||||
Deploys the [FreeCAD Robust MCP Server](https://pypi.org/project/freecad-robust-mcp/)
|
||||
to Caliban as a systemd service with HTTP transport.
|
||||
to Caliban as **two** systemd services:
|
||||
|
||||
- **`freecad-mcp.service`** — the MCP server (HTTP/streamable-http transport on
|
||||
`:22061`), pip-installed into a venv under `/srv/freecad-mcp`, run as the
|
||||
hardened `harper` service user.
|
||||
- **`freecad-mcp-bridge.service`** — FreeCAD itself running in **GUI** mode on
|
||||
the XRDP desktop (display `:10`), exposing the XML-RPC bridge on
|
||||
`localhost:9875`. Run as `robert` (the `principal_user`, who owns the X
|
||||
session), from source staged as a tarball.
|
||||
|
||||
The MCP server connects to the bridge over `localhost:9875`; the bridge in turn
|
||||
drives FreeCAD. The two halves rendezvous only on that local port.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ caliban.incus │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ freecad-mcp.service │ │
|
||||
│ │ (streamable-http) │◄─── :22061 ──────────┤◄── MCP Client
|
||||
│ │ venv + PyPI package │ │
|
||||
│ └──────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ xmlrpc :9875 │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ FreeCAD (future) │ │
|
||||
│ │ XML-RPC server │ │
|
||||
│ └──────────────────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ caliban.incus │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ freecad-mcp.service │ │
|
||||
│ │ (streamable-http) │◄─── :22061 ────────────────────┤◄── MCP Client
|
||||
│ │ venv + PyPI package │ (user: harper, hardened) │
|
||||
│ └──────────┬───────────┘ │
|
||||
│ │ xmlrpc localhost:9875 │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────┐ │
|
||||
│ │ freecad-mcp-bridge.service │ │
|
||||
│ │ /usr/bin/freecad (GUI) │ DISPLAY=:10 (XRDP) │
|
||||
│ │ startup_bridge.py │ user: robert │
|
||||
│ │ XML-RPC :9875 / socket :9876│ │
|
||||
│ └──────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## FreeCAD bridge required for tool calls
|
||||
## Two services, two users (by design)
|
||||
|
||||
The service starts and answers the MCP `initialize` handshake **without** FreeCAD
|
||||
running — the XML-RPC connection to FreeCAD is only attempted on the first CAD
|
||||
tool call (lazy connect). So a green Ansible healthcheck means "transport up",
|
||||
**not** "FreeCAD reachable".
|
||||
| Service | User | Transport / port | Hardened | Needs X |
|
||||
| ---------------------------- | -------- | ----------------------- | -------- | ------- |
|
||||
| `freecad-mcp.service` | `harper` | HTTP `:22061` | yes | no |
|
||||
| `freecad-mcp-bridge.service` | `robert` | XML-RPC `:9875` (+ 9876) | no | yes (`:10`) |
|
||||
|
||||
Actual CAD tool calls require FreeCAD running with the Robust MCP Bridge
|
||||
workbench started, listening on XML-RPC `localhost:9875`. Standing up that bridge
|
||||
(GUI or headless) on Caliban is a separate step from getting this service to
|
||||
boot.
|
||||
The bridge runs as `robert` because it attaches to the standard XRDP display
|
||||
`:10`, owned by `robert` with Xauthority `/home/robert/.Xauthority`. It cannot
|
||||
be hardened like the server unit — it needs the user's X session and home.
|
||||
|
||||
## How the bridge starts (no `just`/`mise`/`uv` needed)
|
||||
|
||||
The bridge runs **inside FreeCAD's own Python interpreter** via
|
||||
`/usr/bin/freecad <startup_bridge.py>`. The README "Option B"
|
||||
(`just freecad::run-gui`) in the upstream repo is only a launcher wrapper that
|
||||
locates FreeCAD and runs that same script — `just`, `mise`, and `uv` are not
|
||||
required for the bridge.
|
||||
|
||||
The bridge scripts are **not** shipped in the pip wheel (it packages only
|
||||
`src/freecad_mcp`). They live in the git repo under
|
||||
`freecad/RobustMCPBridge/freecad_mcp_bridge/`, so the bridge is delivered
|
||||
separately as a staged tarball (see Deployment below).
|
||||
|
||||
> **GUI vs headless:** We run GUI mode to keep the GUI-only tools (screenshots,
|
||||
> object color, visibility, camera). `freecadcmd <blocking_bridge.py>` would run
|
||||
> headless without those tools — not used here.
|
||||
|
||||
> **Python version:** FreeCAD 1.0.0 on Caliban uses the system Python (3.13),
|
||||
> not a bundled 3.11. The upstream ABI-match warning applies only to *embedded*
|
||||
> mode (importing `FreeCAD` into an external interpreter). We run scripts inside
|
||||
> FreeCAD and the bridge is pure stdlib, so the version mismatch is a non-issue.
|
||||
|
||||
## Lazy connect: a green server healthcheck is not "FreeCAD reachable"
|
||||
|
||||
`freecad-mcp.service` starts and answers the MCP `initialize` handshake **without**
|
||||
the bridge running — the XML-RPC connection to FreeCAD is only attempted on the
|
||||
first CAD tool call. So the server playbook's `initialize` check proves
|
||||
"transport up", **not** "FreeCAD reachable". The bridge playbook's validation
|
||||
(below) is what proves the full chain.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Caliban host in Ansible inventory (already exists in Ouranos)
|
||||
- Python 3.11+ on Caliban (already present)
|
||||
- Caliban host in the `freecad_mcp` inventory group (already configured).
|
||||
- `python3` + `python3-venv` on Caliban (installed by the playbook).
|
||||
- `freecad` package on Caliban (installed by the playbook).
|
||||
- The XRDP display `:10` running, owned by `robert` (the standard Ouranos RDP
|
||||
desktop — not configured here, it is always present).
|
||||
|
||||
## Deployment
|
||||
|
||||
### 1. Copy playbook files to Ouranos
|
||||
|
||||
Copy the contents of this directory into your Ouranos repo:
|
||||
## Files in this role
|
||||
|
||||
```
|
||||
ansible/freecad_mcp/
|
||||
├── deploy.yml
|
||||
├── .env.j2
|
||||
└── freecad-mcp.service.j2
|
||||
├── deploy.yml # Two plays: MCP server + GUI bridge
|
||||
├── stage.yml # Clones the fork + builds the bridge tarball
|
||||
├── .env.j2 # MCP server env (FREECAD_* vars)
|
||||
├── freecad-mcp.service.j2 # MCP server unit (harper, hardened)
|
||||
└── freecad-mcp-bridge.service.j2 # FreeCAD GUI bridge unit (robert, :10)
|
||||
```
|
||||
|
||||
### 2. Add inventory group
|
||||
## Inventory
|
||||
|
||||
Add to `ansible/inventory/hosts`:
|
||||
`ansible/inventory/hosts` (already present):
|
||||
|
||||
```yaml
|
||||
freecad_mcp:
|
||||
@@ -64,9 +106,7 @@ freecad_mcp:
|
||||
caliban.incus:
|
||||
```
|
||||
|
||||
### 3. Add host variables
|
||||
|
||||
Add to `ansible/inventory/host_vars/caliban.incus.yml`:
|
||||
Host vars in `ansible/inventory/host_vars/caliban.incus.yml`:
|
||||
|
||||
```yaml
|
||||
# FreeCAD Robust MCP Server
|
||||
@@ -74,60 +114,93 @@ freecad_mcp_user: harper
|
||||
freecad_mcp_group: harper
|
||||
freecad_mcp_directory: /srv/freecad-mcp
|
||||
freecad_mcp_port: 22061
|
||||
freecad_mcp_version: "0.5.0"
|
||||
freecad_mcp_xmlrpc_port: 9875
|
||||
freecad_mcp_socket_port: 9876
|
||||
|
||||
# FreeCAD MCP Bridge (GUI, runs as principal_user on the XRDP display)
|
||||
freecad_mcp_bridge_directory: "/home/{{ principal_user }}/freecad-mcp-bridge"
|
||||
freecad_mcp_bridge_display: ":10"
|
||||
```
|
||||
|
||||
Update `services` list:
|
||||
Group vars in `ansible/inventory/group_vars/all/vars.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
- alloy
|
||||
- caliban
|
||||
- docker
|
||||
- freecad_mcp
|
||||
- kernos
|
||||
freecad_mcp_version: 0.6.1 # PyPI version pin (server install)
|
||||
freecad_mcp_git_ref: "main" # fork ref for BOTH the pip install and the staged bridge tarball
|
||||
```
|
||||
|
||||
### 4. Run the playbook
|
||||
## Deployment
|
||||
|
||||
The bridge source is delivered via the staging pattern: cloned on the Ansible
|
||||
controller, packed with `git archive`, and unpacked on the host (no deploy keys
|
||||
on Caliban). Stage first, then deploy:
|
||||
|
||||
```bash
|
||||
cd ~/git/ouranos/ansible
|
||||
source ~/env/ouranos/bin/activate
|
||||
|
||||
# 1. Build the bridge tarball on the controller (~/rel/freecad_mcp_bridge_<ref>.tar)
|
||||
ansible-playbook freecad_mcp/stage.yml
|
||||
|
||||
# 2. Deploy the MCP server (idempotent) + the GUI bridge
|
||||
ansible-playbook freecad_mcp/deploy.yml
|
||||
```
|
||||
|
||||
`stage.yml` clones/pulls the fork into `~/gh/freecad-addon-robust-mcp-server` at
|
||||
`freecad_mcp_git_ref` and `git archive`s it to
|
||||
`~/rel/freecad_mcp_bridge_<ref>.tar`. `deploy.yml` unpacks that into
|
||||
`~robert/freecad-mcp-bridge` and points the bridge unit at
|
||||
`freecad/RobustMCPBridge/freecad_mcp_bridge/startup_bridge.py`.
|
||||
|
||||
## Upgrading
|
||||
|
||||
To upgrade to a new PyPI version, update `freecad_mcp_version` in host_vars
|
||||
and re-run the playbook. The pip install task will detect the version change
|
||||
and the handler will restart the service.
|
||||
- **MCP server:** bump `freecad_mcp_version` (PyPI) and/or `freecad_mcp_git_ref`
|
||||
in group vars, re-run `deploy.yml`. The pip task detects the change and the
|
||||
handler restarts `freecad-mcp`.
|
||||
- **Bridge:** re-run `stage.yml` (rebuilds the tarball from the latest fork
|
||||
ref), then `deploy.yml`. The `unarchive` change notifies the
|
||||
`restart freecad-mcp-bridge` handler.
|
||||
|
||||
## Validation
|
||||
|
||||
The playbook automatically validates the deployment by:
|
||||
The playbooks validate automatically:
|
||||
|
||||
1. Waiting for the HTTP port to become available
|
||||
2. Sending an MCP `initialize` JSON-RPC request to `/mcp`
|
||||
3. Verifying a 200 response
|
||||
- **Server play:** waits for `:22061`, sends an MCP `initialize` request to
|
||||
`/mcp`, expects HTTP 200 (transport-level only — see lazy-connect note above).
|
||||
- **Bridge play:** waits for `:9875`, then calls the bridge's XML-RPC `execute`
|
||||
with `_result_ = bool(FreeCAD.GuiUp)` and asserts the result is `True` —
|
||||
proving FreeCAD is up **in GUI mode**, end to end.
|
||||
|
||||
You can also manually test:
|
||||
Manual checks:
|
||||
|
||||
```bash
|
||||
# Transport up (no FreeCAD needed):
|
||||
curl -X POST http://caliban.incus:22061/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'
|
||||
|
||||
# Bridge listening + in GUI mode:
|
||||
ss -ltnp | grep 9875
|
||||
python3 -c 'import xmlrpc.client as x; print(x.ServerProxy("http://localhost:9875", allow_none=True).execute("_result_ = bool(FreeCAD.GuiUp)"))'
|
||||
```
|
||||
|
||||
## Service Management
|
||||
|
||||
```bash
|
||||
# On Caliban
|
||||
# MCP server
|
||||
sudo systemctl status freecad-mcp
|
||||
sudo systemctl restart freecad-mcp
|
||||
sudo journalctl -u freecad-mcp -f
|
||||
|
||||
# FreeCAD GUI bridge
|
||||
sudo systemctl status freecad-mcp-bridge
|
||||
sudo systemctl restart freecad-mcp-bridge
|
||||
sudo journalctl -u freecad-mcp-bridge -f
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
The systemd service runs with hardened settings:
|
||||
The **MCP server** unit (`freecad-mcp.service`, user `harper`) is hardened:
|
||||
|
||||
| Setting | Value | Rationale |
|
||||
|---------|-------|-----------|
|
||||
@@ -137,4 +210,15 @@ The systemd service runs with hardened settings:
|
||||
| `PrivateTmp` | `true` | Isolated /tmp namespace |
|
||||
| `ReadWritePaths` | `/srv/freecad-mcp` | Only app directory is writable |
|
||||
|
||||
The **bridge** unit (`freecad-mcp-bridge.service`, user `robert`) is **not**
|
||||
hardened: FreeCAD GUI needs the user's X session, `.Xauthority`, and FreeCAD
|
||||
config in the home directory. It binds XML-RPC/socket on `localhost` only.
|
||||
|
||||
## Known limitation
|
||||
|
||||
The bridge depends on the XRDP `:10` session (owned by `robert`). `Restart=on-failure`
|
||||
recovers crashes, but **not** loss of the X display — if that session restarts,
|
||||
restart `freecad-mcp-bridge` afterward. Auto-tying the two is a possible
|
||||
follow-up.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user