refactor(ansible): rename freecad_mcp env vars and rework deployment

- Drop `FREECAD_MCP_` prefix from env vars (use `FREECAD_*`)
- Update freecad_mcp port from 22032 to 22061
- Document that FreeCAD bridge is required for tool calls
- Replace kottos deployment with pallas deployment
This commit is contained in:
2026-05-30 09:37:56 -04:00
parent bc431a3a2a
commit acf3419450
21 changed files with 876 additions and 258 deletions

View File

@@ -4,18 +4,17 @@
# =============================================================================
# MCP Transport Configuration
# =============================================================================
FREECAD_MCP_TRANSPORT=http
FREECAD_MCP_HTTP_PORT={{ freecad_mcp_port }}
FREECAD_TRANSPORT=http
FREECAD_HTTP_PORT={{ freecad_mcp_port }}
# =============================================================================
# FreeCAD Connection Mode
# =============================================================================
FREECAD_MCP_MODE={{ freecad_mcp_mode | default('xmlrpc') }}
FREECAD_MCP_XMLRPC_HOST={{ freecad_mcp_xmlrpc_host | default('localhost') }}
FREECAD_MCP_XMLRPC_PORT={{ freecad_mcp_xmlrpc_port | default('9875') }}
FREECAD_MCP_TIMEOUT_MS={{ freecad_mcp_timeout_ms | default('30000') }}
FREECAD_MODE={{ freecad_mcp_mode | default('xmlrpc') }}
FREECAD_XMLRPC_PORT={{ freecad_mcp_xmlrpc_port | default('9875') }}
FREECAD_TIMEOUT_MS={{ freecad_mcp_timeout_ms | default('30000') }}
# =============================================================================
# Logging
# =============================================================================
FREECAD_MCP_LOG_LEVEL={{ freecad_mcp_log_level | default('INFO') }}
FREECAD_LOG_LEVEL={{ freecad_mcp_log_level | default('INFO') }}

View File

@@ -1,8 +1,7 @@
# 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, ready for MCP Switchboard
consumption.
to Caliban as a systemd service with HTTP transport.
## Architecture
@@ -12,8 +11,8 @@ consumption.
│ │
│ ┌──────────────────────┐ │
│ │ freecad-mcp.service │ │
│ │ (streamable-http) │◄─── :22032 ──────────┤◄── MCP Switchboard
│ │ venv + PyPI package │ │ (oberon.incus)
│ │ (streamable-http) │◄─── :22061 ──────────┤◄── MCP Client
│ │ venv + PyPI package │ │
│ └──────────────────────┘ │
│ │ │
│ │ xmlrpc :9875 │
@@ -25,6 +24,18 @@ consumption.
└─────────────────────────────────────────────────┘
```
## FreeCAD bridge required for tool calls
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".
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.
## Prerequisites
- Caliban host in Ansible inventory (already exists in Ouranos)
@@ -62,7 +73,7 @@ Add to `ansible/inventory/host_vars/caliban.incus.yml`:
freecad_mcp_user: harper
freecad_mcp_group: harper
freecad_mcp_directory: /srv/freecad-mcp
freecad_mcp_port: 22032
freecad_mcp_port: 22061
freecad_mcp_version: "0.5.0"
```
@@ -100,7 +111,7 @@ The playbook automatically validates the deployment by:
You can also manually test:
```bash
curl -X POST http://caliban.incus:22032/mcp \
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"}}}'
```
@@ -126,5 +137,4 @@ The systemd service runs with hardened settings:
| `PrivateTmp` | `true` | Isolated /tmp namespace |
| `ReadWritePaths` | `/srv/freecad-mcp` | Only app directory is writable |
This is significantly more hardened than the Kernos service (which needs
broad filesystem access for shell commands).

View File

