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.

  1. DISPLAY is :1, not :0. This note originally used :0 in every docker exec below; all of them have been corrected in place to :1. A :0 socket does exist in the container — which is why the wrong value looks plausible — but xwininfo cannot open it, and the failure reads as “no X server / no windows” rather than “wrong display number”.
  2. Dismissing the dialog is NOT always sufficient, and “do NOT restart” is only half the rule. On 2026-09-05 xdotool windowclose did remove the Document Recovery window (verified gone from the window tree) and execute_code still timed out at 90 s afterwards — no dialog present, gui_dispatch still self-reporting healthy. 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 restartmv /config/.cache/FreeCAD/v1-1/Cache/FreeCAD_Doc_*/ into a dated stash dir (MOVE, never rm — they are the user’s only copy of unsaved work), check list_documents() is [], then docker restart freecad. That brought RPC back in ~10 s and execute_code to 0.0 s. Full procedure: What actually worked — stash the recovery snapshots, THEN restart.

Rough discriminator: wedged for minutesxdotool alone is likely enough. Wedged for hours or days → expect to stash and restart. The only proof of success is execute_code returning — never a clean window tree.

The trap in one line

A healthy RPC status does not mean code can executeget_rpc_status deliberately 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

ProbeResultProves the GUI thread works?
ss -ltn inside the container0.0.0.0:9875 LISTEN❌ socket only
get_rpc_statushealthydeliberately 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

0x400075 is not stable. Read it out of xwininfo -root -tree first, 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_code times out + get_rpc_status says healthy ⇒ 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. Run xwininfo -root -tree first.

⚠️ CORRECTED 2026-09-05: if the dialog is gone and execute_code still times out, the Qt event loop is dead and xdotool cannot save it. Then the move is: verify list_documents() is [], move FreeCAD_Doc_* recovery dirs out of /config/.cache/FreeCAD/v1-1/Cache/ into a dated stash, then docker 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_status itself 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=xcb

Because 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-0

The working fix

docker restart freecad

The 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 attempt docker exec … AppRun, and do not spend time on QT_QPA_PLATFORM / WAYLAND_DISPLAY / XDG_RUNTIME_DIR permutations — line 13 of AppRun wins over all of them.

Confirmation probe from the host

curl http://127.0.0.1:9875/
ResponseMeaning
HTTP 501CORRECT — 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

ContainerIP
freecad172.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: true is REQUIRED, not optional

With remote_enabled: false the RPC server binds the container’s own 127.0.0.1. docker-proxy forwards published traffic to the container’s eth0, not its loopback — so the published port can never reach it, no matter what allowed_ips says. This produces exactly the accept-then-close signature above. Setting it true makes the server bind 0.0.0.0:9875 inside the container, which is what ss -ltn should 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-mcp proxy container is down, not FreeCAD. docker ps -a (the -a matters) → Exited (128)docker start freecad-mcp. A restart: unless-stopped policy does not guarantee it came back. See 2026-09-05-freecad-mcp-502-dead-container-and-qt-event-loop-wedge.