freecad_mcp.py•10.1 kB
import os
import FreeCAD as App
import FreeCADGui as Gui
import json
import socket
import threading
import time
import traceback
from PySide import QtCore, QtGui
class FreeCADMCPServer:
def __init__(self, host='localhost', port=9876):
self.host = host
self.port = port
self.running = False
self.socket = None
self.client = None
self.buffer = b''
self.timer = None
def start(self):
self.running = True
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
self.socket.bind((self.host, self.port))
self.socket.listen(1)
self.socket.setblocking(False)
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self._process_server)
self.timer.start(100) # 100ms interval
App.Console.PrintMessage(f"FreeCAD MCP server started on {self.host}:{self.port}\n")
except Exception as e:
App.Console.PrintError(f"Failed to start server: {str(e)}\n")
self.stop()
def stop(self):
self.running = False
if self.timer:
self.timer.stop()
self.timer = None
if self.socket:
self.socket.close()
if self.client:
self.client.close()
self.socket = None
self.client = None
App.Console.PrintMessage("FreeCAD MCP server stopped\n")
def _process_server(self):
if not self.running:
return
try:
if not self.client and self.socket:
try:
self.client, address = self.socket.accept()
self.client.setblocking(False)
App.Console.PrintMessage(f"Connected to client: {address}\n")
except BlockingIOError:
pass
except Exception as e:
App.Console.PrintError(f"Error accepting connection: {str(e)}\n")
if self.client:
try:
try:
data = self.client.recv(8192)
if data:
self.buffer += data
try:
command = json.loads(self.buffer.decode('utf-8'))
self.buffer = b''
response = self.execute_command(command)
response_json = json.dumps(response)
self.client.sendall(response_json.encode('utf-8'))
except json.JSONDecodeError:
pass
else:
App.Console.PrintMessage("Client disconnected\n")
self.client.close()
self.client = None
self.buffer = b''
except BlockingIOError:
pass
except Exception as e:
App.Console.PrintError(f"Error receiving data: {str(e)}\n")
self.client.close()
self.client = None
self.buffer = b''
except Exception as e:
App.Console.PrintError(f"Error with client: {str(e)}\n")
if self.client:
self.client.close()
self.client = None
self.buffer = b''
except Exception as e:
App.Console.PrintError(f"Server error: {str(e)}\n")
def execute_command(self, command):
try:
cmd_type = command.get("type")
params = command.get("params", {})
handlers = {
"send_command": self.handle_send_command,
"run_script": self.handle_run_script
}
handler = handlers.get(cmd_type)
if handler:
try:
App.Console.PrintMessage(f"Executing handler for {cmd_type}\n")
result = handler(**params)
return {"status": "success", "result": result}
except Exception as e:
App.Console.PrintError(f"Error in handler: {str(e)}\n")
traceback.print_exc()
return {"status": "error", "message": str(e)}
else:
return {"status": "error", "message": f"Unknown command type: {cmd_type}"}
except Exception as e:
App.Console.PrintError(f"Error executing command: {str(e)}\n")
traceback.print_exc()
return {"status": "error", "message": str(e)}
def handle_send_command(self, command, get_context=True):
"""Handle a send_command request with document context"""
try:
# Execute the command
exec(command, {"App": App, "Gui": Gui})
# Get document context if requested
context = {}
if get_context:
context = self.get_document_context()
return {
"command_result": "success",
"context": context
}
except Exception as e:
return {
"command_result": "error",
"error": str(e),
"traceback": traceback.format_exc()
}
def handle_run_script(self, script):
"""Handle a run_script request"""
try:
# Create a new local namespace for the script
namespace = {
"App": App,
"Gui": Gui,
"doc": App.ActiveDocument
}
# Execute the script
exec(script, namespace)
return {
"script_result": "success"
}
except Exception as e:
return {
"script_result": "error",
"error": str(e),
"traceback": traceback.format_exc()
}
def get_document_context(self):
"""Get comprehensive information about the current document state"""
doc = App.ActiveDocument
if not doc:
return {
"document": None,
"objects": [],
"view": None
}
# Document info
doc_info = {
"name": doc.Name,
"filename": doc.FileName if hasattr(doc, "FileName") else None,
"object_count": len(doc.Objects)
}
# Objects info
objects = []
for obj in doc.Objects:
obj_info = {
"name": obj.Name,
"label": obj.Label,
"type": obj.TypeId,
"visibility": obj.ViewObject.Visibility if hasattr(obj, "ViewObject") else None
}
# Add placement if available
if hasattr(obj, "Placement"):
pos = obj.Placement.Base
rot = obj.Placement.Rotation
obj_info["placement"] = {
"position": [float(pos.x), float(pos.y), float(pos.z)],
"rotation": [float(rot.Axis.x), float(rot.Axis.y), float(rot.Axis.z), float(rot.Angle)]
}
# Add shape properties if available
if hasattr(obj, "Shape"):
shape = obj.Shape
obj_info["shape"] = {
"type": shape.ShapeType,
"volume": float(shape.Volume) if hasattr(shape, "Volume") else None,
"area": float(shape.Area) if hasattr(shape, "Area") else None
}
objects.append(obj_info)
# View state
view_info = None
if Gui.ActiveDocument:
cam = Gui.ActiveDocument.ActiveView.getCameraNode()
view_info = {
"camera_type": cam.getTypeId(),
"camera_position": [float(x) for x in cam.position.getValue()],
"camera_orientation": [float(x) for x in cam.orientation.getValue()]
}
return {
"document": doc_info,
"objects": objects,
"view": view_info
}
class FreeCADMCPPanel:
def __init__(self):
self.form = QtGui.QWidget()
self.form.setWindowTitle("FreeCAD MCP")
layout = QtGui.QVBoxLayout(self.form)
# Server status
self.status_label = QtGui.QLabel("Server: Stopped")
layout.addWidget(self.status_label)
# Start/Stop buttons
button_layout = QtGui.QHBoxLayout()
self.start_button = QtGui.QPushButton("Start Server")
self.stop_button = QtGui.QPushButton("Stop Server")
self.stop_button.setEnabled(False)
self.start_button.clicked.connect(self.start_server)
self.stop_button.clicked.connect(self.stop_server)
button_layout.addWidget(self.start_button)
button_layout.addWidget(self.stop_button)
layout.addLayout(button_layout)
# Server instance
self.server = None
def start_server(self):
if not self.server:
self.server = FreeCADMCPServer()
self.server.start()
self.status_label.setText("Server: Running")
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
def stop_server(self):
if self.server:
self.server.stop()
self.server = None
self.status_label.setText("Server: Stopped")
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
def show_panel():
panel = FreeCADMCPPanel()
Gui.Control.showDialog(panel)