* 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>
9.2 KiB
Architecture
This document provides a technical overview of the FreeCAD MCP Server architecture.
For the full architecture document with design decisions and rationale, see Detailed Architecture.
Overview
The FreeCAD MCP Server follows a Bridge with Adapter pattern:
┌─────────────────────────────────────────────────────────────────────────┐
│ MCP Server Layer │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ FastMCP Application │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Tools │ │ Resources│ │ Prompts │ │ Lifecycle│ │ │
│ │ │ (82+) │ │ │ │ │ │ Manager │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ FreeCAD Bridge Interface │ │
│ │ (Abstract Base Class) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ ╱ │ ╲ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ EmbeddedBridge │ │ SocketBridge │ │ XMLRPCBridge │ │
│ │ (Linux only) │ │ (JSON-RPC) │ │ (Recommended) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Module Structure
src/freecad_mcp/
├── __init__.py # Package entry point
├── _version.py # Version info (auto-generated)
├── server.py # Main MCP server entry point
├── config.py # Configuration management
├── py.typed # PEP 561 marker for type hints
│
├── bridge/ # FreeCAD communication layer
│ ├── __init__.py # Bridge factory
│ ├── base.py # Abstract bridge interface
│ ├── embedded.py # In-process FreeCAD (Linux only)
│ ├── socket.py # JSON-RPC socket bridge
│ └── xmlrpc.py # XML-RPC bridge (recommended)
│
├── tools/ # MCP tool implementations
│ ├── __init__.py
│ ├── execution.py # Python execution & debugging
│ ├── documents.py # Document management
│ ├── objects.py # Object creation/manipulation
│ ├── partdesign.py # PartDesign parametric modeling
│ ├── view.py # View, camera, display
│ ├── export.py # Export/import operations
│ └── macros.py # Macro management
│
├── resources/ # MCP resource implementations
│ ├── __init__.py
│ └── freecad.py # Document, console, capabilities
│
├── prompts/ # MCP prompt templates
│ ├── __init__.py
│ └── freecad.py # Modeling and debugging prompts
│
└── utils/ # Utility modules
└── __init__.py
Bridge Architecture
Base Interface
All bridges implement FreecadBridge:
class FreecadBridge(ABC):
@abstractmethod
async def connect(self) -> None: ...
@abstractmethod
async def disconnect(self) -> None: ...
@abstractmethod
async def is_connected(self) -> bool: ...
@abstractmethod
async def execute_python(
self, code: str, timeout_ms: int = 30000
) -> ExecutionResult: ...
XML-RPC Bridge (Recommended)
- Connects to FreeCAD via XML-RPC on port 9875
- Proven, reliable protocol
- Works on all platforms
Socket Bridge
- Uses JSON-RPC over TCP sockets on port 9876
- Lower overhead than XML-RPC
- Easier to debug (JSON format)
Embedded Bridge
- Imports FreeCAD directly into the MCP server process
- Linux only (crashes on macOS/Windows)
- Fastest execution (no IPC overhead)
- Headless mode only
Workbench Addon Architecture
The workbench addon runs inside FreeCAD:
addon/FreecadRobustMCP/
├── Init.py # Module initialization
├── InitGui.py # GUI initialization (workbench)
├── FreecadRobustMCP.svg # Workbench icon
└── freecad_mcp_bridge/ # Bridge plugin
├── __init__.py
├── server.py # XML-RPC/JSON-RPC server
├── blocking_bridge.py # Blocking server (keeps FreeCAD running)
└── startup_bridge.py # Non-blocking startup (for interactive GUI)
package.xml # FreeCAD addon metadata (in project root)
Note: The package.xml file is in the project root, not inside the addon directory. This is because it defines metadata for multiple components (workbench and macros) in a single manifest.
Thread Safety
The workbench uses a queue-based system for thread-safe GUI operations:
# Operations queued from network thread
request_queue.put(operation)
# Executed on main GUI thread via QTimer
def process_queue():
while not request_queue.empty():
op = request_queue.get()
result = op()
response_queue.put(result)
Data Flow
Tool Execution
1. AI Assistant sends tool request
↓
2. MCP Server receives request
↓
3. Tool handler prepares Python code
↓
4. Bridge.execute_python() sends code
↓
5. FreeCAD executes code (main thread)
↓
6. Result returned via bridge
↓
7. MCP Server formats response
↓
8. AI Assistant receives result
Code Execution Pattern
Tools generate Python code that runs in FreeCAD:
@mcp.tool()
async def create_box(length: float = 10.0, ...) -> dict:
bridge = await get_bridge()
code = f'''
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Unnamed")
obj = doc.addObject("Part::Box", "Box")
obj.Length = {length}
doc.recompute()
_result_ = {{"name": obj.Name, "volume": obj.Shape.Volume}}
'''
result = await bridge.execute_python(code)
return result.result
GUI Detection
Tools check FreeCAD.GuiUp to handle headless mode:
code = f'''
if not FreeCAD.GuiUp:
_result_ = {{"success": False, "error": "GUI not available"}}
else:
# GUI-only operations
obj.ViewObject.Visibility = True
_result_ = {{"success": True}}
'''
Configuration
Configuration via environment variables:
| Variable | Default | Description |
|---|---|---|
FREECAD_MODE |
xmlrpc |
Connection mode |
FREECAD_PATH |
auto | FreeCAD lib path (embedded) |
FREECAD_SOCKET_HOST |
localhost |
Socket/XML-RPC host |
FREECAD_SOCKET_PORT |
9876 |
JSON-RPC socket port |
FREECAD_XMLRPC_PORT |
9875 |
XML-RPC port |
FREECAD_TIMEOUT_MS |
30000 |
Execution timeout |
Testing Strategy
Unit Tests
- Mock FreeCAD module
- Test bridge logic in isolation
- Run on all platforms
Integration Tests
- Use FreeCAD AppImage in CI
- Test actual FreeCAD operations
- Run in headless mode
Embedded Mode Testing
Embedded mode receives minimal testing:
- Unit tests with mocked FreeCAD
- No CI integration tests (would require Linux + FreeCAD in-process)
- Recommended to use xmlrpc/socket modes for production
Next Steps
- Contributing - How to contribute
- Detailed Architecture - Complete design details