From 77a82b47849928a2e9712ab3e840f97935133431 Mon Sep 17 00:00:00 2001 From: Robert Helewka Date: Sun, 31 May 2026 10:13:43 -0400 Subject: [PATCH] docs: update FreeCAD MCP README to document dual-service architecture --- ansible/freecad_mcp/README.md | 204 ++++++++++++++++++++++++---------- docs/ouranos.md | 141 +++++++++++------------ 2 files changed, 208 insertions(+), 137 deletions(-) diff --git a/ansible/freecad_mcp/README.md b/ansible/freecad_mcp/README.md index 76a8e59..5a5063e 100644 --- a/ansible/freecad_mcp/README.md +++ b/ansible/freecad_mcp/README.md @@ -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 `. 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 ` 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_.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_.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. + diff --git a/docs/ouranos.md b/docs/ouranos.md index 0eb2ad4..f8181bb 100644 --- a/docs/ouranos.md +++ b/docs/ouranos.md @@ -13,7 +13,50 @@ Infrastructure-as-Code project managing the **Ouranos Lab** — a development sa > **DNS Domain**: Incus resolves containers via the `.incus` domain suffix (e.g., `oberon.incus`, `portia.incus`). IPv4 addresses are dynamically assigned — always use DNS names, never hardcode IPs. ---- +## Project Numbers +- External Apps + - Well known: Postgresl, ssh, web, prometheus + - 220: External Apps (legacy) + - 290: External App 1 + - 299: External App 9 +- Django Projects: + - 221: Zelus + - 222: Angelia + - 224: Athena + - 225: Kairos + - 226: Icarlos + - 227: MCP Switchboard (227), Spelunker (228), Peitho (229), Mnemosyne (230) +- FastAgent Projects: + - 240: Pallas Iolaus + - 241: Pallas Kottos + - 242: Pallas Mentor +- FastAPI Projects: + - 200: Daedalus + - 201: Arke + - 202: Kernos + - 203: Rommie + - 204: Orpheus + - 205: Periplus + - 206: Nike + - 207: Stentor + - 208: Argos + - 209: Hecate + - 210: Rhema + - 211: Synesis + +## Port Numbering + +Well-known ports running as a service may be used: Postgresql 5432, Prometheus Metrics 9100. + +However inside a docker project, the number plan needs to be followed to avoid port conflicts and confusion: +XXXYZ +XXX Project Number or 290-299 for external project (host specific) +Y Service: 0 reserved, 1-4 flexible, 5 database, 6 MCP, 7 API, 8 Web App, 9 Prometheus metrics +Z Instance: The running instance of this app on the same host, starting at 1. May also be used to handle exceptions. + +255 Incus port forwarding: Ports in ths range are forwarded from the Incus host to Incus containers (defined in Terraform), but HAProxy through Titania + +514ZZ is the syslog port. Docker containers send their syslog to an Alloy syslog collector port. ZZ is the application instance, they just need to be different on the same host and increment from 01. ## Uranian Host Architecture @@ -40,12 +83,6 @@ This is the host that runs Python projects in the Ouranos sandbox. It has an RDP server and is generally where application development happens. Each project has a number that is used to determine port numbers. -- Docker engine -- JupyterLab (port 22071 via OAuth2-Proxy) -- Gitea Runner (CI/CD agent) -- Django Projects: Zelus (221), Angelia (222), Athena (224), Kairos (225), Icarlos (226), MCP Switchboard (227), Spelunker (228), Peitho (229), Mnemosyne (230) -- FastAgent Projects: Pallas (240) -- FastAPI Projects: Daedalus (200), Arke (201) Kernos (202), Rommie (203), Orpheus (204), Periplus (205), Nike (206), Stentor (207), Argos (208), ### caliban — Agent Automation @@ -53,20 +90,19 @@ Autonomous computer agent learning through environmental interaction. - Docker engine - Agent S MCP Server (MATE desktop, AT-SPI automation) -- Kernos MCP Shell Server (port 22062) -- Rommie MCP Server (port 20361) — agent-to-agent GUI automation via Agent S -- FreeCAD Robust MCP Server (port 22061) — CAD automation via FreeCAD XML-RPC +- Kernos MCP Shell Server +- Rommie MCP Server — agent-to-agent GUI automation via Agent S +- FreeCAD Robust MCP Server — CAD automation via FreeCAD XML-RPC - GPU passthrough -- RDP access (port 25521) +- RDP access ### oberon — Container Orchestration & Dockerized Shared Services King of the Fairies orchestrating containers and managing MCP infrastructure. - Docker engine -- MCP Switchboard (port 22781) — Django app routing MCP tool calls - RabbitMQ message queue -- smtp4dev SMTP test server (port 22025) +- smtp4dev SMTP test server ### portia — Relational Database @@ -78,10 +114,7 @@ Intelligent and resourceful — the reliability of relational databases. ### ariel — Graph Database Air spirit — ethereal, interconnected nature mirroring graph relationships. - -- Neo4j 5.26.0 (Docker) -- HTTP API: port 25554 -- Bolt: port 7687 (reached as `ariel.incus:7687` on the internal network) +- Neo4j (Docker) ### umbriel — Graph Database (Mnemosyne) @@ -91,20 +124,18 @@ instance so Mnemosyne's `Library`/`Collection`/`Item`/`Chunk`/`Concept` labels, vector indexes, and schema migrations can't collide with another tenant's graph on Ariel. -- Neo4j 5.26.0 (Docker) -- HTTP Browser: port 25555 -- Bolt: port 7687 (reached as `umbriel.incus:7687` on the internal network) +- Neo4j (Docker) ### miranda — MCP Docker Host Curious bridge between worlds — hosting MCP server containers. -- Docker engine (API exposed on port 2375 for MCP Switchboard) -- MCPO OpenAI-compatible MCP proxy 22071 -- Argos MCP Server — web search via SearXNG (port 20861) -- Grafana MCP Server (port 22063) -- Neo4j MCP Server (port 22064) -- Gitea MCP Server (port 22062) +- Docker engine +- MCPO OpenAI-compatible MCP +- Argos MCP Server — web search via SearXNG +- Grafana MCP Server +- Neo4j MCP Server +- Gitea MCP Server ### prospero — Observability Stack @@ -121,11 +152,9 @@ Master magician observing all events. Witty and resourceful moon for PHP, Go, and Node.js runtimes. -- SearXNG privacy search (port 22083, behind OAuth2-Proxy) -- Gitea self-hosted Git (port 22082, SSH on 22022) -- LobeChat AI chat interface (port 22081) -- Nextcloud file sharing and collaboration (port 22083) -- AnythingLLM document AI workspace (port 22084) +- SearXNG privacy search +- Gitea self-hosted Git +- Nextcloud file sharing and collaboration - Jellyfin media server (port 22086, NVIDIA transcoding, Casdoor SSO) - Nextcloud data on dedicated Incus storage volume - Open WebUI LLM interface (port 22088, PostgreSQL backend on Portia @@ -135,7 +164,7 @@ Witty and resourceful moon for PHP, Go, and Node.js runtimes. Original magical power wielding language magic. -- Arke LLM API Proxy (port 25540) +- Arke LLM API Proxy - Multi-provider support (OpenAI, Anthropic, etc.) - Session management with Memcached - Database backend on Portia @@ -144,7 +173,7 @@ Original magical power wielding language magic. Queen of the Fairies managing access control and authentication. -- HAProxy 3.x with TLS termination (port 443) +- HAProxy 3.x with TLS termination - Let's Encrypt wildcard certificate via certbot DNS-01 (Namecheap) - HTTP to HTTPS redirect (port 80) - Gitea SSH proxy (port 22022) @@ -153,21 +182,6 @@ Queen of the Fairies managing access control and authentication. --- -## Port Numbering - -Well-known ports running as a service may be used: Postgresql 5432, Prometheus Metrics 9100. - -However inside a docker project, the number plan needs to be followed to avoid port conflicts and confusion: -XXXYZ -XXX Project Number or 220 for external project -Y Service: 0 reserved, 1-4 flexible, 5 database, 6 MCP, 7 API, 8 Web App, 9 Prometheus metrics -Z Instance: The running instance of this app on the same host, starting at 1. May also be used to handle exceptions. - -255 Incus port forwarding: Ports in ths range are forwarded from the Incus host to Incus containers (defined in Terraform) - -514ZZ is the syslog port. Docker containers send their syslog to an Alloy syslog collector port. ZZ is the application instance, they just need to be different on the same host and increment from 01. - ---- ## Application Conventions @@ -256,36 +270,9 @@ Titania provides TLS termination and reverse proxy for all services. - **HTTP**: port 80 (redirects to HTTPS) - **Certificate**: Let's Encrypt wildcard via certbot DNS-01 -### Route Table +### Subdomains -| Subdomain | Backend | Service | -|-----------|---------|---------| -| `ouranos.helu.ca` (root) | puck.incus:22281 | Angelia (Django) | -| `alertmanager.ouranos.helu.ca` | prospero.incus:443 (SSL) | AlertManager | -| `angelia.ouranos.helu.ca` | puck.incus:22281 | Angelia (Django) | -| `anythingllm.ouranos.helu.ca` | rosalind.incus:22084 | AnythingLLM | -| `arke.ouranos.helu.ca` | sycorax.incus:25540 | Arke LLM Proxy | -| `athena.ouranos.helu.ca` | puck.incus:22481 | Athena (Django) | -| `gitea.ouranos.helu.ca` | rosalind.incus:22082 | Gitea | -| `grafana.ouranos.helu.ca` | prospero.incus:443 (SSL) | Grafana | -| `hass.ouranos.helu.ca` | oberon.incus:8123 | Home Assistant | -| `id.ouranos.helu.ca` | titania.incus:22081 | Casdoor SSO | -| `jellyfin.ouranos.helu.ca` | rosalind.incus:22086 | Jellyfin | -| `icarlos.ouranos.helu.ca` | puck.incus:22681 | Icarlos (Django) | -| `jupyterlab.ouranos.helu.ca` | puck.incus:22071 | JupyterLab (OAuth2-Proxy) | -| `kairos.ouranos.helu.ca` | puck.incus:22581 | Kairos (Django) | -| `lobechat.ouranos.helu.ca` | rosalind.incus:22081 | LobeChat | -| `loki.ouranos.helu.ca` | prospero.incus:443 (SSL) | Loki | -| `mcp-switchboard.ouranos.helu.ca` | oberon.incus:22781 | MCP Switchboard | -| `nextcloud.ouranos.helu.ca` | rosalind.incus:22083 | Nextcloud | -| `openwebui.ouranos.helu.ca` | oberon.incus:22088 | Open WebUI | -| `peitho.ouranos.helu.ca` | puck.incus:22981 | Peitho (Django) | -| `periplus.ouranos.helu.ca` | puck.incus:20681 | Periplus (FastAPI + MCP via nginx) | -| `pgadmin.ouranos.helu.ca` | prospero.incus:443 (SSL) | PgAdmin 4 | -| `prometheus.ouranos.helu.ca` | prospero.incus:443 (SSL) | Prometheus | -| `searxng.ouranos.helu.ca` | oberon.incus:22073 | SearXNG (OAuth2-Proxy) | -| `smtp4dev.ouranos.helu.ca` | oberon.incus:22085 | smtp4dev | -| `spelunker.ouranos.helu.ca` | puck.incus:22881 | Spelunker (Django) | +Refer to the Ansible Titania host inventory (`inventory/host_vars/titania.incus.yml`) for current backend routing configuration. ---