# Detailed Architecture - FreeCAD Robust MCP Suite
FreeCAD Robust MCP Suite - MCP Server, Bridge Workbench, and Macros for AI assistant integration

Skip to content

# FreeCAD Robust 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 Robust MCP Server Architecture
- Executive Summary
- Table of Contents
- Competitive Analysis
  - Existing FreeCAD Robust MCP Servers
  - Our Differentiators
  - Key Learnings Applied
- System Overview
  - What is MCP?
  - What is FreeCAD?
  - Integration Vision
- Architecture Goals
  - Primary Goals
  - Secondary Goals
- High-Level Architecture
  - Architecture Pattern: Bridge with Adapter
  - Module Structure
- Component Design
  - 1. MCP Server (`server.py`)
  - 2. Bridge Interface (`bridge/base.py`)
  - 3. Embedded Bridge (`bridge/embedded.py`)
  - 4. Socket Bridge (`bridge/socket.py`)
  - 5. FreeCAD Plugin (`freecad_plugin/server.py`)
- MCP Tools Specification
  - Core Execution Tools
  - `execute_python`
  - `execute_macro`
  - Document Management Tools
  - `create_document`
  - `open_document`
  - `save_document`
  - Object Creation Tools
  - `create_primitive`
  - `create_sketch`
  - Geometry Tools
  - `add_sketch_geometry`
  - `boolean_operation`
  - Export/Import Tools
  - `export_mesh`
  - `export_step`
  - Debugging Tools
  - `inspect_object`
  - `validate_model`
  - Workbench Tools
  - `activate_workbench`
  - `list_workbench_commands`
- MCP Resources Specification
  - Document Resources
  - Console Resources
  - Environment Resources
- Communication Patterns
  - Pattern 1: Stdio Transport (Default for MCP Clients)
  - Pattern 2: HTTP Transport (Remote/Shared)
  - Pattern 3: GUI Integration
- Security Considerations
  - Code Execution Sandboxing
  - Resource Limits
  - Authentication (for HTTP transport)
- Deployment Modes
  - Mode 1: XML-RPC Mode (Recommended)
  - Mode 2: Local Embedded (Linux Only)
  - Mode 3: Local GUI with Plugin
  - Mode 4: Remote/Docker
- Error Handling
  - Error Categories
  - Error Response Format
- Future Extensibility
  - Planned Features
  - Extension Points
- Summary

## Competitive Analysis

See COMPARISON.md for detailed analysis of existing implementations.

### Existing FreeCAD Robust MCP Servers

Project | Stars | Approach | Strengths
--- | --- | --- | ---
neka-nat/freecad-mcp | 380+ | XML-RPC | Thread-safe GUI ops, screenshots, parts library
jango-blockchained/mcp-freecad | - | Multi-mode | 6 connection modes, multi-AI, connection recovery
contextform/freecad-mcp | - | Node.js bridge | 31+ CAD operations, installer, demos
ATOI-Ming/FreeCAD-MCP | - | Macro-centric | GUI panel, macro templates, validation
bonninr/freecad_mcp | - | Simple socket | Minimal, lightweight

### Our Differentiators

1. True Headless Mode: Embedded bridge runs FreeCAD in-process (unique)
2. Dual Protocol: XML-RPC (compatibility) + JSON-RPC (modern)
3. MCP Resources: Document/object introspection (no other impl has this)
4. MCP Prompts: Guided workflow templates (unique)
5. Developer Focus: Debugging tools, console access, macro development
6. Thread-Safe Design: Queue-based GUI operations (learned from neka-nat)
7. Connection Resilience: Auto-reconnect with health monitoring

### Key Learnings Applied

1. From neka-nat: Queue-based GUI thread safety, smart screenshot handling
2. From jango: Multiple connection modes, connection recovery
3. From contextform: Comprehensive PartDesign/Part operations
4. From ATOI-Ming: Macro templates, validation, automatic imports

## System Overview

### What is MCP?

