Fix: rename workbench and prepare for release (#27)

* chore: rename workbench to FreecadRobustMCPBridge

* fix: stdio cleanup and bug fix for JSON RPC

* fix: update FreeCAD MCP bridge and tests

* test: Fix GUI Integration tests and other issues

* fix: unit tests in CI and a few other things

* docs: generate GitHub pages site and link to it.

* fix: General code improvements and DRY refactoring

* chore: General cleanup

* chore: small fixes

* docs: cleanup

* docs: fixes

* docs: small corrections

* docs: fix

* test: release check

* bump: workbench v0.6.0

* chore: bump macros to 0.6.0

* docs: Release improvements

* refactor: improve DRYness of code
This commit is contained in:
Sean P. Kane
2026-01-12 09:30:05 -08:00
committed by GitHub
parent 8c338f6da7
commit f14ab8177d
92 changed files with 2740 additions and 727 deletions
+142
View File
@@ -0,0 +1,142 @@
"""Robust MCP Bridge Workbench - Initialization.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module is executed when FreeCAD starts up. It handles initialization
tasks for the Robust MCP Bridge workbench, including auto-start of the
MCP bridge if configured. Works in both GUI and headless modes.
Note: Status bar updates are handled by InitGui.py since Qt operations
must run on the main thread.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from freecad_mcp_bridge.bridge_utils import GuiWaiter
import FreeCAD
FreeCAD.Console.PrintMessage("Robust MCP Bridge: Init loaded\n")
# Global reference to GuiWaiter and auto-start timer to prevent garbage collection
# Type annotations use Any for timer since it could be QTimer from PySide2 or PySide6
_auto_start_timer: Any | None = None
_gui_waiter: GuiWaiter | None = None
def _auto_start_bridge() -> None:
"""Auto-start the MCP bridge if configured in preferences.
This function is called via a deferred timer (GUI mode) or directly
(headless mode) after FreeCAD finishes loading. It starts the bridge
without requiring the workbench to be selected.
"""
try:
from preferences import get_auto_start
if not get_auto_start():
return
# Check if bridge is already running
from commands import _mcp_plugin
if _mcp_plugin is not None and _mcp_plugin.is_running:
return
FreeCAD.Console.PrintMessage(
"Auto-starting MCP Bridge (configured in preferences)...\n"
)
# Import and start the bridge directly
from freecad_mcp_bridge.server import FreecadMCPPlugin
from preferences import get_socket_port, get_xmlrpc_port
xmlrpc_port = get_xmlrpc_port()
socket_port = get_socket_port()
plugin = FreecadMCPPlugin(
host="localhost",
port=socket_port,
xmlrpc_port=xmlrpc_port,
enable_xmlrpc=True,
)
plugin.start()
# Register plugin with commands module for restart detection
from freecad_mcp_bridge.bridge_utils import register_mcp_plugin
register_mcp_plugin(plugin, xmlrpc_port, socket_port)
FreeCAD.Console.PrintMessage("\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage("MCP Bridge started!\n")
FreeCAD.Console.PrintMessage(f" - XML-RPC: localhost:{xmlrpc_port}\n")
FreeCAD.Console.PrintMessage(f" - Socket: localhost:{socket_port}\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage(
"\nYou can now connect your MCP client (Claude Code, etc.) to FreeCAD.\n"
)
except Exception as e:
FreeCAD.Console.PrintError(f"Failed to auto-start MCP Bridge: {e}\n")
# Schedule auto-start after FreeCAD finishes loading
# Strategy:
# - If FreeCAD.GuiUp is True: Qt event loop is running, use timer for deferred start
# - If FreeCAD.GuiUp is False but Qt is available: FreeCAD GUI is initializing.
# Use GuiWaiter to wait for GuiUp to become True before starting.
# This ensures the bridge uses Qt timer (not background thread) for queue processing.
# - If Qt is not available: Pure headless mode, start bridge directly
#
# CRITICAL: We must wait for FreeCAD.GuiUp to be True before starting the bridge
# in GUI mode. If we start when GuiUp is False, the bridge's _start_queue_processor()
# will see GuiUp=False and use a background thread. Later, code executed on that
# thread will try to do Qt operations, causing crashes (SIGABRT in QCocoaWindow).
try:
from preferences import get_auto_start
if get_auto_start():
# Try to import Qt
import contextlib
QtCore = None
try:
from PySide2 import QtCore # type: ignore[assignment, no-redef]
except ImportError:
with contextlib.suppress(ImportError):
from PySide6 import QtCore # type: ignore[assignment, no-redef]
if FreeCAD.GuiUp:
# GUI is already up - use timer for deferred start
if QtCore is not None:
_auto_start_timer = QtCore.QTimer()
_auto_start_timer.setSingleShot(True)
_auto_start_timer.timeout.connect(_auto_start_bridge)
_auto_start_timer.start(1000)
else:
# GUI is up but Qt import failed - start directly
_auto_start_bridge()
elif QtCore is not None:
# GUI not ready yet, but Qt is available (FreeCAD starting in GUI mode)
# Use GuiWaiter to wait for GuiUp to become True before starting
from freecad_mcp_bridge.bridge_utils import GuiWaiter
_gui_waiter = GuiWaiter(
callback=_auto_start_bridge,
log_prefix="Robust MCP Bridge",
timeout_error_extra=(
"\nTo start the bridge manually, select the Robust MCP Bridge "
"workbench\nand click 'Start MCP Bridge'.\n\n"
),
)
_gui_waiter.start()
else:
# True headless mode - no Qt, no GUI
_auto_start_bridge()
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not set up auto-start: {e}\n")