freecad mcp
by neka-nat
- freecad-mcp
- src
- freecad_mcp
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()