The Model Context Protocol (MCP) 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 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

    ┌─────────────────────────────────────────────────────────────────────────┐
    │                           Claude Code                                    │
    │  ┌─────────────────────────────────────────────────────────────────┐    │
    │  │  User Request: "Create a parametric gear with 20 teeth"         │    │
    │  └─────────────────────────────────────────────────────────────────┘    │
    │                                │                                         │
    │                                ▼                                         │
    │  ┌─────────────────────────────────────────────────────────────────┐    │
    │  │               MCP Client (Claude Code, GPT, etc.)                │    │
    │  └─────────────────────────────────────────────────────────────────┘    │
    └─────────────────────────────────────────────────────────────────────────┘
                                     │
                         MCP Protocol (stdio/HTTP)
                                     │
                                     ▼
    ┌─────────────────────────────────────────────────────────────────────────┐
    │                      FreeCAD Robust 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
2. Bidirectional Communication: Send commands and receive results/errors
3. Dual Mode Support: Work with both GUI and headless FreeCAD instances
4. Real-time Feedback: Stream console output and execution results
5. Document Introspection: Query and understand FreeCAD document structure
6. Safe Execution: Sandboxed execution with timeout and resource limits

### Secondary Goals

1. Macro Development: Assist in writing, testing, and debugging macros
2. Workbench Integration: Access workbench-specific functionality
3. Model Validation: Check model integrity and constraints
4. Export Capabilities: Generate various output formats (STEP, STL, etc.)
5. Version Compatibility: Support FreeCAD 0.21+ and 1.0+

## High-Level Architecture

### Architecture Pattern: Bridge with Adapter

    ┌─────────────────────────────────────────────────────────────────────────┐
    │                         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

    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.

    """FreeCAD Robust 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.

    """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).

    """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, "<mcp>", "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).

    """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.

    """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, "<mcp>", "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.

    @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.

    @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`

    @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`

    @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`

    @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`

    @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`

    @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`

    @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`

    @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`

    @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`

    @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`

    @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`

    @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`

    @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`

    @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

    @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

    @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

    @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)

    MCP Client <--stdio--> MCP Server <--embedded/socket--> FreeCAD

Configuration in Claude Code settings (similar for other MCP clients):

    {
      "mcpServers": {
        "freecad": {
          "command": "uv",
          "args": ["run", "freecad-mcp"],
          "env": {
            "FREECAD_MODE": "xmlrpc"
          }
        }
      }
    }

### Pattern 2: HTTP Transport (Remote/Shared)

    MCP Client <--HTTP--> MCP Server <--socket--> FreeCAD

For shared or remote setups:

    {
      "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
2. Plugin starts socket server on FreeCAD startup
3. MCP server connects via socket bridge
4. Full GUI access including view manipulation

    ┌─────────────────────────────────────────────────────────────┐
    │                    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

    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

    @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)

    # 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).

    # .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
2. Start the bridge using the workbench toolbar button or menu
3. 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.

    # .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:

    just install-freecad-plugin

1. Configure MCP server:

    FREECAD_MODE = "socket"
    FREECAD_SOCKET_HOST = "localhost"
    FREECAD_SOCKET_PORT = "9876"

### Mode 4: Remote/Docker

    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

    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

    {
      "success": false,
      "error": {
        "type": "ExecutionError",
        "message": "NameError: name 'foo' is not defined",
        "traceback": "Traceback (most recent call last):\n  File \"<mcp>\", line 1, in <module>\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
2. View Manipulation: Camera control, section views, rendering in GUI mode
3. Parametric Updates: Modify parameters and observe model updates
4. Assembly Support: Integration with Assembly3/4 workbenches
5. FEM Integration: Finite Element Analysis setup and results
6. Path/CAM Support: CAM toolpath generation and simulation
7. Multi-Document: Work across multiple documents simultaneously
8. Undo/Redo: Transaction management with rollback capability

### Extension Points

    # 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
2. Dual Mode Support: Works with GUI and headless FreeCAD
3. Comprehensive Tooling: Document, object, geometry, export tools
4. Rich Resources: Query documents, objects, console, workbenches
5. Secure Execution: Sandboxing, timeouts, resource limits
6. Flexible Deployment: Embedded, socket, remote modes
7. Extensible Design: Plugin system for custom functionality

The MCP server acts as a powerful bridge enabling AI assistants to become intelligent FreeCAD development partners.

2026-01-12
2026-01-04