# FreeCAD MCP Server Architecture ## Executive Summary This document describes the architecture for a Model Context Protocol (MCP) server that enables tight integration between AI assistants (Claude, GPT, and other MCP-compatible tools) and FreeCAD. The server provides AI assistants with full access to FreeCAD's Python console capabilities in both GUI and headless modes, enabling AI-assisted development and debugging of models, macros, and workbenches. --- ## Table of Contents - [FreeCAD MCP Server Architecture](#freecad-mcp-server-architecture) - [Executive Summary](#executive-summary) - [Table of Contents](#table-of-contents) - [Competitive Analysis](#competitive-analysis) - [Existing FreeCAD MCP Servers](#existing-freecad-mcp-servers) - [Our Differentiators](#our-differentiators) - [Key Learnings Applied](#key-learnings-applied) - [System Overview](#system-overview) - [What is MCP?](#what-is-mcp) - [What is FreeCAD?](#what-is-freecad) - [Integration Vision](#integration-vision) - [Architecture Goals](#architecture-goals) - [Primary Goals](#primary-goals) - [Secondary Goals](#secondary-goals) - [High-Level Architecture](#high-level-architecture) - [Architecture Pattern: Bridge with Adapter](#architecture-pattern-bridge-with-adapter) - [Module Structure](#module-structure) - [Component Design](#component-design) - [1. MCP Server (`server.py`)](#1-mcp-server-serverpy) - [2. Bridge Interface (`bridge/base.py`)](#2-bridge-interface-bridgebasepy) - [3. Embedded Bridge (`bridge/embedded.py`)](#3-embedded-bridge-bridgeembeddedpy) - [4. Socket Bridge (`bridge/socket.py`)](#4-socket-bridge-bridgesocketpy) - [5. FreeCAD Plugin (`freecad_plugin/server.py`)](#5-freecad-plugin-freecad_pluginserverpy) - [MCP Tools Specification](#mcp-tools-specification) - [Core Execution Tools](#core-execution-tools) - [`execute_python`](#execute_python) - [`execute_macro`](#execute_macro) - [Document Management Tools](#document-management-tools) - [`create_document`](#create_document) - [`open_document`](#open_document) - [`save_document`](#save_document) - [Object Creation Tools](#object-creation-tools) - [`create_primitive`](#create_primitive) - [`create_sketch`](#create_sketch) - [Geometry Tools](#geometry-tools) - [`add_sketch_geometry`](#add_sketch_geometry) - [`boolean_operation`](#boolean_operation) - [Export/Import Tools](#exportimport-tools) - [`export_mesh`](#export_mesh) - [`export_step`](#export_step) - [Debugging Tools](#debugging-tools) - [`inspect_object`](#inspect_object) - [`validate_model`](#validate_model) - [Workbench Tools](#workbench-tools) - [`activate_workbench`](#activate_workbench) - [`list_workbench_commands`](#list_workbench_commands) - [MCP Resources Specification](#mcp-resources-specification) - [Document Resources](#document-resources) - [Console Resources](#console-resources) - [Environment Resources](#environment-resources) - [Communication Patterns](#communication-patterns) - [Pattern 1: Stdio Transport (Default for MCP Clients)](#pattern-1-stdio-transport-default-for-mcp-clients) - [Pattern 2: HTTP Transport (Remote/Shared)](#pattern-2-http-transport-remoteshared) - [Pattern 3: GUI Integration](#pattern-3-gui-integration) - [Security Considerations](#security-considerations) - [Code Execution Sandboxing](#code-execution-sandboxing) - [Resource Limits](#resource-limits) - [Authentication (for HTTP transport)](#authentication-for-http-transport) - [Deployment Modes](#deployment-modes) - [Mode 1: XML-RPC Mode (Recommended)](#mode-1-xml-rpc-mode-recommended) - [Mode 2: Local Embedded (Linux Only)](#mode-2-local-embedded-linux-only) - [Mode 3: Local GUI with Plugin](#mode-3-local-gui-with-plugin) - [Mode 4: Remote/Docker](#mode-4-remotedocker) - [Error Handling](#error-handling) - [Error Categories](#error-categories) - [Error Response Format](#error-response-format) - [Future Extensibility](#future-extensibility) - [Planned Features](#planned-features) - [Extension Points](#extension-points) - [Summary](#summary) --- ## Competitive Analysis See [COMPARISON.md](../COMPARISON.md) for detailed analysis of existing implementations. ### Existing FreeCAD MCP Servers | Project | Stars | Approach | Strengths | | ----------------------------------------------------------------------------------- | ----- | -------------- | ------------------------------------------------- | | [neka-nat/freecad-mcp](https://github.com/neka-nat/freecad-mcp) | 380+ | XML-RPC | Thread-safe GUI ops, screenshots, parts library | | [jango-blockchained/mcp-freecad](https://github.com/jango-blockchained/mcp-freecad) | - | Multi-mode | 6 connection modes, multi-AI, connection recovery | | [contextform/freecad-mcp](https://github.com/contextform/freecad-mcp) | - | Node.js bridge | 31+ CAD operations, installer, demos | | [ATOI-Ming/FreeCAD-MCP](https://github.com/ATOI-Ming/FreeCAD-MCP) | - | Macro-centric | GUI panel, macro templates, validation | | [bonninr/freecad_mcp](https://github.com/bonninr/freecad_mcp) | - | Simple socket | Minimal, lightweight | ### Our Differentiators 1. **True Headless Mode**: Embedded bridge runs FreeCAD in-process (unique) 1. **Dual Protocol**: XML-RPC (compatibility) + JSON-RPC (modern) 1. **MCP Resources**: Document/object introspection (no other impl has this) 1. **MCP Prompts**: Guided workflow templates (unique) 1. **Developer Focus**: Debugging tools, console access, macro development 1. **Thread-Safe Design**: Queue-based GUI operations (learned from neka-nat) 1. **Connection Resilience**: Auto-reconnect with health monitoring ### Key Learnings Applied 1. **From neka-nat**: Queue-based GUI thread safety, smart screenshot handling 1. **From jango**: Multiple connection modes, connection recovery 1. **From contextform**: Comprehensive PartDesign/Part operations 1. **From ATOI-Ming**: Macro templates, validation, automatic imports --- ## System Overview ### What is MCP? The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is a standardized protocol that allows LLM applications (like Claude Code, GPT-based tools, and other MCP clients) to interact with external systems through: - **Tools**: Executable functions that perform actions (like POST endpoints) - **Resources**: Data sources that provide context (like GET endpoints) - **Prompts**: Reusable templates for LLM interactions ### What is FreeCAD? [FreeCAD](https://www.freecadweb.org/) is an open-source parametric 3D CAD modeler with: - A comprehensive Python API for scripting - GUI mode with interactive Python console - Headless mode (`freecadcmd`) for batch processing - Workbench architecture for domain-specific tools - Full access to geometry, constraints, and document structure via Python ### Integration Vision ```text ┌─────────────────────────────────────────────────────────────────────────┐ │ Claude Code │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ User Request: "Create a parametric gear with 20 teeth" │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ MCP Client (Claude Code, GPT, etc.) │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ MCP Protocol (stdio/HTTP) │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ FreeCAD MCP Server │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Tools │ │ Resources │ │ Prompts │ │ │ │ - execute_py │ │ - documents │ │ - modeling │ │ │ │ - create_obj │ │ - objects │ │ - debugging │ │ │ │ - export │ │ - console │ │ - macros │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ ┌───────────┴───────────┐ │ │ ▼ ▼ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ FreeCAD Bridge │ │ FreeCAD Bridge │ │ │ │ (GUI Mode) │ │ (Headless Mode) │ │ │ └──────────────────┘ └──────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ FreeCAD Instance │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Documents │ │ Objects │ │ Workbenches │ │ Macros │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Architecture Goals ### Primary Goals 1. **Full Python Console Access**: Execute arbitrary Python code in FreeCAD's context 1. **Bidirectional Communication**: Send commands and receive results/errors 1. **Dual Mode Support**: Work with both GUI and headless FreeCAD instances 1. **Real-time Feedback**: Stream console output and execution results 1. **Document Introspection**: Query and understand FreeCAD document structure 1. **Safe Execution**: Sandboxed execution with timeout and resource limits ### Secondary Goals 1. **Macro Development**: Assist in writing, testing, and debugging macros 1. **Workbench Integration**: Access workbench-specific functionality 1. **Model Validation**: Check model integrity and constraints 1. **Export Capabilities**: Generate various output formats (STEP, STL, etc.) 1. **Version Compatibility**: Support FreeCAD 0.21+ and 1.0+ --- ## High-Level Architecture ### Architecture Pattern: Bridge with Adapter ```text ┌─────────────────────────────────────────────────────────────────────────┐ │ MCP Server Layer │ │ │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ FastMCP Application │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ │ Tool │ │ Resource │ │ Prompt │ │ Lifecycle│ │ │ │ │ │ Registry │ │ Registry │ │ Registry │ │ Manager │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ └────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ FreeCAD Bridge Interface │ │ │ │ (Abstract Base Class) │ │ │ └────────────────────────────────────────────────────────────────────┘ │ │ ╱ ╲ │ │ ╱ ╲ │ │ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ │ │ EmbeddedBridge │ │ SocketBridge │ │ │ │ (In-process FreeCAD) │ │ (Remote FreeCAD) │ │ │ │ │ │ │ │ │ │ - Direct Python API access │ │ - TCP/Unix socket comm │ │ │ │ - Headless mode only │ │ - GUI or headless mode │ │ │ │ - Fastest execution │ │ - Process isolation │ │ │ └─────────────────────────────┘ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Module Structure ```text freecad_mcp/ ├── __init__.py ├── server.py # Main MCP server entry point ├── config.py # Configuration management │ ├── bridge/ # FreeCAD communication layer │ ├── __init__.py │ ├── base.py # Abstract bridge interface │ ├── embedded.py # In-process FreeCAD bridge (Linux only) │ ├── socket.py # JSON-RPC socket bridge │ ├── xmlrpc.py # XML-RPC bridge (recommended) │ └── protocol.py # Wire protocol for socket communication │ ├── tools/ # MCP tool implementations (82 tools) │ ├── __init__.py │ ├── execution.py # Python execution & debugging (4 tools) │ ├── documents.py # Document management (7 tools) │ ├── objects.py # Object creation/manipulation (12 tools) │ ├── partdesign.py # PartDesign parametric modeling (19 tools) │ ├── view.py # View, camera, display (11 tools) │ ├── export.py # Export/import operations (7 tools) │ └── macros.py # Macro management (6 tools) │ ├── resources/ # MCP resource implementations │ ├── __init__.py │ └── freecad.py # Document, console, capabilities resources │ ├── prompts/ # MCP prompt templates │ ├── __init__.py │ └── freecad.py # Modeling and debugging prompts │ ├── freecad_plugin/ # Plugin to install IN FreeCAD │ ├── __init__.py │ ├── server.py # XML-RPC/JSON-RPC server │ ├── handlers.py # Request handlers │ ├── blocking_bridge.py # Blocking server (keeps FreeCAD running) │ └── startup_bridge.py # Non-blocking startup (for interactive GUI) │ └── utils/ # Utility modules └── __init__.py ``` --- ## Component Design ### 1. MCP Server (`server.py`) The main entry point using FastMCP from the official MCP Python SDK. ```python """FreeCAD MCP Server - Main entry point.""" from mcp.server.fastmcp import FastMCP from freecad_mcp.config import ServerConfig from freecad_mcp.bridge import create_bridge from freecad_mcp.tools import register_all_tools from freecad_mcp.resources import register_all_resources from freecad_mcp.prompts import register_all_prompts mcp = FastMCP( name="freecad-mcp", version="0.1.0", description="MCP server for FreeCAD integration with AI assistants" ) # Bridge instance (initialized on startup) bridge = None @mcp.on_startup async def startup(): """Initialize FreeCAD bridge on server startup.""" global bridge config = ServerConfig.from_env() bridge = await create_bridge(config) @mcp.on_shutdown async def shutdown(): """Clean up FreeCAD bridge on server shutdown.""" if bridge: await bridge.close() ``` ### 2. Bridge Interface (`bridge/base.py`) Abstract interface for FreeCAD communication. ```python """Abstract bridge interface for FreeCAD communication.""" from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any @dataclass class ExecutionResult: """Result of Python code execution.""" success: bool result: Any stdout: str stderr: str execution_time_ms: float error_type: str | None = None error_traceback: str | None = None @dataclass class DocumentInfo: """Information about a FreeCAD document.""" name: str path: str | None objects: list[str] is_modified: bool @dataclass class ObjectInfo: """Information about a FreeCAD object.""" name: str label: str type_id: str properties: dict[str, Any] shape_info: dict[str, Any] | None children: list[str] class FreecadBridge(ABC): """Abstract base class for FreeCAD bridges.""" @abstractmethod async def connect(self) -> None: """Establish connection to FreeCAD.""" pass @abstractmethod async def disconnect(self) -> None: """Close connection to FreeCAD.""" pass @abstractmethod async def is_connected(self) -> bool: """Check if bridge is connected.""" pass @abstractmethod async def execute_python( self, code: str, timeout_ms: int = 30000 ) -> ExecutionResult: """Execute Python code in FreeCAD context.""" pass @abstractmethod async def get_documents(self) -> list[DocumentInfo]: """Get list of open documents.""" pass @abstractmethod async def get_active_document(self) -> DocumentInfo | None: """Get the active document.""" pass @abstractmethod async def get_object( self, doc_name: str, obj_name: str ) -> ObjectInfo: """Get detailed object information.""" pass @abstractmethod async def get_console_output( self, lines: int = 100 ) -> list[str]: """Get recent console output.""" pass @abstractmethod async def get_freecad_version(self) -> dict[str, Any]: """Get FreeCAD version information.""" pass @abstractmethod async def is_gui_available(self) -> bool: """Check if GUI is available.""" pass ``` ### 3. Embedded Bridge (`bridge/embedded.py`) For running FreeCAD in-process (headless mode). ```python """Embedded bridge - runs FreeCAD in-process.""" import asyncio import sys import io from contextlib import redirect_stdout, redirect_stderr from typing import Any from freecad_mcp.bridge.base import ( FreecadBridge, ExecutionResult, DocumentInfo, ObjectInfo, ) class EmbeddedBridge(FreecadBridge): """Bridge that runs FreeCAD embedded in the MCP server process.""" def __init__(self, freecad_path: str | None = None): self._freecad_path = freecad_path self._fc_module = None self._executor = None async def connect(self) -> None: """Import and initialize FreeCAD.""" if self._freecad_path: sys.path.insert(0, self._freecad_path) # Run import in thread pool to avoid blocking loop = asyncio.get_event_loop() self._fc_module = await loop.run_in_executor( None, self._import_freecad ) def _import_freecad(self): """Import FreeCAD module.""" import FreeCAD return FreeCAD async def execute_python( self, code: str, timeout_ms: int = 30000 ) -> ExecutionResult: """Execute Python code in FreeCAD context.""" import time start = time.perf_counter() stdout_capture = io.StringIO() stderr_capture = io.StringIO() # Build execution context with FreeCAD modules exec_globals = { "FreeCAD": self._fc_module, "App": self._fc_module, "__builtins__": __builtins__, } # Add GUI module if available if await self.is_gui_available(): import FreeCADGui exec_globals["FreeCADGui"] = FreeCADGui exec_globals["Gui"] = FreeCADGui try: with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture): # Execute with timeout loop = asyncio.get_event_loop() result = await asyncio.wait_for( loop.run_in_executor( None, lambda: exec(compile(code, "", "exec"), exec_globals) ), timeout=timeout_ms / 1000 ) elapsed = (time.perf_counter() - start) * 1000 return ExecutionResult( success=True, result=exec_globals.get("_result_"), # Convention for return values stdout=stdout_capture.getvalue(), stderr=stderr_capture.getvalue(), execution_time_ms=elapsed, ) except asyncio.TimeoutError: return ExecutionResult( success=False, result=None, stdout=stdout_capture.getvalue(), stderr=stderr_capture.getvalue(), execution_time_ms=timeout_ms, error_type="TimeoutError", error_traceback=f"Execution timed out after {timeout_ms}ms", ) except Exception as e: import traceback elapsed = (time.perf_counter() - start) * 1000 return ExecutionResult( success=False, result=None, stdout=stdout_capture.getvalue(), stderr=stderr_capture.getvalue(), execution_time_ms=elapsed, error_type=type(e).__name__, error_traceback=traceback.format_exc(), ) ``` ### 4. Socket Bridge (`bridge/socket.py`) For communicating with a running FreeCAD instance (GUI or remote). ```python """Socket bridge - communicates with FreeCAD over TCP/Unix socket.""" import asyncio import json from typing import Any from freecad_mcp.bridge.base import ( FreecadBridge, ExecutionResult, DocumentInfo, ObjectInfo, ) from freecad_mcp.bridge.protocol import MCPWireProtocol class SocketBridge(FreecadBridge): """Bridge that communicates with FreeCAD over sockets.""" def __init__( self, host: str = "localhost", port: int = 9876, unix_socket: str | None = None, ): self._host = host self._port = port self._unix_socket = unix_socket self._reader: asyncio.StreamReader | None = None self._writer: asyncio.StreamWriter | None = None self._protocol = MCPWireProtocol() self._request_id = 0 self._pending: dict[int, asyncio.Future] = {} async def connect(self) -> None: """Connect to FreeCAD socket server.""" if self._unix_socket: self._reader, self._writer = await asyncio.open_unix_connection( self._unix_socket ) else: self._reader, self._writer = await asyncio.open_connection( self._host, self._port ) # Start response reader task asyncio.create_task(self._read_responses()) async def _read_responses(self) -> None: """Background task to read responses from FreeCAD.""" while self._reader: try: data = await self._reader.readline() if not data: break response = self._protocol.decode(data) request_id = response.get("id") if request_id in self._pending: self._pending[request_id].set_result(response) except Exception as e: # Handle connection errors break async def _send_request( self, method: str, params: dict[str, Any] ) -> dict[str, Any]: """Send a request and wait for response.""" self._request_id += 1 request_id = self._request_id request = { "id": request_id, "method": method, "params": params, } future: asyncio.Future = asyncio.get_event_loop().create_future() self._pending[request_id] = future try: data = self._protocol.encode(request) self._writer.write(data) await self._writer.drain() response = await asyncio.wait_for(future, timeout=60.0) return response finally: del self._pending[request_id] async def execute_python( self, code: str, timeout_ms: int = 30000 ) -> ExecutionResult: """Execute Python code in FreeCAD context.""" response = await self._send_request("execute", { "code": code, "timeout_ms": timeout_ms, }) if "error" in response: return ExecutionResult( success=False, result=None, stdout="", stderr=response["error"]["message"], execution_time_ms=0, error_type=response["error"].get("type", "Error"), error_traceback=response["error"].get("traceback"), ) result = response["result"] return ExecutionResult( success=True, result=result.get("value"), stdout=result.get("stdout", ""), stderr=result.get("stderr", ""), execution_time_ms=result.get("execution_time_ms", 0), ) ``` ### 5. FreeCAD Plugin (`freecad_plugin/server.py`) Plugin that runs inside FreeCAD to accept socket connections. ```python """FreeCAD plugin - Socket server running inside FreeCAD.""" import asyncio import json import threading import io from contextlib import redirect_stdout, redirect_stderr import FreeCAD import FreeCADGui class FreecadMCPPlugin: """Plugin that runs inside FreeCAD to handle MCP bridge requests.""" def __init__(self, host: str = "localhost", port: int = 9876): self._host = host self._port = port self._server = None self._loop = None self._thread = None def start(self) -> None: """Start the socket server in a background thread.""" self._thread = threading.Thread(target=self._run_server, daemon=True) self._thread.start() FreeCAD.Console.PrintMessage( f"MCP Bridge server started on {self._host}:{self._port}\n" ) def _run_server(self) -> None: """Run the asyncio event loop in background thread.""" self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) self._loop.run_until_complete(self._start_server()) self._loop.run_forever() async def _start_server(self) -> None: """Start the TCP server.""" self._server = await asyncio.start_server( self._handle_client, self._host, self._port, ) async def _handle_client( self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter ) -> None: """Handle a connected client.""" try: while True: data = await reader.readline() if not data: break request = json.loads(data.decode()) response = await self._process_request(request) writer.write(json.dumps(response).encode() + b"\n") await writer.drain() except Exception as e: FreeCAD.Console.PrintError(f"MCP client error: {e}\n") finally: writer.close() async def _process_request(self, request: dict) -> dict: """Process a single request.""" request_id = request.get("id") method = request.get("method") params = request.get("params", {}) try: if method == "execute": result = await self._execute_python(params) elif method == "get_documents": result = self._get_documents() elif method == "get_object": result = self._get_object(params) elif method == "get_version": result = self._get_version() else: return { "id": request_id, "error": {"message": f"Unknown method: {method}"} } return {"id": request_id, "result": result} except Exception as e: import traceback return { "id": request_id, "error": { "type": type(e).__name__, "message": str(e), "traceback": traceback.format_exc(), } } async def _execute_python(self, params: dict) -> dict: """Execute Python code in FreeCAD's main thread.""" import time code = params["code"] timeout_ms = params.get("timeout_ms", 30000) # Must execute in main thread for FreeCAD compatibility result_holder = {} def execute(): start = time.perf_counter() stdout_capture = io.StringIO() stderr_capture = io.StringIO() exec_globals = { "FreeCAD": FreeCAD, "App": FreeCAD, "FreeCADGui": FreeCADGui, "Gui": FreeCADGui, "__builtins__": __builtins__, } try: with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture): exec(compile(code, "", "exec"), exec_globals) result_holder["value"] = exec_globals.get("_result_") result_holder["stdout"] = stdout_capture.getvalue() result_holder["stderr"] = stderr_capture.getvalue() result_holder["execution_time_ms"] = ( time.perf_counter() - start ) * 1000 result_holder["success"] = True except Exception as e: import traceback result_holder["success"] = False result_holder["error_type"] = type(e).__name__ result_holder["error_message"] = str(e) result_holder["error_traceback"] = traceback.format_exc() result_holder["stdout"] = stdout_capture.getvalue() result_holder["stderr"] = stderr_capture.getvalue() result_holder["execution_time_ms"] = ( time.perf_counter() - start ) * 1000 # Schedule execution in FreeCAD's main thread # Use FreeCAD's timer mechanism for thread safety from PySide2 import QtCore event = threading.Event() def main_thread_execute(): execute() event.set() QtCore.QTimer.singleShot(0, main_thread_execute) # Wait for execution with timeout if not event.wait(timeout=timeout_ms / 1000): return { "success": False, "error_type": "TimeoutError", "error_message": f"Execution timed out after {timeout_ms}ms", } return result_holder ``` --- ## MCP Tools Specification ### Core Execution Tools #### `execute_python` Execute arbitrary Python code in FreeCAD's context. ```python @mcp.tool() async def execute_python( code: str, timeout_ms: int = 30000, capture_result: bool = True, ) -> dict: """Execute Python code in FreeCAD's Python console context. Args: code: Python code to execute. Use `_result_ = value` to return data. timeout_ms: Maximum execution time in milliseconds. capture_result: Whether to capture and return the result value. Returns: Dictionary containing: - success: Whether execution completed without errors - result: The value assigned to `_result_` if capture_result is True - stdout: Captured standard output - stderr: Captured standard error - execution_time_ms: Time taken in milliseconds - error_type: Type of exception if failed - error_traceback: Full traceback if failed Example: >>> execute_python(''' ... import Part ... box = Part.makeBox(10, 10, 10) ... _result_ = {"volume": box.Volume, "area": box.Area} ... ''') """ result = await bridge.execute_python(code, timeout_ms) return result.__dict__ ``` #### `execute_macro` Run a FreeCAD macro file. ```python @mcp.tool() async def execute_macro( macro_name: str, args: dict | None = None, ) -> dict: """Execute a FreeCAD macro by name. Args: macro_name: Name of the macro (without .FCMacro extension) args: Optional arguments to pass to the macro as variables Returns: Execution result with stdout/stderr and any errors """ # Build code that loads and runs the macro code = f''' import FreeCAD macro_path = FreeCAD.getUserMacroDir(True) + "/{macro_name}.FCMacro" with open(macro_path) as f: macro_code = f.read() exec(macro_code) ''' return await bridge.execute_python(code) ``` ### Document Management Tools #### `create_document` ```python @mcp.tool() async def create_document( name: str = "Unnamed", label: str | None = None, ) -> dict: """Create a new FreeCAD document. Args: name: Internal document name (no spaces) label: Display label (can contain spaces) Returns: Document information including name and path """ code = f''' doc = FreeCAD.newDocument("{name}") if {repr(label)}: doc.Label = {repr(label)} _result_ = {{"name": doc.Name, "label": doc.Label}} ''' return await bridge.execute_python(code) ``` #### `open_document` ```python @mcp.tool() async def open_document(path: str) -> dict: """Open an existing FreeCAD document. Args: path: Full path to the .FCStd file Returns: Document information """ code = f''' doc = FreeCAD.openDocument({repr(path)}) _result_ = {{ "name": doc.Name, "label": doc.Label, "path": doc.FileName, "objects": [obj.Name for obj in doc.Objects], }} ''' return await bridge.execute_python(code) ``` #### `save_document` ```python @mcp.tool() async def save_document( doc_name: str | None = None, path: str | None = None, ) -> dict: """Save a FreeCAD document. Args: doc_name: Document name (uses active document if None) path: Save path (uses existing path if None) Returns: Save status and path """ code = f''' doc = FreeCAD.getDocument({repr(doc_name)}) if {repr(doc_name)} else FreeCAD.ActiveDocument if {repr(path)}: doc.saveAs({repr(path)}) else: doc.save() _result_ = {{"saved": True, "path": doc.FileName}} ''' return await bridge.execute_python(code) ``` ### Object Creation Tools #### `create_primitive` ```python @mcp.tool() async def create_primitive( primitive_type: str, name: str | None = None, parameters: dict | None = None, doc_name: str | None = None, ) -> dict: """Create a Part primitive object. Args: primitive_type: One of 'Box', 'Cylinder', 'Sphere', 'Cone', 'Torus' name: Object name (auto-generated if None) parameters: Primitive-specific parameters (e.g., {"Length": 10, "Width": 5}) doc_name: Target document (active document if None) Returns: Created object information """ params = parameters or {} code = f''' import Part doc = FreeCAD.getDocument({repr(doc_name)}) if {repr(doc_name)} else FreeCAD.ActiveDocument obj = doc.addObject("Part::{primitive_type}", {repr(name) if name else repr(primitive_type)}) for key, value in {repr(params)}.items(): setattr(obj, key, value) doc.recompute() _result_ = {{ "name": obj.Name, "label": obj.Label, "type": obj.TypeId, "properties": {{p: getattr(obj, p) for p in obj.PropertiesList[:20]}}, }} ''' return await bridge.execute_python(code) ``` #### `create_sketch` ```python @mcp.tool() async def create_sketch( plane: str = "XY", name: str | None = None, doc_name: str | None = None, ) -> dict: """Create a new sketch on a standard plane. Args: plane: One of 'XY', 'XZ', 'YZ' name: Sketch name doc_name: Target document Returns: Sketch object information """ plane_vectors = { "XY": "FreeCAD.Vector(0, 0, 1)", "XZ": "FreeCAD.Vector(0, 1, 0)", "YZ": "FreeCAD.Vector(1, 0, 0)", } code = f''' doc = FreeCAD.getDocument({repr(doc_name)}) if {repr(doc_name)} else FreeCAD.ActiveDocument sketch = doc.addObject("Sketcher::SketchObject", {repr(name) or '"Sketch"'}) sketch.MapMode = "Deactivated" sketch.Placement = FreeCAD.Placement( FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation({plane_vectors[plane]}, 0) ) doc.recompute() _result_ = {{"name": sketch.Name, "geometry_count": sketch.GeometryCount}} ''' return await bridge.execute_python(code) ``` ### Geometry Tools #### `add_sketch_geometry` ```python @mcp.tool() async def add_sketch_geometry( sketch_name: str, geometry_type: str, parameters: dict, doc_name: str | None = None, ) -> dict: """Add geometry to a sketch. Args: sketch_name: Name of the sketch object geometry_type: One of 'line', 'circle', 'arc', 'rectangle', 'point' parameters: Geometry-specific parameters doc_name: Document containing the sketch Returns: Geometry index and sketch state Example parameters: line: {"start": [0, 0], "end": [10, 10]} circle: {"center": [0, 0], "radius": 5} rectangle: {"corner1": [0, 0], "corner2": [10, 10]} """ pass # Implementation depends on geometry_type ``` #### `boolean_operation` ```python @mcp.tool() async def boolean_operation( operation: str, base_object: str, tool_objects: list[str], doc_name: str | None = None, ) -> dict: """Perform boolean operation on Part objects. Args: operation: One of 'fuse' (union), 'cut' (subtract), 'common' (intersect) base_object: Name of the base object tool_objects: List of tool object names doc_name: Document name Returns: Resulting object information """ pass ``` ### Export/Import Tools #### `export_mesh` ```python @mcp.tool() async def export_mesh( objects: list[str], path: str, format: str = "stl", options: dict | None = None, doc_name: str | None = None, ) -> dict: """Export objects as mesh (STL, OBJ, etc.). Args: objects: List of object names to export path: Output file path format: Export format ('stl', 'obj', 'ply', 'off') options: Format-specific options (e.g., mesh deflection) doc_name: Document name Returns: Export status and file info """ pass ``` #### `export_step` ```python @mcp.tool() async def export_step( objects: list[str], path: str, doc_name: str | None = None, ) -> dict: """Export objects as STEP file. Args: objects: List of object names to export path: Output file path (.step or .stp) doc_name: Document name Returns: Export status """ pass ``` ### Debugging Tools #### `inspect_object` ```python @mcp.tool() async def inspect_object( object_name: str, doc_name: str | None = None, include_shape: bool = True, ) -> dict: """Get detailed information about an object. Args: object_name: Name of the object doc_name: Document name include_shape: Include shape geometry details Returns: Comprehensive object information including: - All properties and their values - Shape information (if applicable) - Parent/child relationships - Placement/position data """ pass ``` #### `validate_model` ```python @mcp.tool() async def validate_model( doc_name: str | None = None, check_geometry: bool = True, check_constraints: bool = True, ) -> dict: """Validate model integrity and constraints. Args: doc_name: Document to validate check_geometry: Check for geometry errors check_constraints: Check sketch constraints Returns: Validation results with any issues found """ pass ``` ### Workbench Tools #### `activate_workbench` ```python @mcp.tool() async def activate_workbench(workbench_name: str) -> dict: """Activate a FreeCAD workbench. Args: workbench_name: Workbench identifier (e.g., 'PartDesignWorkbench') Returns: Activation status and available commands """ pass ``` #### `list_workbench_commands` ```python @mcp.tool() async def list_workbench_commands( workbench_name: str | None = None, ) -> dict: """List available commands in a workbench. Args: workbench_name: Workbench to query (active if None) Returns: List of commands with descriptions """ pass ``` --- ## MCP Resources Specification ### Document Resources ```python @mcp.resource("freecad://documents") async def list_documents() -> str: """List all open FreeCAD documents.""" docs = await bridge.get_documents() return json.dumps([d.__dict__ for d in docs], indent=2) @mcp.resource("freecad://documents/{doc_name}") async def get_document(doc_name: str) -> str: """Get detailed document information.""" pass @mcp.resource("freecad://documents/{doc_name}/objects") async def list_objects(doc_name: str) -> str: """List all objects in a document with hierarchy.""" pass @mcp.resource("freecad://documents/{doc_name}/objects/{obj_name}") async def get_object_details(doc_name: str, obj_name: str) -> str: """Get full object details including properties and shape.""" pass ``` ### Console Resources ```python @mcp.resource("freecad://console/history") async def get_console_history() -> str: """Get Python console command history.""" pass @mcp.resource("freecad://console/output") async def get_console_output() -> str: """Get recent console output (stdout/stderr).""" pass ``` ### Environment Resources ```python @mcp.resource("freecad://version") async def get_version() -> str: """Get FreeCAD version and build information.""" pass @mcp.resource("freecad://workbenches") async def list_workbenches() -> str: """List installed workbenches with status.""" pass @mcp.resource("freecad://macros") async def list_macros() -> str: """List installed macros.""" pass @mcp.resource("freecad://preferences/{path}") async def get_preference(path: str) -> str: """Get FreeCAD preference value.""" pass ``` --- ## Communication Patterns ### Pattern 1: Stdio Transport (Default for MCP Clients) ```text MCP Client <--stdio--> MCP Server <--embedded/socket--> FreeCAD ``` Configuration in Claude Code settings (similar for other MCP clients): ```json { "mcpServers": { "freecad": { "command": "uv", "args": ["run", "freecad-mcp"], "env": { "FREECAD_MODE": "xmlrpc" } } } } ``` ### Pattern 2: HTTP Transport (Remote/Shared) ```text MCP Client <--HTTP--> MCP Server <--socket--> FreeCAD ``` For shared or remote setups: ```json { "mcpServers": { "freecad": { "url": "http://localhost:8000/mcp", "transport": "streamable-http" } } } ``` ### Pattern 3: GUI Integration When FreeCAD GUI is running: 1. User installs FreeCAD plugin via Addon Manager 1. Plugin starts socket server on FreeCAD startup 1. MCP server connects via socket bridge 1. Full GUI access including view manipulation ```text ┌─────────────────────────────────────────────────────────────┐ │ FreeCAD GUI │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ MCP Bridge Plugin │ │ │ │ (Socket Server on port 9876) │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ▲ │ TCP Socket ▼ ┌─────────────────────────────────────────────────────────────┐ │ MCP Server (SocketBridge mode) │ └─────────────────────────────────────────────────────────────┘ ▲ │ stdio ▼ ┌─────────────────────────────────────────────────────────────┐ │ MCP Client (Claude Code, etc.) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Security Considerations ### Code Execution Sandboxing ```python class CodeSandbox: """Sandbox for restricting Python code execution.""" FORBIDDEN_MODULES = { "subprocess", "os.system", "pty", "socket", # System access "pickle", "marshal", # Serialization attacks "__import__", # Dynamic imports } FORBIDDEN_BUILTINS = { "exec", "eval", "compile", # Meta-execution "open", "__import__", # File/module access } def validate_code(self, code: str) -> list[str]: """Check code for dangerous patterns.""" import ast issues = [] tree = ast.parse(code) for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: if alias.name in self.FORBIDDEN_MODULES: issues.append(f"Forbidden import: {alias.name}") elif isinstance(node, ast.ImportFrom): if node.module in self.FORBIDDEN_MODULES: issues.append(f"Forbidden import: {node.module}") return issues ``` ### Resource Limits ```python @dataclass class ExecutionLimits: """Resource limits for code execution.""" max_execution_time_ms: int = 30000 # 30 seconds max_memory_mb: int = 512 max_output_size_bytes: int = 1_000_000 # 1 MB max_objects_created: int = 1000 ``` ### Authentication (for HTTP transport) ```python # OAuth 2.1 resource server implementation from mcp.server.auth import ResourceServer auth = ResourceServer( issuer="https://auth.example.com", audience="freecad-mcp", ) @mcp.middleware async def auth_middleware(request, call_next): """Validate OAuth tokens for HTTP transport.""" if mcp.transport_type == "http": await auth.validate_request(request) return await call_next(request) ``` --- ## Deployment Modes ### Mode 1: XML-RPC Mode (Recommended) Connects to FreeCAD via XML-RPC protocol. Works on all platforms (macOS, Linux, Windows). ```yaml # .mise.toml [tools] python = "3.11" # Required for FreeCAD ABI compatibility uv = "latest" [env] FREECAD_MODE = "xmlrpc" FREECAD_XMLRPC_PORT = "9875" ``` **Setup:** 1. Install the Robust MCP Bridge workbench via FreeCAD Addon Manager, or run `just freecad::run-gui` from source 1. Start the bridge using the workbench toolbar button or menu 1. The bridge starts both XML-RPC (port 9875) and JSON-RPC (port 9876) servers ### Mode 2: Local Embedded (Linux Only) Runs FreeCAD in-process for fastest execution. **Only works on Linux** - crashes on macOS/Windows due to `@rpath/libpython3.11.dylib` conflicts. ```yaml # .mise.toml [tools] python = "3.11" # Must match FreeCAD's bundled Python version uv = "latest" [env] FREECAD_MODE = "embedded" FREECAD_PATH = "/usr/lib/freecad/lib" # Linux only ``` ### Mode 3: Local GUI with Plugin 1. Install FreeCAD plugin: ```bash just install-freecad-plugin ``` 1. Configure MCP server: ```yaml FREECAD_MODE = "socket" FREECAD_SOCKET_HOST = "localhost" FREECAD_SOCKET_PORT = "9876" ``` ### Mode 4: Remote/Docker ```dockerfile FROM freecad/freecad:latest # Install MCP plugin COPY freecad_plugin /root/.FreeCAD/Mod/MCPBridge # Expose socket port EXPOSE 9876 CMD ["freecadcmd", "-c", "from freecad_mcp.plugin import start; start()"] ``` --- ## Error Handling ### Error Categories ```python class MCPError(Exception): """Base MCP error.""" pass class ConnectionError(MCPError): """Failed to connect to FreeCAD.""" pass class ExecutionError(MCPError): """Python code execution failed.""" pass class TimeoutError(MCPError): """Execution timed out.""" pass class ValidationError(MCPError): """Input validation failed.""" pass class ResourceNotFoundError(MCPError): """Requested resource does not exist.""" pass ``` ### Error Response Format ```json { "success": false, "error": { "type": "ExecutionError", "message": "NameError: name 'foo' is not defined", "traceback": "Traceback (most recent call last):\n File \"\", line 1, in \nNameError: name 'foo' is not defined", "context": { "code_snippet": "foo.bar()", "line_number": 1 } } } ``` --- ## Future Extensibility ### Planned Features 1. **Streaming Execution Output**: Real-time stdout/stderr streaming for long operations 1. **View Manipulation**: Camera control, section views, rendering in GUI mode 1. **Parametric Updates**: Modify parameters and observe model updates 1. **Assembly Support**: Integration with Assembly3/4 workbenches 1. **FEM Integration**: Finite Element Analysis setup and results 1. **Path/CAM Support**: CAM toolpath generation and simulation 1. **Multi-Document**: Work across multiple documents simultaneously 1. **Undo/Redo**: Transaction management with rollback capability ### Extension Points ```python # Plugin system for custom tools class ToolPlugin(Protocol): """Protocol for tool plugins.""" @property def name(self) -> str: ... @property def tools(self) -> list[Callable]: ... async def initialize(self, bridge: FreecadBridge) -> None: ... # Register custom plugins mcp.register_plugin(MyCustomToolPlugin()) ``` --- ## Summary This architecture provides: 1. **Full Python Console Access**: Execute any FreeCAD Python code 1. **Dual Mode Support**: Works with GUI and headless FreeCAD 1. **Comprehensive Tooling**: Document, object, geometry, export tools 1. **Rich Resources**: Query documents, objects, console, workbenches 1. **Secure Execution**: Sandboxing, timeouts, resource limits 1. **Flexible Deployment**: Embedded, socket, remote modes 1. **Extensible Design**: Plugin system for custom functionality The MCP server acts as a powerful bridge enabling AI assistants to become intelligent FreeCAD development partners.