Skip to main content
Glama
freecad_rpc_server.py15.3 kB
#!/usr/bin/env python3 """ FreeCAD XML-RPC Server This script is designed to run inside FreeCAD and provide an XML-RPC interface for remote control of FreeCAD operations. It creates a server that listens for XML-RPC requests and executes them within the FreeCAD environment. Usage: Inside FreeCAD Python Console: ``` import sys sys.path.append("/path/to/mcp-freecad") exec(open("/path/to/mcp-freecad/src/mcp_freecad/client/freecad_rpc_server.py").read()) ``` Or if mcp-freecad is installed as a package: ``` from mcp_freecad.client import freecad_rpc_server freecad_rpc_server.start_rpc_server() ``` """ import base64 import contextlib import io import json import os import queue import tempfile import threading from typing import Any, Dict, List, Optional from xmlrpc.server import SimpleXMLRPCServer try: import FreeCAD import FreeCADGui # Defer QtCore import until FreeCAD GUI is fully initialised to avoid crashes from PySide2 import QtCore FREECAD_AVAILABLE = True except ImportError: FREECAD_AVAILABLE = False print("FreeCAD modules not available. This script must run inside FreeCAD.") # Global variables to track server state rpc_server_thread = None rpc_server_instance = None # Keep reference to timer to avoid garbage collection task_timer = None # GUI task queue (for operations that must run in the main thread) rpc_request_queue = queue.Queue() rpc_response_queue = queue.Queue() def process_gui_tasks(): """Process any pending tasks in the GUI thread queue (runs in GUI thread)""" if not FREECAD_AVAILABLE: return # Process all queued tasks while not rpc_request_queue.empty(): try: task = rpc_request_queue.get_nowait() except Exception: break try: res = task() if res is not None: rpc_response_queue.put(res) except Exception as e: import traceback import FreeCAD FreeCAD.Console.PrintError( f"Error executing GUI task: {e}\n{traceback.format_exc()}\n" ) # Nothing else: function returns, QTimer will trigger again automatically class FreeCADRPC: """RPC server implementation for FreeCAD""" def ping(self): """Test if the server is responsive""" return True def get_version(self): """Get FreeCAD version information""" if not FREECAD_AVAILABLE: return {"success": False, "error": "FreeCAD not available"} try: version = FreeCAD.Version() return { "success": True, "version": { "major": version[0], "minor": version[1], "build": version[2], "string": ".".join(version[:3]), "additional": " ".join(version[3:]), }, } except Exception as e: return {"success": False, "error": str(e)} def create_document(self, name="Unnamed"): """Create a new FreeCAD document""" if not FREECAD_AVAILABLE: return {"success": False, "error": "FreeCAD not available"} # This needs to run in the GUI thread rpc_request_queue.put(lambda: self._create_document_gui(name)) res = rpc_response_queue.get() if isinstance(res, bool) and res: return {"success": True, "document_name": name} else: return {"success": False, "error": res} def _create_document_gui(self, name): """Internal method to create document in GUI thread""" try: doc = FreeCAD.newDocument(name) doc.recompute() FreeCAD.Console.PrintMessage(f"Document '{name}' created via RPC.\n") return True except Exception as e: FreeCAD.Console.PrintError(f"Error creating document: {e}\n") return str(e) def create_object(self, doc_name, obj_data): """Create a new object in a FreeCAD document Args: doc_name: Name of the document obj_data: Dictionary with object data including: - Name: Object name - Type: Object type (e.g., "Part::Box") - Properties: Dictionary of property values Returns: Dictionary with success status and object name or error """ if not FREECAD_AVAILABLE: return {"success": False, "error": "FreeCAD not available"} rpc_request_queue.put(lambda: self._create_object_gui(doc_name, obj_data)) res = rpc_response_queue.get() if isinstance(res, bool) and res: return {"success": True, "object_name": obj_data.get("Name", "Unknown")} else: return {"success": False, "error": res} def _create_object_gui(self, doc_name, obj_data): """Internal method to create object in GUI thread""" try: doc = FreeCAD.getDocument(doc_name) if not doc: return f"Document '{doc_name}' not found" obj_type = obj_data.get("Type") obj_name = obj_data.get("Name", "Unnamed") properties = obj_data.get("Properties", {}) # Create the object obj = doc.addObject(obj_type, obj_name) # Set properties for prop, value in properties.items(): if prop in obj.PropertiesList: # Handle special property types if prop == "Placement" and isinstance(value, dict): base = value.get("Base", {}) rot = value.get("Rotation", {}) placement = FreeCAD.Placement( FreeCAD.Vector( base.get("x", 0), base.get("y", 0), base.get("z", 0) ), FreeCAD.Rotation( FreeCAD.Vector( rot.get("Axis", {}).get("x", 0), rot.get("Axis", {}).get("y", 0), rot.get("Axis", {}).get("z", 1), ), rot.get("Angle", 0), ), ) setattr(obj, prop, placement) elif prop == "ShapeColor" and isinstance(value, list): obj.ViewObject.ShapeColor = tuple(value) else: setattr(obj, prop, value) doc.recompute() FreeCAD.Console.PrintMessage(f"Object '{obj_name}' created via RPC.\n") return True except Exception as e: FreeCAD.Console.PrintError(f"Error creating object: {e}\n") return str(e) def export_stl(self, doc_name, obj_name, file_path): """Export an object to STL format Args: doc_name: Name of the document obj_name: Name of the object to export file_path: Path where to save the STL file Returns: Dictionary with success status and file path or error """ if not FREECAD_AVAILABLE: return {"success": False, "error": "FreeCAD not available"} rpc_request_queue.put( lambda: self._export_stl_gui(doc_name, obj_name, file_path) ) res = rpc_response_queue.get() if isinstance(res, bool) and res: return {"success": True, "path": file_path} else: return {"success": False, "error": res} def _export_stl_gui(self, doc_name, obj_name, file_path): """Internal method to export STL in GUI thread""" try: doc = FreeCAD.getDocument(doc_name) if not doc: return f"Document '{doc_name}' not found" obj = doc.getObject(obj_name) if not obj: return f"Object '{obj_name}' not found in document '{doc_name}'" import Mesh Mesh.export([obj], file_path) FreeCAD.Console.PrintMessage(f"Exported '{obj_name}' to '{file_path}'.\n") return True except Exception as e: FreeCAD.Console.PrintError(f"Error exporting to STL: {e}\n") return str(e) def execute_code(self, code): """Execute arbitrary Python code in FreeCAD Args: code: Python code to execute Returns: Dictionary with success status and output or error """ if not FREECAD_AVAILABLE: return {"success": False, "error": "FreeCAD not available"} output_buffer = io.StringIO() def execute_task(): try: with contextlib.redirect_stdout(output_buffer): exec(code, globals()) return True except Exception as e: FreeCAD.Console.PrintError(f"Error executing Python code: {e}\n") return str(e) rpc_request_queue.put(execute_task) res = rpc_response_queue.get() if isinstance(res, bool) and res: return { "success": True, "message": "Code executed successfully", "output": output_buffer.getvalue(), } else: return {"success": False, "error": res} def get_active_screenshot(self, view_name="Isometric"): """Get a screenshot of the active view Args: view_name: Name of the view (Isometric, Front, Top, etc.) Returns: Base64-encoded PNG image data """ if not FREECAD_AVAILABLE: return None temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False) rpc_request_queue.put( lambda: self._save_screenshot_gui(temp_file.name, view_name) ) res = rpc_response_queue.get() if isinstance(res, bool) and res: with open(temp_file.name, "rb") as image_file: image_bytes = image_file.read() os.remove(temp_file.name) return base64.b64encode(image_bytes).decode("utf-8") else: return None def _save_screenshot_gui(self, save_path, view_name="Isometric"): """Internal method to save screenshot in GUI thread""" try: view = FreeCADGui.ActiveDocument.ActiveView # Set view orientation if view_name == "Isometric": view.viewIsometric() elif view_name == "Front": view.viewFront() elif view_name == "Top": view.viewTop() elif view_name == "Right": view.viewRight() elif view_name == "Back": view.viewBack() elif view_name == "Left": view.viewLeft() elif view_name == "Bottom": view.viewBottom() else: # Default to isometric for unsupported view names view.viewIsometric() view.fitAll() view.saveImage(save_path, 1920, 1080) return True except Exception as e: FreeCAD.Console.PrintError(f"Error saving screenshot: {e}\n") return str(e) def start_rpc_server(host="localhost", port=9875): """Start the XML-RPC server Args: host: Host address to bind to port: Port number to use Returns: String message about server status """ global rpc_server_thread, rpc_server_instance if not FREECAD_AVAILABLE: return "Error: FreeCAD modules not available. Cannot start RPC server." if rpc_server_instance: return "RPC Server already running." try: # Create server instance rpc_server_instance = SimpleXMLRPCServer( (host, port), allow_none=True, logRequests=False ) rpc_server_instance.register_instance(FreeCADRPC()) # Start server in a separate thread def server_loop(): FreeCAD.Console.PrintMessage(f"RPC Server started at {host}:{port}\n") rpc_server_instance.serve_forever() rpc_server_thread = threading.Thread(target=server_loop, daemon=True) rpc_server_thread.start() # Start task processing timer using persistent QTimer to avoid singleShot recursion global task_timer task_timer = QtCore.QTimer() task_timer.setInterval(500) # 500 ms task_timer.timeout.connect(process_gui_tasks) task_timer.start() return f"RPC Server started at {host}:{port}" except Exception as e: return f"Error starting RPC server: {e}" def stop_rpc_server(): """Stop the XML-RPC server Returns: String message about server status """ global rpc_server_instance, rpc_server_thread if not rpc_server_instance: return "RPC Server not running." try: rpc_server_instance.shutdown() rpc_server_thread.join() rpc_server_instance = None rpc_server_thread = None FreeCAD.Console.PrintMessage("RPC Server stopped.\n") return "RPC Server stopped." except Exception as e: return f"Error stopping RPC server: {e}" # Auto-start if run directly if __name__ == "__main__" and FREECAD_AVAILABLE: try: # Try to register FreeCAD GUI commands for the RPC server from PySide2 import QtCore class StartRPCServerCommand: """FreeCAD command to start the RPC server""" def GetResources(self): return { "MenuText": "Start RPC Server", "ToolTip": "Start XML-RPC Server for MCP-FreeCAD", "Pixmap": "", } def Activated(self): msg = start_rpc_server() FreeCAD.Console.PrintMessage(msg + "\n") def IsActive(self): return True class StopRPCServerCommand: """FreeCAD command to stop the RPC server""" def GetResources(self): return { "MenuText": "Stop RPC Server", "ToolTip": "Stop XML-RPC Server for MCP-FreeCAD", "Pixmap": "", } def Activated(self): msg = stop_rpc_server() FreeCAD.Console.PrintMessage(msg + "\n") def IsActive(self): return True # Register the commands FreeCADGui.addCommand("MCP_Start_RPC_Server", StartRPCServerCommand()) FreeCADGui.addCommand("MCP_Stop_RPC_Server", StopRPCServerCommand()) # Auto-start the server start_msg = start_rpc_server() FreeCAD.Console.PrintMessage(start_msg + "\n") except Exception as e: FreeCAD.Console.PrintError(f"Error setting up RPC server commands: {e}\n") # Still try to start the server even if GUI commands fail start_msg = start_rpc_server() FreeCAD.Console.PrintMessage(start_msg + "\n")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jango-blockchained/mcp-freecad'

If you have feedback or need assistance with the MCP directory API, please join our Discord server