232 lines
9.0 KiB
Python
232 lines
9.0 KiB
Python
"""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
|
|
|
|
# Import FreeCAD first so we can log early
|
|
import FreeCAD
|
|
|
|
FreeCAD.Console.PrintMessage("Robust MCP Bridge: Init.py loaded\n")
|
|
|
|
from typing import TYPE_CHECKING, Any # noqa: E402
|
|
|
|
if TYPE_CHECKING:
|
|
from freecad_mcp_bridge.bridge_utils import GuiWaiter
|
|
|
|
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.
|
|
|
|
Args:
|
|
None.
|
|
|
|
Returns:
|
|
None. Early returns if auto-start is disabled or bridge is already running.
|
|
|
|
Raises:
|
|
Exception: Any exception during bridge startup is caught, logged to
|
|
FreeCAD.Console.PrintError with full traceback, and suppressed.
|
|
|
|
Side Effects:
|
|
- Imports and checks auto-start preference from preferences module
|
|
- Creates and starts a FreecadMCPPlugin instance if not already running
|
|
- Registers the plugin with the workbench commands module
|
|
- Prints status messages to FreeCAD.Console
|
|
|
|
Example:
|
|
This function is typically called via QTimer or GuiWaiter callback::
|
|
|
|
QtCore.QTimer.singleShot(1000, _auto_start_bridge)
|
|
"""
|
|
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")
|
|
import traceback
|
|
|
|
FreeCAD.Console.PrintError(f"Traceback: {traceback.format_exc()}\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 QApplication exists: 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 no QApplication: True headless mode, start bridge directly
|
|
#
|
|
# IMPORTANT: We check for QApplication.instance() rather than just QtCore availability
|
|
# because FreeCAD bundles PySide even in headless mode (freecadcmd), but there's no
|
|
# Qt event loop running. Without a QApplication, Qt timers will never fire.
|
|
#
|
|
# 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
|
|
|
|
_auto_start_enabled = get_auto_start()
|
|
FreeCAD.Console.PrintMessage(
|
|
f"Robust MCP Bridge: Auto-start preference = {_auto_start_enabled}\n"
|
|
)
|
|
|
|
if _auto_start_enabled:
|
|
# Try to import Qt and check for running QApplication
|
|
import contextlib
|
|
|
|
QtCore = None
|
|
QtWidgets = None
|
|
_has_qapp = False
|
|
_is_true_headless = False
|
|
|
|
try:
|
|
from PySide2 import QtCore, QtWidgets # type: ignore[assignment, no-redef]
|
|
except ImportError:
|
|
with contextlib.suppress(ImportError):
|
|
from PySide6 import ( # type: ignore[assignment, no-redef]
|
|
QtCore,
|
|
QtWidgets,
|
|
)
|
|
|
|
# Detect GUI mode vs true headless mode
|
|
# - True headless (freecadcmd): QCoreApplication exists but NOT QApplication
|
|
# - GUI mode early startup: No app yet, or QApplication being initialized
|
|
# - GUI mode ready: FreeCAD.GuiUp is True
|
|
if QtWidgets is not None and QtCore is not None:
|
|
qapp = QtWidgets.QApplication.instance()
|
|
if qapp is not None:
|
|
_has_qapp = True
|
|
else:
|
|
# No QApplication - check if QCoreApplication exists
|
|
# If QCoreApplication exists but is NOT a QApplication, it's true headless
|
|
qcore_app = QtCore.QCoreApplication.instance()
|
|
if qcore_app is not None and not isinstance(
|
|
qcore_app, QtWidgets.QApplication
|
|
):
|
|
_is_true_headless = True
|
|
# If no app at all, assume early GUI startup (will use GuiWaiter)
|
|
|
|
FreeCAD.Console.PrintMessage(
|
|
f"Robust MCP Bridge: GuiUp={FreeCAD.GuiUp}, "
|
|
f"QtCore={'available' if QtCore else 'unavailable'}, "
|
|
f"QApp={'running' if _has_qapp else 'none'}, "
|
|
f"headless={_is_true_headless}\n"
|
|
)
|
|
|
|
if FreeCAD.GuiUp:
|
|
# GUI is already up - use timer for deferred start
|
|
FreeCAD.Console.PrintMessage(
|
|
"Robust MCP Bridge: GUI already up, scheduling deferred start...\n"
|
|
)
|
|
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 _is_true_headless:
|
|
# True headless mode - QCoreApplication exists but not QApplication
|
|
# No Qt event loop for GUI, so start bridge directly with background thread
|
|
FreeCAD.Console.PrintMessage(
|
|
"Robust MCP Bridge: True headless mode (QCoreApplication only), "
|
|
"starting directly...\n"
|
|
)
|
|
_auto_start_bridge()
|
|
elif QtCore is not None:
|
|
# GUI not ready yet (either QApplication exists or no app yet)
|
|
# Use GuiWaiter to wait for GuiUp to become True before starting
|
|
# This ensures the bridge uses Qt timer (not background thread) for queue
|
|
FreeCAD.Console.PrintMessage(
|
|
"Robust MCP Bridge: GUI not ready, using GuiWaiter...\n"
|
|
)
|
|
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:
|
|
# No Qt available at all - unusual state, start directly
|
|
FreeCAD.Console.PrintMessage(
|
|
"Robust MCP Bridge: No Qt available, starting directly...\n"
|
|
)
|
|
_auto_start_bridge()
|
|
except Exception as e:
|
|
FreeCAD.Console.PrintWarning(f"Could not set up auto-start: {e}\n")
|
|
import traceback
|
|
|
|
FreeCAD.Console.PrintWarning(f"Traceback: {traceback.format_exc()}\n")
|