Files
freecad-robust-mcp-fc111/addon/FreecadRobustMCP/InitGui.py
T
8c338f6da7 feat: MCP Bridge Workbench, just command cleanup, testing, etc. (#24)
* fix: lots of fixes and name refactoring

* feat: Add workbench preferences

* fix: MCP bridge status widget and just command fixes

* fix(tests): Use the correct mesa-glx package

* fix(ci): Add fontconfig to GUI test dependencies

FreeCAD GUI was failing to start with:
"Fontconfig error: Cannot load default config file: No such file"

Added fontconfig and fonts-dejavu-core packages to the GUI test job
dependencies to resolve the font configuration issue.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(addon): Extract path utilities into shared module

Create path_utils.py module that consolidates duplicated path-finding
logic from commands.py and InitGui.py:
- get_addon_path(): Find addon directory with caching and fallbacks
- get_icon_path(): Get full path to an icon file
- get_icons_dir(): Get path to icons directory
- get_workbench_icon(): Get path to workbench main icon

This removes ~100 lines of duplicated code while preserving the same
behavior including _addon_path_cache and all fallback methods.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(addon): Prevent stale plugin state on startup failure

The StartMCPBridgeCommand.Activated method could leave _mcp_plugin in
a partially initialized state if FreecadMCPPlugin.start() failed after
the plugin was instantiated.

Changes:
- Create plugin in a local variable first
- Only assign to _mcp_plugin after start() succeeds
- Explicitly clear _mcp_plugin and _running_config in exception
  handlers to ensure clean state for subsequent retry attempts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Lot of broad improvements

* fix(ci): Use blocking headless_server.py for GUI tests

The GUI test was using startup_bridge.py which is non-blocking
(designed for interactive use). For CI, even in GUI mode, we need
the blocking headless_server.py that calls run_forever() to keep
FreeCAD running. GUI features are still available since we use
the 'freecad' executable instead of 'freecadcmd'.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(addon): Rename headless_server.py to blocking_bridge.py

The old name was misleading because:
- It works with both GUI (freecad) and headless (freecadcmd) modes
- The key characteristic is that it BLOCKS with run_forever()

New naming convention clarifies the difference:
- blocking_bridge.py: Starts bridge and blocks (for CI, servers)
- startup_bridge.py: Starts bridge and returns (for interactive GUI)

Updated all references across:
- GitHub workflow (macro-test.yaml)
- Just commands (freecad.just)
- Unit tests (test_addon_structure.py)
- Documentation (5 files)
- CLAUDE.md

Also improved the script to detect GUI mode dynamically using
FreeCAD.GuiUp and display the appropriate status message.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(just): Remove erroneous rm of startup_bridge.py on error

The startup script is now a permanent source file in the repository,
not a generated temporary file. The rm -f would have deleted source
code if FreeCAD wasn't found.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: General improvements

* fix: Lots of general fixes and only stable to PyPi

* fix: small cleanup

* fix: Small fixes and hopefully fixes the GUI tests

* fix: Add proper library paths for FreeCAD GUI in CI

- Create wrapper scripts instead of symlinks for AppImage binaries
- Set LD_LIBRARY_PATH, QT_PLUGIN_PATH for GUI mode
- Add diagnostic output to identify startup failures

* fix: Use apprun for GUI tests in CI

* fix: Improving Xvfb tests

* fix: GUI tests worlk

* chore: remove invalid --no-splash comments

* fix: ARM64 architecture support and other fixes

* fix: cleanup

* test: just commands test suite

* test: improve just command tests

* fix: more general improvements

* fix: more cleanup

* fix: more updates

* fix: small tweaks

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:26:33 -08:00

175 lines
6.2 KiB
Python

"""Robust MCP Bridge Workbench - GUI Initialization.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module defines the workbench class for the Robust MCP Bridge.
It provides toolbar buttons and menu items to start and stop the
MCP bridge server. Commands are defined in the commands module.
"""
from __future__ import annotations
import FreeCAD
import FreeCADGui
# Register icons path for preferences page icon
# This must be done at module level, before the preferences page is registered
try:
from path_utils import get_icons_dir
_icons_dir = get_icons_dir()
if _icons_dir:
FreeCADGui.addIconPath(_icons_dir)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not register icon path: {e}\n")
# Register preferences page with FreeCAD's Preferences dialog
# This must be done at module level, before the workbench is registered
try:
from preferences_page import MCPBridgePreferencesPage
FreeCADGui.addPreferencePage(MCPBridgePreferencesPage, "Robust MCP Bridge")
except Exception as e:
FreeCAD.Console.PrintWarning(
f"Could not register MCP Bridge preferences page: {e}\n"
)
class FreecadRobustMCPWorkbench(FreeCADGui.Workbench):
"""FreeCAD Robust MCP Workbench.
Provides toolbar and menu commands to start, stop, and monitor
the MCP bridge server for AI assistant integration.
"""
MenuText = "Robust MCP Bridge"
ToolTip = "Robust MCP Bridge for AI assistant integration with FreeCAD"
def __init__(self) -> None:
"""Initialize workbench with icon path."""
from path_utils import get_workbench_icon
self.Icon = get_workbench_icon()
def Initialize(self) -> None:
"""Initialize the workbench - called once when first activated."""
# Import commands module here (not at top level) to ensure
# it's available during FreeCAD's module loading process
from commands import (
MCPBridgePreferencesCommand,
MCPBridgeStatusCommand,
StartMCPBridgeCommand,
StopMCPBridgeCommand,
)
# Register commands
FreeCADGui.addCommand("Start_MCP_Bridge", StartMCPBridgeCommand())
FreeCADGui.addCommand("Stop_MCP_Bridge", StopMCPBridgeCommand())
FreeCADGui.addCommand("MCP_Bridge_Status", MCPBridgeStatusCommand())
FreeCADGui.addCommand("MCP_Bridge_Preferences", MCPBridgePreferencesCommand())
# Create toolbar with main commands
toolbar_commands = [
"Start_MCP_Bridge",
"Stop_MCP_Bridge",
"MCP_Bridge_Status",
]
self.appendToolbar("Robust MCP Bridge", toolbar_commands)
# Create menu with all commands including preferences
menu_commands = [
"Start_MCP_Bridge",
"Stop_MCP_Bridge",
"MCP_Bridge_Status",
"Separator",
"MCP_Bridge_Preferences",
]
self.appendMenu("Robust MCP Bridge", menu_commands)
FreeCAD.Console.PrintMessage("Robust MCP Bridge workbench initialized\n")
# Auto-start bridge if preference is enabled
# This is a fallback if the module-level timer didn't fire
# (which can happen if the module isn't loaded until workbench selection)
try:
from preferences import get_auto_start
if get_auto_start():
# Check if already running (timer might have started it)
from commands import is_bridge_running
if not is_bridge_running():
FreeCAD.Console.PrintMessage(
"Auto-starting MCP Bridge (configured in preferences)...\n"
)
FreeCADGui.runCommand("Start_MCP_Bridge")
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not auto-start MCP Bridge: {e}\n")
# Sync status bar widget with current bridge state
# (bridge may have been started by Init.py before workbench was selected)
try:
from status_widget import sync_status_with_bridge
sync_status_with_bridge()
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not sync status bar: {e}\n")
def Activated(self) -> None:
"""Called when the workbench is activated."""
# Sync status bar widget with current bridge state
try:
from status_widget import sync_status_with_bridge
sync_status_with_bridge()
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not sync status bar: {e}\n")
def Deactivated(self) -> None:
"""Called when the workbench is deactivated."""
pass
def GetClassName(self) -> str:
"""Return the C++ class name for this workbench."""
return "Gui::PythonWorkbench"
# Register the workbench
FreeCADGui.addWorkbench(FreecadRobustMCPWorkbench())
# Schedule status bar sync after a short delay to allow GUI to finish initializing
# This runs on the main thread (InitGui.py is executed on main thread)
try:
try:
from PySide2 import QtCore
except ImportError:
from PySide6 import QtCore
def _deferred_status_bar_sync() -> None:
"""Sync status bar with bridge state after GUI is ready."""
try:
from commands import is_bridge_running
from preferences import get_status_bar_enabled
from status_widget import sync_status_with_bridge
if get_status_bar_enabled() and is_bridge_running():
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: Syncing status bar from InitGui...\n"
)
sync_status_with_bridge()
except Exception as e:
FreeCAD.Console.PrintWarning(
f"Robust MCP Bridge: Deferred status bar sync failed: {e}\n"
)
# Use QTimer.singleShot on the main thread - this should work
QtCore.QTimer.singleShot(2000, _deferred_status_bar_sync)
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: Status bar sync scheduled from InitGui (2s)\n"
)
except Exception as e:
FreeCAD.Console.PrintWarning(
f"Robust MCP Bridge: Could not schedule status bar sync: {e}\n"
)