execute_code times out on every call while get_rpc_status reports healthy. The RPC server is genuinely up — a modal Document Recovery dialog owns FreeCAD’s main thread and starves the addon’s GUI-dispatch queue. Dismiss the dialog with xdotool; do not restart the container. Follow-on chapter to 2026-09-02-freecad-mcp-rpc-refused-gui-not-running — same debugging session, next failure mode.
⚠️ CORRECTED 2026-09-05 — READ THIS BEFORE FOLLOWING THE FIX BELOW
Two things in this note are wrong or incomplete. Both were found on 2026-09-05 while the same dialog was still standing, three days on: 2026-09-05-freecad-mcp-502-dead-container-and-qt-event-loop-wedge.
DISPLAYis:1, not:0. This note originally used:0in everydocker execbelow; all of them have been corrected in place to:1. A:0socket does exist in the container — which is why the wrong value looks plausible — butxwininfocannot open it, and the failure reads as “no X server / no windows” rather than “wrong display number”.- Dismissing the dialog is NOT always sufficient, and “do NOT restart” is only half the rule. On 2026-09-05
xdotool windowclosedid remove the Document Recovery window (verified gone from the window tree) andexecute_codestill timed out at 90 s afterwards — no dialog present,gui_dispatchstill self-reportinghealthy. After ~3 days behind that modal the Qt event loop stayed dead; the dialog had caused the wedge but was no longer what was holding it, so destroying the window freed nothing.The rule that actually works: “do not restart” is correct only because a bare restart re-raises the dialog while recovery files are present and re-wedges you. Move the recovery snapshots aside FIRST, then restart —
mv /config/.cache/FreeCAD/v1-1/Cache/FreeCAD_Doc_*/into a dated stash dir (MOVE, neverrm— they are the user’s only copy of unsaved work), checklist_documents()is[], thendocker restart freecad. That brought RPC back in ~10 s andexecute_codeto 0.0 s. Full procedure: What actually worked — stash the recovery snapshots, THEN restart.Rough discriminator: wedged for minutes →
xdotoolalone is likely enough. Wedged for hours or days → expect to stash and restart. The only proof of success isexecute_codereturning — never a clean window tree.
The trap in one line
A
healthyRPC status does not mean code can execute —get_rpc_statusdeliberately avoids the GUI thread, which is precisely the thread that is blocked.
Host: telep-mainframe, container freecad (the cad node stack from 2026-08-26-freecad-cad-workstation). All commands below verified in-session on 2026-09-02.
Gotcha 1 — a modal dialog silently blocks every MCP tool call
Symptom
After docker restart freecad, every health signal was green:
docker exec freecad ss -ltn # → 0.0.0.0:9875 LISTEN ✅// mcp__freecad__get_rpc_status
{"rpc_server": "running", "gui_dispatch": {"state": "healthy"}}…yet every execute_code call failed. Through the MCP layer it surfaced only as a bare:
The operation timed out
Calling the XML-RPC method directly (bypassing the MCP proxy) revealed the real error:
{'success': False, 'error': 'GUI dispatch timed out after 90s'}Always re-run a timing-out MCP call as raw XML-RPC
The MCP proxy collapses the backend’s structured error into a generic timeout string. The direct XML-RPC call is what named the actual subsystem (
GUI dispatch) and gave the 90 s bound. One extra probe turned “something is broken” into “the GUI thread is blocked.”
Why the health check lied
| Probe | Result | Proves the GUI thread works? |
|---|---|---|
ss -ltn inside the container | 0.0.0.0:9875 LISTEN | ❌ socket only |
get_rpc_status | healthy | ❌ deliberately does not use the GUI thread |
ping() | True | ❌ |
list_documents() | [] | ❌ |
execute_code(...) | timeout | ✅ the only real test |
get_rpc_status is designed to answer without touching the GUI thread so it can report while the GUI is busy. That makes it useless as evidence that work can be dispatched. ping() and list_documents() are no better. execute_code is the only probe that exercises the dispatch queue.
Root cause
FreeCAD had crashed earlier in the session. On relaunch it displayed its Document Recovery modal dialog. A modal dialog owns Qt’s main thread and never returns to the event loop, so the freecad-mcp addon’s GUI-dispatch queue is starved — every queued call sits until the 90 s bound expires.
Note the causal chain: the crash is invisible; only its recovery prompt is observable, and only in the X window tree. Nothing in the logs, the port state, or the RPC status mentions it.
Diagnosis — list the X windows
xdotool and xwininfo are preinstalled in the container:
docker exec -u abc -e DISPLAY=:1 freecad sh -c "xwininfo -root -tree"Output on the failing run listed the dialog alongside the main window:
0x400062 "FreeCAD 1.1.3"
0x400075 "Document Recovery" ← the blocker
Fix — dismiss it non-destructively
Escape defers recovery and leaves the backup files in place — nothing is discarded, so this is safe to run blind.
docker exec -u abc -e DISPLAY=:1 freecad sh -c \
"xdotool windowactivate 0x400075; xdotool key --window 0x400075 Escape"The window id changes on every run
0x400075is not stable. Read it out ofxwininfo -root -treefirst, every time.
In practice windowactivate alone dismissed the dialog, and the follow-up key call then errored with BadWindow. That error is harmless — it is confirmation that the dialog was already gone. Do not treat it as a failure and retry.
execute_code worked immediately after dismissal.
For Agents — decision rule
execute_codetimes out +get_rpc_statussayshealthy⇒ look for a modal dialog. Do NOT restart the container bare. A bare restart destroys the evidence, re-triggers the recovery prompt on the next launch, and puts you straight back in the same state. Runxwininfo -root -treefirst.⚠️ CORRECTED 2026-09-05: if the dialog is gone and
execute_codestill times out, the Qt event loop is dead andxdotoolcannot save it. Then the move is: verifylist_documents()is[], moveFreeCAD_Doc_*recovery dirs out of/config/.cache/FreeCAD/v1-1/Cache/into a dated stash, thendocker restart freecad. See 2026-09-05-freecad-mcp-502-dead-container-and-qt-event-loop-wedge.Compare with the other failure mode in 2026-09-02-freecad-mcp-rpc-refused-gui-not-running: there,
get_rpc_statusitself failed (Errno 111) and a restart was correct. The status call is the discriminator between the two.
Gotcha 2 — you cannot launch FreeCAD in that container with docker exec
Once the GUI is down, the instinct is to relaunch the binary by hand. It cannot be done from docker exec.
Why
/opt/freecad/AppRun line 13 hardcodes the Qt platform:
export QT_QPA_PLATFORM=xcbBecause it is an export inside the launcher, passing -e QT_QPA_PLATFORM=wayland to docker exec is silently overridden. Every hand-launch aborted with:
This application failed to start because no Qt platform plugin could be initialized
(xcb requires libxcb-cursor0)
docker exec does not inherit the desktop session environment, so the forced xcb path has no display to attach to.
Attempts that also failed, for the same reason:
# both of these still hit the hardcoded xcb export — no effect
-e XDG_RUNTIME_DIR=/config/.XDG -e WAYLAND_DISPLAY=wayland-1
-e XDG_RUNTIME_DIR=/config/.XDG -e WAYLAND_DISPLAY=wayland-0The working fix
docker restart freecadThe container’s own s6 desktop startup supplies the correct session environment: /defaults/autostart = /opt/freecad/AppRun, launched via startwm_wayland.sh. RPC came up 4 seconds after the restart.
For Agents
There is exactly one supported way to start FreeCAD in this container: let s6 do it via
docker restart. Do not attemptdocker exec … AppRun, and do not spend time onQT_QPA_PLATFORM/WAYLAND_DISPLAY/XDG_RUNTIME_DIRpermutations — line 13 ofAppRunwins over all of them.
Confirmation probe from the host
curl http://127.0.0.1:9875/| Response | Meaning |
|---|---|
| HTTP 501 | ✅ CORRECT — healthy. XML-RPC accepts POST only, so a GET is supposed to be rejected as not-implemented. |
| Connection accepted, then closed with no response | ❌ docker-proxy has the port bound, but nothing is listening inside the container |
501 looks like an error and is the success case. This is the container-local counterpart of the accept-then-close fingerprint documented in 2026-09-02-freecad-mcp-rpc-refused-gui-not-running.
Environment reference (verified 2026-09-02)
Addon settings — two files, both live
/config/.FreeCAD/freecad_mcp_settings.json
/config/.local/share/FreeCAD/freecad_mcp_settings.json
Both were already correct:
{
"remote_enabled": true,
"auto_start_rpc": true,
"allowed_ips": "127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
}Docker network
freecad_default = 172.22.0.0/16
| Container | IP |
|---|---|
freecad | 172.22.0.3 |
freecad-mcp (proxy) | 172.22.0.5 |
The 172.16.0.0/12 entry in allowed_ips covers 172.22.0.0/16 — so the proxy is permitted. No change needed there.
remote_enabled: trueis REQUIRED, not optionalWith
remote_enabled: falsethe RPC server binds the container’s own127.0.0.1.docker-proxyforwards published traffic to the container’seth0, not its loopback — so the published port can never reach it, no matter whatallowed_ipssays. This produces exactly the accept-then-close signature above. Setting ittruemakes the server bind0.0.0.0:9875inside the container, which is whatss -ltnshould show.
Output convention
The container has /exports bind-mounted from host /home/levander/freecad/exports. Save FCStd / STL / STEP / 3MF there — it is the natural drop point and the only one reachable from the host (and onward via the browsable listing / Online3DViewer described in 2026-08-26-freecad-cad-workstation).
Triage flowchart
FreeCAD MCP tool fails
│
├─ get_rpc_status fails (Errno 111 / connection refused)
│ → RPC/GUI is DOWN → docker restart freecad
│ [[2026-09-02-freecad-mcp-rpc-refused-gui-not-running]]
│
└─ get_rpc_status says "healthy" but execute_code times out
→ GUI thread BLOCKED — do NOT restart BARE
1. docker exec -u abc -e DISPLAY=:1 freecad sh -c "xwininfo -root -tree"
2. find the modal dialog's window id
3. xdotool windowactivate <id> (BadWindow after = already dismissed, fine)
4. retry execute_code
5. STILL timing out with no dialog on screen? ← added 2026-09-05
→ the event loop is dead; xdotool cannot fix it.
a. list_documents() must be [] (safety check)
b. MOVE /config/.cache/FreeCAD/v1-1/Cache/FreeCAD_Doc_*/
→ /config/.cache/FreeCAD/recovery-stash-<date>/ (never rm)
c. docker restart freecad
There is a third failure mode above this one
Before any of the above: if the tailnet URL itself returns HTTP 502, the
freecad-mcpproxy container is down, not FreeCAD.docker ps -a(the-amatters) →Exited (128)→docker start freecad-mcp. Arestart: unless-stoppedpolicy does not guarantee it came back. See 2026-09-05-freecad-mcp-502-dead-container-and-qt-event-loop-wedge.
Related
- 2026-09-05-freecad-mcp-502-dead-container-and-qt-event-loop-wedge — ⚠️ the correction to this note (
DISPLAY=:1; dismissing the dialog is not always enough — stash the recovery files, then restart), plus thefreecad-mcp502 / dead-container failure mode that sits above this one - 2026-09-02-freecad-mcp-rpc-refused-gui-not-running — the previous chapter of this same session: RPC refused entirely,
ncfalse positive, and why a restart is right there and wrong here - 2026-08-26-freecad-cad-workstation — the FreeCAD + noVNC + MCP architecture, port map,
/exportspipeline - 2026-08-26-cad-designer-agent — the on-prem agent that drives this MCP
- telep-mainframe — the host running the
freecad/freecad-mcpcontainers - service-unreachable · runbooks-index
- LOG · TOPICS