@@ -216,3 +216,102 @@
ansible.builtin.systemd:
name: freecad-mcp
state: restarted
# =============================================================================
# FreeCAD MCP Bridge (GUI) — runs FreeCAD on the XRDP desktop as principal_user,
# exposing the XML-RPC bridge on localhost:9875 that the MCP server connects to.
# =============================================================================
- name: Deploy FreeCAD MCP Bridge (GUI)
hosts: freecad_mcp
tasks:
- name: Ensure FreeCAD is installed
become: true
ansible.builtin.apt:
name: [freecad, tar]
state: present
update_cache: true
- name: Create FreeCAD MCP bridge directory
become: true
become_user: "{{ principal_user }}"
ansible.builtin.file:
path: "{{ freecad_mcp_bridge_directory }}"
state: directory
mode: '0755'
- name: Transfer and extract FreeCAD MCP bridge release
become: true
become_user: "{{ principal_user }}"
ansible.builtin.unarchive:
src: "~/rel/freecad_mcp_bridge_{{ freecad_mcp_git_ref }}.tar"
dest: "{{ freecad_mcp_bridge_directory }}"
notify: restart freecad-mcp-bridge
- name: Template FreeCAD MCP bridge systemd service
become: true
ansible.builtin.template:
src: freecad-mcp-bridge.service.j2
dest: /etc/systemd/system/freecad-mcp-bridge.service
owner: root
group: root
mode: '644'
notify:
- reload systemd
- restart freecad-mcp-bridge
- name: Enable and start freecad-mcp-bridge service
become: true
ansible.builtin.systemd:
name: freecad-mcp-bridge
enabled: true
state: started
daemon_reload: true
- name: Flush handlers to restart bridge before validation
ansible.builtin.meta: flush_handlers
- name: Wait for FreeCAD XML-RPC bridge to listen
ansible.builtin.wait_for:
port: "{{ freecad_mcp_xmlrpc_port | default(9875) }}"
host: localhost
delay: 5
timeout: 60
- name: Verify bridge is in GUI mode (FreeCAD.GuiUp via XML-RPC execute)
ansible.builtin.command:
argv:
- python3
- -c
- |
import sys, xmlrpc.client
proxy = xmlrpc.client.ServerProxy(
"http://localhost:{{ freecad_mcp_xmlrpc_port | default(9875) }}", allow_none=True)
resp = proxy.execute("_result_ = bool(FreeCAD.GuiUp)")
if not (resp.get("success") and resp.get("result") is True):
sys.exit("Bridge reachable but not in GUI mode: %r" % resp)
print("FreeCAD bridge GUI mode confirmed")
register: bridge_gui_check
retries: 5
delay: 5
until: bridge_gui_check.rc == 0
changed_when: false
- name: Display bridge info
ansible.builtin.debug:
msg: >-
FreeCAD MCP Bridge running in GUI mode on {{ inventory_hostname }},
XML-RPC localhost:{{ freecad_mcp_xmlrpc_port | default(9875) }}
handlers:
- name: reload systemd
become: true
ansible.builtin.systemd:
daemon_reload: true
- name: restart freecad-mcp-bridge
become: true
ansible.builtin.systemd:
name: freecad-mcp-bridge
state: restarted

View File

@@ -0,0 +1,21 @@
[Unit]
Description=FreeCAD MCP XML-RPC Bridge (GUI)
After=network.target
[Service]
Type=simple
User={{ principal_user }}
WorkingDirectory={{ freecad_mcp_bridge_directory }}
Environment=DISPLAY={{ freecad_mcp_bridge_display }}
Environment=XAUTHORITY=/home/{{ principal_user }}/.Xauthority
Environment=FREECAD_XMLRPC_PORT={{ freecad_mcp_xmlrpc_port | default('9875') }}
Environment=FREECAD_SOCKET_PORT={{ freecad_mcp_socket_port | default('9876') }}
ExecStart=/usr/bin/freecad {{ freecad_mcp_bridge_directory }}/freecad/RobustMCPBridge/freecad_mcp_bridge/startup_bridge.py
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=freecad-mcp-bridge
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,46 @@
---
- name: Stage FreeCAD MCP bridge release tarball
hosts: localhost
gather_facts: false
vars:
freecad_mcp_archive: "{{rel_dir}}/freecad_mcp_bridge_{{freecad_mcp_git_ref}}.tar"
freecad_mcp_repo_url: "git@github.com:heluca/freecad-addon-robust-mcp-server.git"
freecad_mcp_repo_dir: "{{github_dir}}/freecad-addon-robust-mcp-server"
tasks:
- name: Ensure release directory exists
file:
path: "{{rel_dir}}"
state: directory
mode: '755'
- name: Ensure github directory exists
file:
path: "{{github_dir}}"
state: directory
mode: '755'
- name: Clone freecad-addon-robust-mcp-server repository if not present
ansible.builtin.git:
repo: "{{freecad_mcp_repo_url}}"
dest: "{{freecad_mcp_repo_dir}}"
version: "{{freecad_mcp_git_ref}}"
accept_hostkey: true
register: freecad_mcp_clone
- name: Fetch all remote branches and tags
ansible.builtin.command: git fetch --all
args:
chdir: "{{freecad_mcp_repo_dir}}"
when: freecad_mcp_clone is not changed
- name: Pull latest changes
ansible.builtin.command: git pull
args:
chdir: "{{freecad_mcp_repo_dir}}"
when: freecad_mcp_clone is not changed
- name: Create FreeCAD MCP bridge archive for specified release
ansible.builtin.command: git archive -o "{{freecad_mcp_archive}}" "{{freecad_mcp_git_ref}}"
args:
chdir: "{{freecad_mcp_repo_dir}}"