freecad mcp

MIT License
39
  • Apple
  • Linux
import json import logging import xmlrpc.client from contextlib import asynccontextmanager from typing import AsyncIterator, Dict, Any, Literal from mcp.server.fastmcp import FastMCP, Context from mcp.types import TextContent, ImageContent # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) logger = logging.getLogger("FreeCADMCPserver") class FreeCADConnection: def __init__(self, host: str = "localhost", port: int = 9875): self.server = xmlrpc.client.ServerProxy(f"http://{host}:{port}", allow_none=True) def ping(self) -> bool: return self.server.ping() def create_document(self, name: str) -> dict[str, Any]: return self.server.create_document(name) def create_object(self, doc_name: str, obj_data: dict[str, Any]) -> dict[str, Any]: return self.server.create_object(doc_name, obj_data) def edit_object(self, doc_name: str, obj_name: str, obj_data: dict[str, Any]) -> dict[str, Any]: return self.server.edit_object(doc_name, obj_name, obj_data) def delete_object(self, doc_name: str, obj_name: str) -> dict[str, Any]: return self.server.delete_object(doc_name, obj_name) def insert_part_from_library(self, relative_path: str) -> dict[str, Any]: return self.server.insert_part_from_library(relative_path) def execute_code(self, code: str) -> dict[str, Any]: return self.server.execute_code(code) def get_active_screenshot(self, view_name: str = "Isometric") -> str: return self.server.get_active_screenshot(view_name) def get_objects(self, doc_name: str) -> list[dict[str, Any]]: return self.server.get_objects(doc_name) def get_object(self, doc_name: str, obj_name: str) -> dict[str, Any]: return self.server.get_object(doc_name, obj_name) def get_parts_list(self) -> list[str]: return self.server.get_parts_list() @asynccontextmanager async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: try: logger.info("FreeCADMCP server starting up") try: _ = get_freecad_connection() logger.info("Successfully connected to FreeCAD on startup") except Exception as e: logger.warning(f"Could not connect to FreeCAD on startup: {str(e)}") logger.warning( "Make sure the FreeCAD addon is running before using FreeCAD resources or tools" ) yield {} finally: # Clean up the global connection on shutdown global _freecad_connection if _freecad_connection: logger.info("Disconnecting from FreeCAD on shutdown") _freecad_connection.disconnect() _freecad_connection = None logger.info("FreeCADMCP server shut down") mcp = FastMCP( "FreeCADMCP", description="FreeCAD integration through the Model Context Protocol", lifespan=server_lifespan, ) _freecad_connection: FreeCADConnection | None = None def get_freecad_connection(): """Get or create a persistent FreeCAD connection""" global _freecad_connection if _freecad_connection is None: _freecad_connection = FreeCADConnection(host="localhost", port=9875) if not _freecad_connection.ping(): logger.error("Failed to ping FreeCAD") _freecad_connection = None raise Exception( "Failed to connect to FreeCAD. Make sure the FreeCAD addon is running." ) return _freecad_connection @mcp.tool() def create_document(ctx: Context, name: str) -> list[TextContent]: """Create a new document in FreeCAD. Args: name: The name of the document to create. Returns: A message indicating the success or failure of the document creation. Examples: If you want to create a document named "MyDocument", you can use the following data. ```json { "name": "MyDocument" } ``` """ freecad = get_freecad_connection() try: res = freecad.create_document(name) if res["success"]: return [ TextContent(type="text", text=f"Document '{res['document_name']}' created successfully") ] else: return [ TextContent(type="text", text=f"Failed to create document: {res['error']}") ] except Exception as e: logger.error(f"Failed to create document: {str(e)}") return [ TextContent(type="text", text=f"Failed to create document: {str(e)}") ] @mcp.tool() def create_object( ctx: Context, doc_name: str, obj_type: str, obj_name: str, analysis_name: str | None = None, obj_properties: dict[str, Any] = None, ) -> list[TextContent | ImageContent]: """Create a new object in FreeCAD. Object type is starts with "Part::" or "Draft::" or "PartDesign::" or "Fem::". Args: doc_name: The name of the document to create the object in. obj_type: The type of the object to create (e.g. 'Part::Box', 'Part::Cylinder', 'Draft::Circle', 'PartDesign::Body', etc.). obj_name: The name of the object to create. obj_properties: The properties of the object to create. Returns: A message indicating the success or failure of the object creation and a screenshot of the object. Examples: If you want to create a cylinder with a height of 30 and a radius of 10, you can use the following data. ```json { "doc_name": "MyCylinder", "obj_name": "Cylinder", "obj_type": "Part::Cylinder", "obj_properties": { "Height": 30, "Radius": 10, "Placement": { "Base": { "x": 10, "y": 10, "z": 0 }, "Rotation": { "Axis": { "x": 0, "y": 0, "z": 1 }, "Angle": 45 } }, "ViewObject": { "ShapeColor": [0.5, 0.5, 0.5, 1.0] } } } ``` If you want to create a circle with a radius of 10, you can use the following data. ```json { "doc_name": "MyCircle", "obj_name": "Circle", "obj_type": "Draft::Circle", } ``` If you want to create a FEM analysis, you can use the following data. ```json { "doc_name": "MyFEMAnalysis", "obj_name": "FemAnalysis", "obj_type": "Fem::AnalysisPython", } ``` If you want to create a FEM constraint, you can use the following data. ```json { "doc_name": "MyFEMConstraint", "obj_name": "FemConstraint", "obj_type": "Fem::ConstraintFixed", "analysis_name": "MyFEMAnalysis", "obj_properties": { "References": [ { "object_name": "MyObject", "face": "Face1" } ] } } ``` If you want to create a FEM mechanical material, you can use the following data. ```json { "doc_name": "MyFEMAnalysis", "obj_name": "FemMechanicalMaterial", "obj_type": "Fem::MaterialCommon", "analysis_name": "MyFEMAnalysis", "obj_properties": { "Material": { "Name": "MyMaterial", "Density": "7900 kg/m^3", "YoungModulus": "210 GPa", "PoissonRatio": 0.3 } } } ``` If you want to create a FEM mesh, you can use the following data. The `Part` property is required. ```json { "doc_name": "MyFEMMesh", "obj_name": "FemMesh", "obj_type": "Fem::FemMeshGmsh", "analysis_name": "MyFEMAnalysis", "obj_properties": { "Part": "MyObject", "ElementSizeMax": 10, "ElementSizeMin": 0.1, "MeshAlgorithm": 2 } } ``` """ freecad = get_freecad_connection() try: obj_data = {"Name": obj_name, "Type": obj_type, "Properties": obj_properties or {}, "Analysis": analysis_name} res = freecad.create_object(doc_name, obj_data) screenshot = freecad.get_active_screenshot() if res["success"]: return [ TextContent(type="text", text=f"Object '{res['object_name']}' created successfully"), ImageContent(type="image", data=screenshot, mimeType="image/png") ] else: return [ TextContent(type="text", text=f"Failed to create object: {res['error']}"), ImageContent(type="image", data=screenshot, mimeType="image/png") ] except Exception as e: logger.error(f"Failed to create object: {str(e)}") return [ TextContent(type="text", text=f"Failed to create object: {str(e)}") ] @mcp.tool() def edit_object( ctx: Context, doc_name: str, obj_name: str, obj_properties: dict[str, Any] ) -> list[TextContent | ImageContent]: """Edit an object in FreeCAD. This tool is used when the `create_object` tool cannot handle the object creation. Args: doc_name: The name of the document to edit the object in. obj_name: The name of the object to edit. obj_properties: The properties of the object to edit. Returns: A message indicating the success or failure of the object editing and a screenshot of the object. """ freecad = get_freecad_connection() try: res = freecad.edit_object(doc_name, obj_name, obj_properties) screenshot = freecad.get_active_screenshot() if res["success"]: return [ TextContent(type="text", text=f"Object '{res['object_name']}' edited successfully"), ImageContent(type="image", data=screenshot, mimeType="image/png") ] else: return [ TextContent(type="text", text=f"Failed to edit object: {res['error']}"), ImageContent(type="image", data=screenshot, mimeType="image/png") ] except Exception as e: logger.error(f"Failed to edit object: {str(e)}") return [ TextContent(type="text", text=f"Failed to edit object: {str(e)}") ] @mcp.tool() def delete_object(ctx: Context, doc_name: str, obj_name: str) -> list[TextContent | ImageContent]: """Delete an object in FreeCAD. Args: doc_name: The name of the document to delete the object from. obj_name: The name of the object to delete. Returns: A message indicating the success or failure of the object deletion and a screenshot of the object. """ freecad = get_freecad_connection() try: res = freecad.delete_object(doc_name, obj_name) screenshot = freecad.get_active_screenshot() if res["success"]: return [ TextContent(type="text", text=f"Object '{res['object_name']}' deleted successfully"), ImageContent(type="image", data=screenshot, mimeType="image/png") ] else: return [ TextContent(type="text", text=f"Failed to delete object: {res['error']}"), ImageContent(type="image", data=screenshot, mimeType="image/png") ] except Exception as e: logger.error(f"Failed to delete object: {str(e)}") return [ TextContent(type="text", text=f"Failed to delete object: {str(e)}") ] @mcp.tool() def execute_code(ctx: Context, code: str) -> list[TextContent | ImageContent]: """Execute arbitrary Python code in FreeCAD. Args: code: The Python code to execute. Returns: A message indicating the success or failure of the code execution, the output of the code execution, and a screenshot of the object. """ freecad = get_freecad_connection() try: res = freecad.execute_code(code) screenshot = freecad.get_active_screenshot() if res["success"]: return [ TextContent(type="text", text=f"Code executed successfully: {res['message']}"), ImageContent(type="image", data=screenshot, mimeType="image/png") ] else: return [ TextContent(type="text", text=f"Failed to execute code: {res['error']}"), ImageContent(type="image", data=screenshot, mimeType="image/png") ] except Exception as e: logger.error(f"Failed to execute code: {str(e)}") return [ TextContent(type="text", text=f"Failed to execute code: {str(e)}") ] @mcp.tool() def get_view(ctx: Context, view_name: Literal["Isometric", "Front", "Top", "Right", "Back", "Left", "Bottom", "Dimetric", "Trimetric"]) -> list[ImageContent]: """Get a screenshot of the active view. Args: view_name: The name of the view to get the screenshot of. The following views are available: - "Isometric" - "Front" - "Top" - "Right" - "Back" - "Left" - "Bottom" - "Dimetric" - "Trimetric" Returns: A screenshot of the active view. """ freecad = get_freecad_connection() screenshot = freecad.get_active_screenshot(view_name) return [ImageContent(type="image", data=screenshot, mimeType="image/png")] @mcp.tool() def insert_part_from_library(ctx: Context, relative_path: str) -> list[TextContent | ImageContent]: """Insert a part from the parts library addon. Args: relative_path: The relative path of the part to insert. Returns: A message indicating the success or failure of the part insertion and a screenshot of the object. """ freecad = get_freecad_connection() try: res = freecad.insert_part_from_library(relative_path) screenshot = freecad.get_active_screenshot() if res["success"]: return [ TextContent(type="text", text=f"Part inserted from library: {res['message']}"), ImageContent(type="image", data=screenshot, mimeType="image/png") ] else: return [ TextContent(type="text", text=f"Failed to insert part from library: {res['error']}"), ImageContent(type="image", data=screenshot, mimeType="image/png") ] except Exception as e: logger.error(f"Failed to insert part from library: {str(e)}") return [ TextContent(type="text", text=f"Failed to insert part from library: {str(e)}") ] @mcp.tool() def get_objects(ctx: Context, doc_name: str) -> list[dict[str, Any]]: """Get all objects in a document. You can use this tool to get the objects in a document to see what you can check or edit. Args: doc_name: The name of the document to get the objects from. Returns: A list of objects in the document and a screenshot of the document. """ freecad = get_freecad_connection() try: screenshot = freecad.get_active_screenshot() return [ TextContent(type="text", text=json.dumps(freecad.get_objects(doc_name))), ImageContent(type="image", data=screenshot, mimeType="image/png") ] except Exception as e: logger.error(f"Failed to get objects: {str(e)}") return [ TextContent(type="text", text=f"Failed to get objects: {str(e)}") ] @mcp.tool() def get_object(ctx: Context, doc_name: str, obj_name: str) -> dict[str, Any]: """Get an object from a document. You can use this tool to get the properties of an object to see what you can check or edit. Args: doc_name: The name of the document to get the object from. obj_name: The name of the object to get. Returns: The object and a screenshot of the object. """ freecad = get_freecad_connection() try: screenshot = freecad.get_active_screenshot() return [ TextContent(type="text", text=json.dumps(freecad.get_object(doc_name, obj_name))), ImageContent(type="image", data=screenshot, mimeType="image/png") ] except Exception as e: logger.error(f"Failed to get object: {str(e)}") return [ TextContent(type="text", text=f"Failed to get object: {str(e)}") ] @mcp.tool() def get_parts_list(ctx: Context) -> list[str]: """Get the list of parts in the parts library addon. """ freecad = get_freecad_connection() parts = freecad.get_parts_list() if parts: return [ TextContent(type="text", text=json.dumps(parts)) ] else: return [ TextContent(type="text", text=f"No parts found in the parts library. You must add parts_library addon.") ] @mcp.prompt() def asset_creation_strategy() -> str: return """ Asset Creation Strategy for FreeCAD MCP When creating content in FreeCAD, always follow these steps: 0. Before starting any task, always use get_objects() to confirm the current state of the document. 1. Utilize the parts library: - Check available parts using get_parts_list(). - If the required part exists in the library, use insert_part_from_library() to insert it into your document. 2. If the appropriate asset is not available in the parts library: - Create basic shapes (e.g., cubes, cylinders, spheres) using create_object(). - Adjust and define detailed properties of the shapes as necessary using edit_object(). 3. Always assign clear and descriptive names to objects when adding them to the document. 4. Explicitly set the position, scale, and rotation properties of created or inserted objects using edit_object() to ensure proper spatial relationships. 5. After editing an object, always verify that the set properties have been correctly applied by using get_object(). 6. If detailed customization or specialized operations are necessary, use execute_code() to run custom Python scripts. Only revert to basic creation methods in the following cases: - When the required asset is not available in the parts library. - When a basic shape is explicitly requested. - When creating complex shapes requires custom scripting. """ def main(): """Run the MCP server""" mcp.run()