* FreeCAD addon support for Workbench and Plugins * docs: refactor docs and clean up linters, etc. * Remove mdformat * test: improve test coverage * test:Lots of general fixes * chore: more general fixes
223 lines
7.6 KiB
Python
223 lines
7.6 KiB
Python
"""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())
|