"""FreeCAD Robust MCP Workbench - GUI Initialization. SPDX-License-Identifier: MIT Copyright (c) 2025 Sean P. Kane (GitHub: spkane) This module defines the workbench class and GUI commands for the MCP Bridge. It provides toolbar buttons and menu items to start and stop the MCP bridge server. """ from __future__ import annotations from pathlib import Path from typing import Any import FreeCAD import FreeCADGui # Global reference to the plugin instance _mcp_plugin: Any = None def get_addon_path() -> str: """Get the path to this addon's directory.""" return str(Path(__file__).resolve().parent) def get_icon_path(icon_name: str) -> str: """Get the full path to an icon file. Args: icon_name: Name of the icon file (e.g., "FreecadRobustMCP.svg") Returns: Full path to the icon file. """ return str(Path(get_addon_path()) / icon_name) class StartMCPBridgeCommand: """Command to start the MCP bridge server.""" def GetResources(self) -> dict[str, str]: """Return the command resources (icon, menu text, tooltip).""" return { "Pixmap": get_icon_path("FreecadRobustMCP.svg"), "MenuText": "Start MCP Bridge", "ToolTip": ( "Start the MCP bridge server for AI assistant integration.\n" "Listens on XML-RPC (port 9875) and Socket (port 9876)." ), } def IsActive(self) -> bool: """Return True if the command can be executed.""" global _mcp_plugin # noqa: PLW0602 # Can only start if not already running return _mcp_plugin is None or not _mcp_plugin.is_running def Activated(self) -> None: """Execute the command to start the MCP bridge.""" global _mcp_plugin if _mcp_plugin is not None and _mcp_plugin.is_running: FreeCAD.Console.PrintWarning("MCP Bridge is already running.\n") return try: # Import the server module from the bundled code from freecad_mcp_bridge.server import FreecadMCPPlugin # Create and start the plugin _mcp_plugin = FreecadMCPPlugin( host="localhost", port=9876, # JSON-RPC socket port xmlrpc_port=9875, # XML-RPC port enable_xmlrpc=True, ) _mcp_plugin.start() FreeCAD.Console.PrintMessage("\n") FreeCAD.Console.PrintMessage("=" * 50 + "\n") FreeCAD.Console.PrintMessage("MCP Bridge started!\n") FreeCAD.Console.PrintMessage(" - XML-RPC: localhost:9875\n") FreeCAD.Console.PrintMessage(" - Socket: localhost:9876\n") FreeCAD.Console.PrintMessage("=" * 50 + "\n") FreeCAD.Console.PrintMessage( "\nYou can now connect your MCP client (Claude Code, etc.) to FreeCAD.\n" ) except ImportError as e: FreeCAD.Console.PrintError(f"Failed to import MCP Bridge module: {e}\n") FreeCAD.Console.PrintError( "Ensure the FreecadRobustMCP addon is properly installed.\n" ) except Exception as e: FreeCAD.Console.PrintError(f"Failed to start MCP Bridge: {e}\n") class StopMCPBridgeCommand: """Command to stop the MCP bridge server.""" def GetResources(self) -> dict[str, str]: """Return the command resources (icon, menu text, tooltip).""" return { "Pixmap": get_icon_path("FreecadRobustMCP.svg"), "MenuText": "Stop MCP Bridge", "ToolTip": "Stop the running MCP bridge server.", } def IsActive(self) -> bool: """Return True if the command can be executed.""" global _mcp_plugin # noqa: PLW0602 # Can only stop if currently running return _mcp_plugin is not None and _mcp_plugin.is_running def Activated(self) -> None: """Execute the command to stop the MCP bridge.""" global _mcp_plugin if _mcp_plugin is None or not _mcp_plugin.is_running: FreeCAD.Console.PrintWarning("MCP Bridge is not running.\n") return try: _mcp_plugin.stop() _mcp_plugin = None FreeCAD.Console.PrintMessage("\n") FreeCAD.Console.PrintMessage("=" * 50 + "\n") FreeCAD.Console.PrintMessage("MCP Bridge stopped.\n") FreeCAD.Console.PrintMessage("=" * 50 + "\n") except Exception as e: FreeCAD.Console.PrintError(f"Failed to stop MCP Bridge: {e}\n") class MCPBridgeStatusCommand: """Command to show MCP bridge status.""" def GetResources(self) -> dict[str, str]: """Return the command resources (icon, menu text, tooltip).""" return { "Pixmap": get_icon_path("FreecadRobustMCP.svg"), "MenuText": "MCP Bridge Status", "ToolTip": "Show the current status of the MCP bridge server.", } def IsActive(self) -> bool: """Return True if the command can be executed.""" # Always active - can always show status return True def Activated(self) -> None: """Execute the command to show MCP bridge status.""" global _mcp_plugin # noqa: PLW0602 FreeCAD.Console.PrintMessage("\n") FreeCAD.Console.PrintMessage("=" * 50 + "\n") FreeCAD.Console.PrintMessage("MCP Bridge Status\n") FreeCAD.Console.PrintMessage("=" * 50 + "\n") if _mcp_plugin is None: FreeCAD.Console.PrintMessage("Status: Not initialized\n") elif not _mcp_plugin.is_running: FreeCAD.Console.PrintMessage("Status: Stopped\n") else: FreeCAD.Console.PrintMessage("Status: Running\n") FreeCAD.Console.PrintMessage(f" Instance ID: {_mcp_plugin.instance_id}\n") FreeCAD.Console.PrintMessage(f" XML-RPC Port: {_mcp_plugin.xmlrpc_port}\n") FreeCAD.Console.PrintMessage(f" Socket Port: {_mcp_plugin.socket_port}\n") FreeCAD.Console.PrintMessage( f" Requests processed: {_mcp_plugin.request_count}\n" ) FreeCAD.Console.PrintMessage("=" * 50 + "\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 = "MCP Bridge" ToolTip = "MCP Bridge for AI assistant integration with FreeCAD" Icon = get_icon_path("FreecadRobustMCP.svg") def Initialize(self) -> None: """Initialize the workbench - called once when first activated.""" # Register commands FreeCADGui.addCommand("Start_MCP_Bridge", StartMCPBridgeCommand()) FreeCADGui.addCommand("Stop_MCP_Bridge", StopMCPBridgeCommand()) FreeCADGui.addCommand("MCP_Bridge_Status", MCPBridgeStatusCommand()) # Create toolbar and menu commands = ["Start_MCP_Bridge", "Stop_MCP_Bridge", "MCP_Bridge_Status"] self.appendToolbar("MCP Bridge", commands) self.appendMenu("MCP Bridge", commands) FreeCAD.Console.PrintMessage("FreeCAD Robust MCP workbench initialized\n") def Activated(self) -> None: """Called when the workbench is activated.""" pass def Deactivated(self) -> None: """Called when the workbench is deactivated.""" pass def ContextMenu(self, recipient: Any) -> None: """Called when right-clicking in the view or object tree.""" pass def GetClassName(self) -> str: """Return the C++ class name for this workbench.""" return "Gui::PythonWorkbench" # Register the workbench FreeCADGui.addWorkbench(FreecadRobustMCPWorkbench())