#
tokens: 15311/50000 16/16 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .python-version
├── addon
│   └── FreeCADMCP
│       ├── Init.py
│       ├── InitGui.py
│       └── rpc_server
│           ├── __init__.py
│           ├── parts_library.py
│           ├── rpc_server.py
│           └── serialize.py
├── assets
│   ├── b9-1.png
│   ├── freecad_mcp4.gif
│   ├── from_2ddrawing.gif
│   ├── make_toycar4.gif
│   ├── start_rpc_server.png
│   └── workbench_list.png
├── examples
│   ├── adk
│   │   ├── __init__.py
│   │   ├── .env
│   │   └── agent.py
│   └── langchain
│       └── react.py
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│   └── freecad_mcp
│       ├── __init__.py
│       ├── py.typed
│       └── server.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.12
2 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | __pycache__/
2 | 
```

--------------------------------------------------------------------------------
/examples/adk/.env:
--------------------------------------------------------------------------------

```
1 | GOOGLE_API_KEY=xxxx
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/neka-nat-freecad-mcp-badge.png)](https://mseep.ai/app/neka-nat-freecad-mcp)
  2 | 
  3 | # FreeCAD MCP
  4 | 
  5 | This repository is a FreeCAD MCP that allows you to control FreeCAD from Claude Desktop.
  6 | 
  7 | ## Demo
  8 | 
  9 | ### Design a flange
 10 | 
 11 | ![demo](./assets/freecad_mcp4.gif)
 12 | 
 13 | ### Design a toy car
 14 | 
 15 | ![demo](./assets/make_toycar4.gif)
 16 | 
 17 | ### Design a part from 2D drawing
 18 | 
 19 | #### Input 2D drawing
 20 | 
 21 | ![input](./assets/b9-1.png)
 22 | 
 23 | #### Demo
 24 | 
 25 | ![demo](./assets/from_2ddrawing.gif)
 26 | 
 27 | This is the conversation history.
 28 | https://claude.ai/share/7b48fd60-68ba-46fb-bb21-2fbb17399b48
 29 | 
 30 | ## Install addon
 31 | 
 32 | FreeCAD Addon directory is
 33 | * Windows: `%APPDATA%\FreeCAD\Mod\`
 34 | * Mac: `~/Library/Application\ Support/FreeCAD/Mod/`
 35 | * Linux:
 36 |   * Ubuntu: `~/.FreeCAD/Mod/` or `~/snap/freecad/common/Mod/` (if you install FreeCAD from snap)
 37 |   * Debian: `~/.local/share/FreeCAD/Mod`
 38 | 
 39 | Please put `addon/FreeCADMCP` directory to the addon directory.
 40 | 
 41 | ```bash
 42 | git clone https://github.com/neka-nat/freecad-mcp.git
 43 | cd freecad-mcp
 44 | cp -r addon/FreeCADMCP ~/.FreeCAD/Mod/
 45 | ```
 46 | 
 47 | When you install addon, you need to restart FreeCAD.
 48 | You can select "MCP Addon" from Workbench list and use it.
 49 | 
 50 | ![workbench_list](./assets/workbench_list.png)
 51 | 
 52 | And you can start RPC server by "Start RPC Server" command in "FreeCAD MCP" toolbar.
 53 | 
 54 | ![start_rpc_server](./assets/start_rpc_server.png)
 55 | 
 56 | ## Setting up Claude Desktop
 57 | 
 58 | Pre-installation of the [uvx](https://docs.astral.sh/uv/guides/tools/) is required.
 59 | 
 60 | And you need to edit Claude Desktop config file, `claude_desktop_config.json`.
 61 | 
 62 | For user.
 63 | 
 64 | ```json
 65 | {
 66 |   "mcpServers": {
 67 |     "freecad": {
 68 |       "command": "uvx",
 69 |       "args": [
 70 |         "freecad-mcp"
 71 |       ]
 72 |     }
 73 |   }
 74 | }
 75 | ```
 76 | 
 77 | If you want to save token, you can set `only_text_feedback` to `true` and use only text feedback.
 78 | 
 79 | ```json
 80 | {
 81 |   "mcpServers": {
 82 |     "freecad": {
 83 |       "command": "uvx",
 84 |       "args": [
 85 |         "freecad-mcp",
 86 |         "--only-text-feedback"
 87 |       ]
 88 |     }
 89 |   }
 90 | }
 91 | ```
 92 | 
 93 | 
 94 | For developer.
 95 | First, you need clone this repository.
 96 | 
 97 | ```bash
 98 | git clone https://github.com/neka-nat/freecad-mcp.git
 99 | ```
100 | 
101 | ```json
102 | {
103 |   "mcpServers": {
104 |     "freecad": {
105 |       "command": "uv",
106 |       "args": [
107 |         "--directory",
108 |         "/path/to/freecad-mcp/",
109 |         "run",
110 |         "freecad-mcp"
111 |       ]
112 |     }
113 |   }
114 | }
115 | ```
116 | 
117 | ## Tools
118 | 
119 | * `create_document`: Create a new document in FreeCAD.
120 | * `create_object`: Create a new object in FreeCAD.
121 | * `edit_object`: Edit an object in FreeCAD.
122 | * `delete_object`: Delete an object in FreeCAD.
123 | * `execute_code`: Execute arbitrary Python code in FreeCAD.
124 | * `insert_part_from_library`: Insert a part from the [parts library](https://github.com/FreeCAD/FreeCAD-library).
125 | * `get_view`: Get a screenshot of the active view.
126 | * `get_objects`: Get all objects in a document.
127 | * `get_object`: Get an object in a document.
128 | * `get_parts_list`: Get the list of parts in the [parts library](https://github.com/FreeCAD/FreeCAD-library).
129 | 
130 | ## Contributors
131 | 
132 | <a href="https://github.com/neka-nat/freecad-mcp/graphs/contributors">
133 |   <img src="https://contrib.rocks/image?repo=neka-nat/freecad-mcp" />
134 | </a>
135 | 
136 | Made with [contrib.rocks](https://contrib.rocks).
137 | 
```

--------------------------------------------------------------------------------
/src/freecad_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/addon/FreeCADMCP/Init.py:
--------------------------------------------------------------------------------

```python
1 | 
2 | 
```

--------------------------------------------------------------------------------
/examples/adk/__init__.py:
--------------------------------------------------------------------------------

```python
1 | from .import agent
```

--------------------------------------------------------------------------------
/addon/FreeCADMCP/rpc_server/__init__.py:
--------------------------------------------------------------------------------

```python
1 | from . import rpc_server
2 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "freecad-mcp"
 3 | version = "0.1.13"
 4 | description = "Add your description here"
 5 | readme = "README.md"
 6 | authors = [
 7 |     { name = "neka-nat", email = "[email protected]" }
 8 | ]
 9 | license = { text = "MIT" }
10 | requires-python = ">=3.12"
11 | dependencies = [
12 |     "mcp[cli]>=1.12.2",
13 | ]
14 | 
15 | [project.scripts]
16 | freecad-mcp = "freecad_mcp.server:main"
17 | 
18 | [tool.hatch.build.targets.sdist]
19 | exclude = ["assets", "results"]
20 | 
21 | [build-system]
22 | requires = ["hatchling"]
23 | build-backend = "hatchling.build"
24 | 
```

--------------------------------------------------------------------------------
/addon/FreeCADMCP/InitGui.py:
--------------------------------------------------------------------------------

```python
 1 | class FreeCADMCPAddonWorkbench(Workbench):
 2 |     MenuText = "MCP Addon"
 3 |     ToolTip = "Addon for MCP Communication"
 4 | 
 5 |     def Initialize(self):
 6 |         from rpc_server import rpc_server
 7 | 
 8 |         commands = ["Start_RPC_Server", "Stop_RPC_Server"]
 9 |         self.appendToolbar("FreeCAD MCP", commands)
10 |         self.appendMenu("FreeCAD MCP", commands)
11 | 
12 |     def Activated(self):
13 |         pass
14 | 
15 |     def Deactivated(self):
16 |         pass
17 | 
18 |     def ContextMenu(self, recipient):
19 |         pass
20 | 
21 |     def GetClassName(self):
22 |         return "Gui::PythonWorkbench"
23 | 
24 | 
25 | Gui.addWorkbench(FreeCADMCPAddonWorkbench())
26 | 
```

--------------------------------------------------------------------------------
/examples/adk/agent.py:
--------------------------------------------------------------------------------

```python
 1 | from google.adk.agents.llm_agent import LlmAgent
 2 | from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters
 3 | 
 4 | # Agent configuration
 5 | AGENT_NAME = "cad_design_agent"
 6 | MODEL_NAME = "gemini-2.5-flash-lite"
 7 | FREECAD_MCP_DIR = "path/to/freecad-mcp"  # Replace with actual path
 8 | 
 9 | # Basic instruction
10 | BASIC_PROMPT = "You are a CAD designer."
11 | 
12 | # Initialize agent
13 | root_agent = LlmAgent(
14 |     model=MODEL_NAME,
15 |     name=AGENT_NAME,
16 |     instruction=BASIC_PROMPT,
17 |     tools=[
18 |         MCPToolset(
19 |             connection_params=StdioServerParameters(
20 |                 command="uv",
21 |                 args=["--directory", FREECAD_MCP_DIR, "run", "freecad-mcp"]
22 |             )
23 |         )
24 |     ]
25 | )
26 | 
```

--------------------------------------------------------------------------------
/addon/FreeCADMCP/rpc_server/parts_library.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | from functools import cache
 3 | 
 4 | import FreeCAD
 5 | import FreeCADGui
 6 | 
 7 | 
 8 | def insert_part_from_library(relative_path):
 9 |     parts_lib_path = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", "parts_library")
10 |     part_path = os.path.join(parts_lib_path, relative_path)
11 | 
12 |     if not os.path.exists(part_path):
13 |         raise FileNotFoundError(f"Not found: {part_path}")
14 | 
15 |     FreeCADGui.ActiveDocument.mergeProject(part_path)
16 | 
17 | 
18 | @cache
19 | def get_parts_list() -> list[str]:
20 |     parts_lib_path = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", "parts_library")
21 | 
22 |     if not os.path.exists(parts_lib_path):
23 |         raise FileNotFoundError(f"Not found: {parts_lib_path}")
24 | 
25 |     parts = []
26 | 
27 |     for root, _, files in os.walk(parts_lib_path):
28 |         for file in files:
29 |             if file.endswith(".FCStd"):
30 |                 relative_path = os.path.relpath(os.path.join(root, file), parts_lib_path)
31 |                 parts.append(relative_path)
32 | 
33 |     return parts
34 | 
```

--------------------------------------------------------------------------------
/examples/langchain/react.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | import logging
 3 | import asyncio
 4 | from langchain_groq import ChatGroq
 5 | from mcp import ClientSession, StdioServerParameters
 6 | from langchain_mcp_adapters.tools import load_mcp_tools
 7 | from langgraph.prebuilt import create_react_agent
 8 | from langchain_core.messages import SystemMessage, HumanMessage
 9 | from mcp.client.stdio import stdio_client
10 | 
11 | # Setup logging and environment
12 | logging.basicConfig(level=logging.INFO)
13 | 
14 | 
15 | # Initialize LLM
16 | llm = ChatGroq(
17 |     model="llama-3.1-8b-instant",
18 |     temperature=0.7,
19 |     name="cad_design_agent"
20 | )
21 | 
22 | # MCP server parameters
23 | server_params = StdioServerParameters(
24 |     command="uv",
25 |     args=["--directory", "path/to/freecad-mcp", "run", "freecad-mcp"]
26 | )
27 | 
28 | # Basic CAD assistant prompt
29 | INSTRUCTION = "You are a CAD designer."
30 | 
31 | async def main():
32 |     if "GROQ_API_KEY" not in os.environ:
33 |         logging.error("GROQ_API_KEY is missing.")
34 |         return
35 | 
36 |     logging.info("Starting MCP client...")
37 |     async with stdio_client(server_params) as (read, write):
38 |         logging.info("Connected to MCP server.")
39 |         async with ClientSession(read, write) as session:
40 |             await session.initialize()
41 |             tools = await load_mcp_tools(session)
42 | 
43 |             agent = create_react_agent(llm, tools)
44 | 
45 |             print("\n Ready! Type 'exit' to quit.\n")
46 |             while True:
47 |                 user_input = input("You: ").strip()
48 |                 if user_input.lower() in ("exit", "quit"):
49 |                     print("Goodbye!")
50 |                     break
51 | 
52 |                 messages = [
53 |                     SystemMessage(content=INSTRUCTION),
54 |                     HumanMessage(content=user_input)
55 |                 ]
56 |                 try:
57 |                     response = await agent.ainvoke({"messages": messages})
58 |                     ai_msgs = response.get("messages", [])
59 |                     if ai_msgs:
60 |                         print(f"\n{ai_msgs[-1].content}\n")
61 |                     else:
62 |                         print("No response from agent.")
63 |                 except Exception as e:
64 |                     logging.error(f"Agent error: {e}")
65 |                     print("Something went wrong.")
66 | 
67 | if __name__ == "__main__":
68 |     asyncio.run(main())
69 | 
```

--------------------------------------------------------------------------------
/addon/FreeCADMCP/rpc_server/serialize.py:
--------------------------------------------------------------------------------

```python
 1 | import FreeCAD as App
 2 | import json
 3 | 
 4 | 
 5 | def serialize_value(value):
 6 |     if isinstance(value, (int, float, str, bool)):
 7 |         return value
 8 |     elif isinstance(value, App.Vector):
 9 |         return {"x": value.x, "y": value.y, "z": value.z}
10 |     elif isinstance(value, App.Rotation):
11 |         return {
12 |             "Axis": {"x": value.Axis.x, "y": value.Axis.y, "z": value.Axis.z},
13 |             "Angle": value.Angle,
14 |         }
15 |     elif isinstance(value, App.Placement):
16 |         return {
17 |             "Base": serialize_value(value.Base),
18 |             "Rotation": serialize_value(value.Rotation),
19 |         }
20 |     elif isinstance(value, (list, tuple)):
21 |         return [serialize_value(v) for v in value]
22 |     elif isinstance(value, (App.Color)):
23 |         return tuple(value)
24 |     else:
25 |         return str(value)
26 | 
27 | 
28 | def serialize_shape(shape):
29 |     if shape is None:
30 |         return None
31 |     return {
32 |         "Volume": shape.Volume,
33 |         "Area": shape.Area,
34 |         "VertexCount": len(shape.Vertexes),
35 |         "EdgeCount": len(shape.Edges),
36 |         "FaceCount": len(shape.Faces),
37 |     }
38 | 
39 | 
40 | def serialize_view_object(view):
41 |     if view is None:
42 |         return None
43 |     return {
44 |         "ShapeColor": serialize_value(view.ShapeColor),
45 |         "Transparency": view.Transparency,
46 |         "Visibility": view.Visibility,
47 |     }
48 | 
49 | 
50 | def serialize_object(obj):
51 |     if isinstance(obj, list):
52 |         return [serialize_object(item) for item in obj]
53 |     elif isinstance(obj, App.Document):
54 |         return {
55 |             "Name": obj.Name,
56 |             "Label": obj.Label,
57 |             "FileName": obj.FileName,
58 |             "Objects": [serialize_object(child) for child in obj.Objects],
59 |         }
60 |     else:
61 |         result = {
62 |             "Name": obj.Name,
63 |             "Label": obj.Label,
64 |             "TypeId": obj.TypeId,
65 |             "Properties": {},
66 |             "Placement": serialize_value(getattr(obj, "Placement", None)),
67 |             "Shape": serialize_shape(getattr(obj, "Shape", None)),
68 |             "ViewObject": {},
69 |         }
70 | 
71 |         for prop in obj.PropertiesList:
72 |             try:
73 |                 result["Properties"][prop] = serialize_value(getattr(obj, prop))
74 |             except Exception as e:
75 |                 result["Properties"][prop] = f"<error: {str(e)}>"
76 | 
77 |         if hasattr(obj, "ViewObject") and obj.ViewObject is not None:
78 |             view = obj.ViewObject
79 |             result["ViewObject"] = serialize_view_object(view)
80 | 
81 |         return result
82 | 
```

--------------------------------------------------------------------------------
/addon/FreeCADMCP/rpc_server/rpc_server.py:
--------------------------------------------------------------------------------

```python
  1 | import FreeCAD
  2 | import FreeCADGui
  3 | import ObjectsFem
  4 | 
  5 | import contextlib
  6 | import queue
  7 | import base64
  8 | import io
  9 | import os
 10 | import tempfile
 11 | import threading
 12 | from dataclasses import dataclass, field
 13 | from typing import Any
 14 | from xmlrpc.server import SimpleXMLRPCServer
 15 | 
 16 | from PySide import QtCore
 17 | 
 18 | from .parts_library import get_parts_list, insert_part_from_library
 19 | from .serialize import serialize_object
 20 | 
 21 | rpc_server_thread = None
 22 | rpc_server_instance = None
 23 | 
 24 | # GUI task queue
 25 | rpc_request_queue = queue.Queue()
 26 | rpc_response_queue = queue.Queue()
 27 | 
 28 | 
 29 | def process_gui_tasks():
 30 |     while not rpc_request_queue.empty():
 31 |         task = rpc_request_queue.get()
 32 |         res = task()
 33 |         if res is not None:
 34 |             rpc_response_queue.put(res)
 35 |     QtCore.QTimer.singleShot(500, process_gui_tasks)
 36 | 
 37 | 
 38 | @dataclass
 39 | class Object:
 40 |     name: str
 41 |     type: str | None = None
 42 |     analysis: str | None = None
 43 |     properties: dict[str, Any] = field(default_factory=dict)
 44 | 
 45 | 
 46 | def set_object_property(
 47 |     doc: FreeCAD.Document, obj: FreeCAD.DocumentObject, properties: dict[str, Any]
 48 | ):
 49 |     for prop, val in properties.items():
 50 |         try:
 51 |             if prop in obj.PropertiesList:
 52 |                 if prop == "Placement" and isinstance(val, dict):
 53 |                     if "Base" in val:
 54 |                         pos = val["Base"]
 55 |                     elif "Position" in val:
 56 |                         pos = val["Position"]
 57 |                     else:
 58 |                         pos = {}
 59 |                     rot = val.get("Rotation", {})
 60 |                     placement = FreeCAD.Placement(
 61 |                         FreeCAD.Vector(
 62 |                             pos.get("x", 0),
 63 |                             pos.get("y", 0),
 64 |                             pos.get("z", 0),
 65 |                         ),
 66 |                         FreeCAD.Rotation(
 67 |                             FreeCAD.Vector(
 68 |                                 rot.get("Axis", {}).get("x", 0),
 69 |                                 rot.get("Axis", {}).get("y", 0),
 70 |                                 rot.get("Axis", {}).get("z", 1),
 71 |                             ),
 72 |                             rot.get("Angle", 0),
 73 |                         ),
 74 |                     )
 75 |                     setattr(obj, prop, placement)
 76 | 
 77 |                 elif isinstance(getattr(obj, prop), FreeCAD.Vector) and isinstance(
 78 |                     val, dict
 79 |                 ):
 80 |                     vector = FreeCAD.Vector(
 81 |                         val.get("x", 0), val.get("y", 0), val.get("z", 0)
 82 |                     )
 83 |                     setattr(obj, prop, vector)
 84 | 
 85 |                 elif prop in ["Base", "Tool", "Source", "Profile"] and isinstance(
 86 |                     val, str
 87 |                 ):
 88 |                     ref_obj = doc.getObject(val)
 89 |                     if ref_obj:
 90 |                         setattr(obj, prop, ref_obj)
 91 |                     else:
 92 |                         raise ValueError(f"Referenced object '{val}' not found.")
 93 | 
 94 |                 elif prop == "References" and isinstance(val, list):
 95 |                     refs = []
 96 |                     for ref_name, face in val:
 97 |                         ref_obj = doc.getObject(ref_name)
 98 |                         if ref_obj:
 99 |                             refs.append((ref_obj, face))
100 |                         else:
101 |                             raise ValueError(f"Referenced object '{ref_name}' not found.")
102 |                     setattr(obj, prop, refs)
103 | 
104 |                 else:
105 |                     setattr(obj, prop, val)
106 |             # ShapeColor is a property of the ViewObject
107 |             elif prop == "ShapeColor" and isinstance(val, (list, tuple)):
108 |                 setattr(obj.ViewObject, prop, (float(val[0]), float(val[1]), float(val[2]), float(val[3])))
109 | 
110 |             elif prop == "ViewObject" and isinstance(val, dict):
111 |                 for k, v in val.items():
112 |                     if k == "ShapeColor":
113 |                         setattr(obj.ViewObject, k, (float(v[0]), float(v[1]), float(v[2]), float(v[3])))
114 |                     else:
115 |                         setattr(obj.ViewObject, k, v)
116 | 
117 |             else:
118 |                 setattr(obj, prop, val)
119 | 
120 |         except Exception as e:
121 |             FreeCAD.Console.PrintError(f"Property '{prop}' assignment error: {e}\n")
122 | 
123 | 
124 | class FreeCADRPC:
125 |     """RPC server for FreeCAD"""
126 | 
127 |     def ping(self):
128 |         return True
129 | 
130 |     def create_document(self, name="New_Document"):
131 |         rpc_request_queue.put(lambda: self._create_document_gui(name))
132 |         res = rpc_response_queue.get()
133 |         if res is True:
134 |             return {"success": True, "document_name": name}
135 |         else:
136 |             return {"success": False, "error": res}
137 | 
138 |     def create_object(self, doc_name, obj_data: dict[str, Any]):
139 |         obj = Object(
140 |             name=obj_data.get("Name", "New_Object"),
141 |             type=obj_data["Type"],
142 |             analysis=obj_data.get("Analysis", None),
143 |             properties=obj_data.get("Properties", {}),
144 |         )
145 |         rpc_request_queue.put(lambda: self._create_object_gui(doc_name, obj))
146 |         res = rpc_response_queue.get()
147 |         if res is True:
148 |             return {"success": True, "object_name": obj.name}
149 |         else:
150 |             return {"success": False, "error": res}
151 | 
152 |     def edit_object(self, doc_name: str, obj_name: str, properties: dict[str, Any]) -> dict[str, Any]:
153 |         obj = Object(
154 |             name=obj_name,
155 |             properties=properties.get("Properties", {}),
156 |         )
157 |         rpc_request_queue.put(lambda: self._edit_object_gui(doc_name, obj))
158 |         res = rpc_response_queue.get()
159 |         if res is True:
160 |             return {"success": True, "object_name": obj.name}
161 |         else:
162 |             return {"success": False, "error": res}
163 | 
164 |     def delete_object(self, doc_name: str, obj_name: str):
165 |         rpc_request_queue.put(lambda: self._delete_object_gui(doc_name, obj_name))
166 |         res = rpc_response_queue.get()
167 |         if res is True:
168 |             return {"success": True, "object_name": obj_name}
169 |         else:
170 |             return {"success": False, "error": res}
171 | 
172 |     def execute_code(self, code: str) -> dict[str, Any]:
173 |         output_buffer = io.StringIO()
174 |         def task():
175 |             try:
176 |                 with contextlib.redirect_stdout(output_buffer):
177 |                     exec(code, globals())
178 |                 FreeCAD.Console.PrintMessage("Python code executed successfully.\n")
179 |                 return True
180 |             except Exception as e:
181 |                 FreeCAD.Console.PrintError(
182 |                     f"Error executing Python code: {e}\n"
183 |                 )
184 |                 return f"Error executing Python code: {e}\n"
185 | 
186 |         rpc_request_queue.put(task)
187 |         res = rpc_response_queue.get()
188 |         if res is True:
189 |             return {
190 |                 "success": True,
191 |                 "message": "Python code execution scheduled. \nOutput: " + output_buffer.getvalue()
192 |             }
193 |         else:
194 |             return {"success": False, "error": res}
195 | 
196 |     def get_objects(self, doc_name):
197 |         doc = FreeCAD.getDocument(doc_name)
198 |         if doc:
199 |             return [serialize_object(obj) for obj in doc.Objects]
200 |         else:
201 |             return []
202 | 
203 |     def get_object(self, doc_name, obj_name):
204 |         doc = FreeCAD.getDocument(doc_name)
205 |         if doc:
206 |             return serialize_object(doc.getObject(obj_name))
207 |         else:
208 |             return None
209 | 
210 |     def insert_part_from_library(self, relative_path):
211 |         rpc_request_queue.put(lambda: self._insert_part_from_library(relative_path))
212 |         res = rpc_response_queue.get()
213 |         if res is True:
214 |             return {"success": True, "message": "Part inserted from library."}
215 |         else:
216 |             return {"success": False, "error": res}
217 | 
218 |     def list_documents(self):
219 |         return list(FreeCAD.listDocuments().keys())
220 | 
221 |     def get_parts_list(self):
222 |         return get_parts_list()
223 | 
224 |     def get_active_screenshot(self, view_name: str = "Isometric") -> str:
225 |         """Get a screenshot of the active view.
226 |         
227 |         Returns a base64-encoded string of the screenshot or None if a screenshot
228 |         cannot be captured (e.g., when in TechDraw or Spreadsheet view).
229 |         """
230 |         # First check if the active view supports screenshots
231 |         def check_view_supports_screenshots():
232 |             try:
233 |                 active_view = FreeCADGui.ActiveDocument.ActiveView
234 |                 if active_view is None:
235 |                     FreeCAD.Console.PrintWarning("No active view available\n")
236 |                     return False
237 |                 
238 |                 view_type = type(active_view).__name__
239 |                 has_save_image = hasattr(active_view, 'saveImage')
240 |                 FreeCAD.Console.PrintMessage(f"View type: {view_type}, Has saveImage: {has_save_image}\n")
241 |                 return has_save_image
242 |             except Exception as e:
243 |                 FreeCAD.Console.PrintError(f"Error checking view capabilities: {e}\n")
244 |                 return False
245 |                 
246 |         rpc_request_queue.put(check_view_supports_screenshots)
247 |         supports_screenshots = rpc_response_queue.get()
248 |         
249 |         if not supports_screenshots:
250 |             FreeCAD.Console.PrintWarning("Current view does not support screenshots\n")
251 |             return None
252 |             
253 |         # If view supports screenshots, proceed with capture
254 |         fd, tmp_path = tempfile.mkstemp(suffix=".png")
255 |         os.close(fd)
256 |         rpc_request_queue.put(
257 |             lambda: self._save_active_screenshot(tmp_path, view_name)
258 |         )
259 |         res = rpc_response_queue.get()
260 |         if res is True:
261 |             try:
262 |                 with open(tmp_path, "rb") as image_file:
263 |                     image_bytes = image_file.read()
264 |                     encoded = base64.b64encode(image_bytes).decode("utf-8")
265 |             finally:
266 |                 if os.path.exists(tmp_path):
267 |                     os.remove(tmp_path)
268 |             return encoded
269 |         else:
270 |             if os.path.exists(tmp_path):
271 |                 os.remove(tmp_path)
272 |             FreeCAD.Console.PrintWarning(f"Failed to capture screenshot: {res}\n")
273 |             return None
274 | 
275 |     def _create_document_gui(self, name):
276 |         doc = FreeCAD.newDocument(name)
277 |         doc.recompute()
278 |         FreeCAD.Console.PrintMessage(f"Document '{name}' created via RPC.\n")
279 |         return True
280 | 
281 |     def _create_object_gui(self, doc_name, obj: Object):
282 |         doc = FreeCAD.getDocument(doc_name)
283 |         if doc:
284 |             try:
285 |                 if obj.type == "Fem::FemMeshGmsh" and obj.analysis:
286 |                     from femmesh.gmshtools import GmshTools
287 |                     res = getattr(doc, obj.analysis).addObject(ObjectsFem.makeMeshGmsh(doc, obj.name))[0]
288 |                     if "Part" in obj.properties:
289 |                         target_obj = doc.getObject(obj.properties["Part"])
290 |                         if target_obj:
291 |                             res.Part = target_obj
292 |                         else:
293 |                             raise ValueError(f"Referenced object '{obj.properties['Part']}' not found.")
294 |                         del obj.properties["Part"]
295 |                     else:
296 |                         raise ValueError("'Part' property not found in properties.")
297 | 
298 |                     for param, value in obj.properties.items():
299 |                         if hasattr(res, param):
300 |                             setattr(res, param, value)
301 |                     doc.recompute()
302 | 
303 |                     gmsh_tools = GmshTools(res)
304 |                     gmsh_tools.create_mesh()
305 |                     FreeCAD.Console.PrintMessage(
306 |                         f"FEM Mesh '{res.Name}' generated successfully in '{doc_name}'.\n"
307 |                     )
308 |                 elif obj.type.startswith("Fem::"):
309 |                     fem_make_methods = {
310 |                         "MaterialCommon": ObjectsFem.makeMaterialSolid,
311 |                         "AnalysisPython": ObjectsFem.makeAnalysis,
312 |                     }
313 |                     obj_type_short = obj.type.split("::")[1]
314 |                     method_name = "make" + obj_type_short
315 |                     make_method = fem_make_methods.get(obj_type_short, getattr(ObjectsFem, method_name, None))
316 | 
317 |                     if callable(make_method):
318 |                         res = make_method(doc, obj.name)
319 |                         set_object_property(doc, res, obj.properties)
320 |                         FreeCAD.Console.PrintMessage(
321 |                             f"FEM object '{res.Name}' created with '{method_name}'.\n"
322 |                         )
323 |                     else:
324 |                         raise ValueError(f"No creation method '{method_name}' found in ObjectsFem.")
325 |                     if obj.type != "Fem::AnalysisPython" and obj.analysis:
326 |                         getattr(doc, obj.analysis).addObject(res)
327 |                 else:
328 |                     res = doc.addObject(obj.type, obj.name)
329 |                     set_object_property(doc, res, obj.properties)
330 |                     FreeCAD.Console.PrintMessage(
331 |                         f"{res.TypeId} '{res.Name}' added to '{doc_name}' via RPC.\n"
332 |                     )
333 |  
334 |                 doc.recompute()
335 |                 return True
336 |             except Exception as e:
337 |                 return str(e)
338 |         else:
339 |             FreeCAD.Console.PrintError(f"Document '{doc_name}' not found.\n")
340 |             return f"Document '{doc_name}' not found.\n"
341 | 
342 |     def _edit_object_gui(self, doc_name: str, obj: Object):
343 |         doc = FreeCAD.getDocument(doc_name)
344 |         if not doc:
345 |             FreeCAD.Console.PrintError(f"Document '{doc_name}' not found.\n")
346 |             return f"Document '{doc_name}' not found.\n"
347 | 
348 |         obj_ins = doc.getObject(obj.name)
349 |         if not obj_ins:
350 |             FreeCAD.Console.PrintError(f"Object '{obj.name}' not found in document '{doc_name}'.\n")
351 |             return f"Object '{obj.name}' not found in document '{doc_name}'.\n"
352 | 
353 |         try:
354 |             # For Fem::ConstraintFixed
355 |             if hasattr(obj_ins, "References") and "References" in obj.properties:
356 |                 refs = []
357 |                 for ref_name, face in obj.properties["References"]:
358 |                     ref_obj = doc.getObject(ref_name)
359 |                     if ref_obj:
360 |                         refs.append((ref_obj, face))
361 |                     else:
362 |                         raise ValueError(f"Referenced object '{ref_name}' not found.")
363 |                 obj_ins.References = refs
364 |                 FreeCAD.Console.PrintMessage(
365 |                     f"References updated for '{obj.name}' in '{doc_name}'.\n"
366 |                 )
367 |                 # delete References from properties
368 |                 del obj.properties["References"]
369 |             set_object_property(doc, obj_ins, obj.properties)
370 |             doc.recompute()
371 |             FreeCAD.Console.PrintMessage(f"Object '{obj.name}' updated via RPC.\n")
372 |             return True
373 |         except Exception as e:
374 |             return str(e)
375 | 
376 |     def _delete_object_gui(self, doc_name: str, obj_name: str):
377 |         doc = FreeCAD.getDocument(doc_name)
378 |         if not doc:
379 |             FreeCAD.Console.PrintError(f"Document '{doc_name}' not found.\n")
380 |             return f"Document '{doc_name}' not found.\n"
381 | 
382 |         try:
383 |             doc.removeObject(obj_name)
384 |             doc.recompute()
385 |             FreeCAD.Console.PrintMessage(f"Object '{obj_name}' deleted via RPC.\n")
386 |             return True
387 |         except Exception as e:
388 |             return str(e)
389 | 
390 |     def _insert_part_from_library(self, relative_path):
391 |         try:
392 |             insert_part_from_library(relative_path)
393 |             return True
394 |         except Exception as e:
395 |             return str(e)
396 | 
397 |     def _save_active_screenshot(self, save_path: str, view_name: str = "Isometric"):
398 |         try:
399 |             view = FreeCADGui.ActiveDocument.ActiveView
400 |             # Check if the view supports screenshots
401 |             if not hasattr(view, 'saveImage'):
402 |                 return "Current view does not support screenshots"
403 |                 
404 |             if view_name == "Isometric":
405 |                 view.viewIsometric()
406 |             elif view_name == "Front":
407 |                 view.viewFront()
408 |             elif view_name == "Top":
409 |                 view.viewTop()
410 |             elif view_name == "Right":
411 |                 view.viewRight()
412 |             elif view_name == "Back":
413 |                 view.viewBack()
414 |             elif view_name == "Left":
415 |                 view.viewLeft()
416 |             elif view_name == "Bottom":
417 |                 view.viewBottom()
418 |             elif view_name == "Dimetric":
419 |                 view.viewDimetric()
420 |             elif view_name == "Trimetric":
421 |                 view.viewTrimetric()
422 |             else:
423 |                 raise ValueError(f"Invalid view name: {view_name}")
424 |             view.fitAll()
425 |             view.saveImage(save_path, 1)
426 |             return True
427 |         except Exception as e:
428 |             return str(e)
429 | 
430 | 
431 | def start_rpc_server(host="localhost", port=9875):
432 |     global rpc_server_thread, rpc_server_instance
433 | 
434 |     if rpc_server_instance:
435 |         return "RPC Server already running."
436 | 
437 |     rpc_server_instance = SimpleXMLRPCServer(
438 |         (host, port), allow_none=True, logRequests=False
439 |     )
440 |     rpc_server_instance.register_instance(FreeCADRPC())
441 | 
442 |     def server_loop():
443 |         FreeCAD.Console.PrintMessage(f"RPC Server started at {host}:{port}\n")
444 |         rpc_server_instance.serve_forever()
445 | 
446 |     rpc_server_thread = threading.Thread(target=server_loop, daemon=True)
447 |     rpc_server_thread.start()
448 | 
449 |     QtCore.QTimer.singleShot(500, process_gui_tasks)
450 | 
451 |     return f"RPC Server started at {host}:{port}."
452 | 
453 | 
454 | def stop_rpc_server():
455 |     global rpc_server_instance, rpc_server_thread
456 | 
457 |     if rpc_server_instance:
458 |         rpc_server_instance.shutdown()
459 |         rpc_server_thread.join()
460 |         rpc_server_instance = None
461 |         rpc_server_thread = None
462 |         FreeCAD.Console.PrintMessage("RPC Server stopped.\n")
463 |         return "RPC Server stopped."
464 | 
465 |     return "RPC Server was not running."
466 | 
467 | 
468 | class StartRPCServerCommand:
469 |     def GetResources(self):
470 |         return {"MenuText": "Start RPC Server", "ToolTip": "Start RPC Server"}
471 | 
472 |     def Activated(self):
473 |         msg = start_rpc_server()
474 |         FreeCAD.Console.PrintMessage(msg + "\n")
475 | 
476 |     def IsActive(self):
477 |         return True
478 | 
479 | 
480 | class StopRPCServerCommand:
481 |     def GetResources(self):
482 |         return {"MenuText": "Stop RPC Server", "ToolTip": "Stop RPC Server"}
483 | 
484 |     def Activated(self):
485 |         msg = stop_rpc_server()
486 |         FreeCAD.Console.PrintMessage(msg + "\n")
487 | 
488 |     def IsActive(self):
489 |         return True
490 | 
491 | 
492 | FreeCADGui.addCommand("Start_RPC_Server", StartRPCServerCommand())
493 | FreeCADGui.addCommand("Stop_RPC_Server", StopRPCServerCommand())
```

--------------------------------------------------------------------------------
/src/freecad_mcp/server.py:
--------------------------------------------------------------------------------

```python
  1 | import json
  2 | import logging
  3 | import xmlrpc.client
  4 | from contextlib import asynccontextmanager
  5 | from typing import AsyncIterator, Dict, Any, Literal
  6 | 
  7 | from mcp.server.fastmcp import FastMCP, Context
  8 | from mcp.types import TextContent, ImageContent
  9 | 
 10 | # Configure logging
 11 | logging.basicConfig(
 12 |     level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
 13 | )
 14 | logger = logging.getLogger("FreeCADMCPserver")
 15 | 
 16 | 
 17 | _only_text_feedback = False
 18 | 
 19 | 
 20 | class FreeCADConnection:
 21 |     def __init__(self, host: str = "localhost", port: int = 9875):
 22 |         self.server = xmlrpc.client.ServerProxy(f"http://{host}:{port}", allow_none=True)
 23 | 
 24 |     def ping(self) -> bool:
 25 |         return self.server.ping()
 26 | 
 27 |     def create_document(self, name: str) -> dict[str, Any]:
 28 |         return self.server.create_document(name)
 29 | 
 30 |     def create_object(self, doc_name: str, obj_data: dict[str, Any]) -> dict[str, Any]:
 31 |         return self.server.create_object(doc_name, obj_data)
 32 | 
 33 |     def edit_object(self, doc_name: str, obj_name: str, obj_data: dict[str, Any]) -> dict[str, Any]:
 34 |         return self.server.edit_object(doc_name, obj_name, obj_data)
 35 | 
 36 |     def delete_object(self, doc_name: str, obj_name: str) -> dict[str, Any]:
 37 |         return self.server.delete_object(doc_name, obj_name)
 38 | 
 39 |     def insert_part_from_library(self, relative_path: str) -> dict[str, Any]:
 40 |         return self.server.insert_part_from_library(relative_path)
 41 | 
 42 |     def execute_code(self, code: str) -> dict[str, Any]:
 43 |         return self.server.execute_code(code)
 44 | 
 45 |     def get_active_screenshot(self, view_name: str = "Isometric") -> str | None:
 46 |         try:
 47 |             # Check if we're in a view that supports screenshots
 48 |             result = self.server.execute_code("""
 49 | import FreeCAD
 50 | import FreeCADGui
 51 | 
 52 | if FreeCAD.Gui.ActiveDocument and FreeCAD.Gui.ActiveDocument.ActiveView:
 53 |     view_type = type(FreeCAD.Gui.ActiveDocument.ActiveView).__name__
 54 |     
 55 |     # These view types don't support screenshots
 56 |     unsupported_views = ['SpreadsheetGui::SheetView', 'DrawingGui::DrawingView', 'TechDrawGui::MDIViewPage']
 57 |     
 58 |     if view_type in unsupported_views or not hasattr(FreeCAD.Gui.ActiveDocument.ActiveView, 'saveImage'):
 59 |         print("Current view does not support screenshots")
 60 |         False
 61 |     else:
 62 |         print(f"Current view supports screenshots: {view_type}")
 63 |         True
 64 | else:
 65 |     print("No active view")
 66 |     False
 67 | """)
 68 | 
 69 |             # If the view doesn't support screenshots, return None
 70 |             if not result.get("success", False) or "Current view does not support screenshots" in result.get("message", ""):
 71 |                 logger.info("Screenshot unavailable in current view (likely Spreadsheet or TechDraw view)")
 72 |                 return None
 73 | 
 74 |             # Otherwise, try to get the screenshot
 75 |             return self.server.get_active_screenshot(view_name)
 76 |         except Exception as e:
 77 |             # Log the error but return None instead of raising an exception
 78 |             logger.error(f"Error getting screenshot: {e}")
 79 |             return None
 80 | 
 81 |     def get_objects(self, doc_name: str) -> list[dict[str, Any]]:
 82 |         return self.server.get_objects(doc_name)
 83 | 
 84 |     def get_object(self, doc_name: str, obj_name: str) -> dict[str, Any]:
 85 |         return self.server.get_object(doc_name, obj_name)
 86 | 
 87 |     def get_parts_list(self) -> list[str]:
 88 |         return self.server.get_parts_list()
 89 | 
 90 | 
 91 | @asynccontextmanager
 92 | async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
 93 |     try:
 94 |         logger.info("FreeCADMCP server starting up")
 95 |         try:
 96 |             _ = get_freecad_connection()
 97 |             logger.info("Successfully connected to FreeCAD on startup")
 98 |         except Exception as e:
 99 |             logger.warning(f"Could not connect to FreeCAD on startup: {str(e)}")
100 |             logger.warning(
101 |                 "Make sure the FreeCAD addon is running before using FreeCAD resources or tools"
102 |             )
103 |         yield {}
104 |     finally:
105 |         # Clean up the global connection on shutdown
106 |         global _freecad_connection
107 |         if _freecad_connection:
108 |             logger.info("Disconnecting from FreeCAD on shutdown")
109 |             _freecad_connection.disconnect()
110 |             _freecad_connection = None
111 |         logger.info("FreeCADMCP server shut down")
112 | 
113 | 
114 | mcp = FastMCP(
115 |     "FreeCADMCP",
116 |     instructions="FreeCAD integration through the Model Context Protocol",
117 |     lifespan=server_lifespan,
118 | )
119 | 
120 | 
121 | _freecad_connection: FreeCADConnection | None = None
122 | 
123 | 
124 | def get_freecad_connection():
125 |     """Get or create a persistent FreeCAD connection"""
126 |     global _freecad_connection
127 |     if _freecad_connection is None:
128 |         _freecad_connection = FreeCADConnection(host="localhost", port=9875)
129 |         if not _freecad_connection.ping():
130 |             logger.error("Failed to ping FreeCAD")
131 |             _freecad_connection = None
132 |             raise Exception(
133 |                 "Failed to connect to FreeCAD. Make sure the FreeCAD addon is running."
134 |             )
135 |     return _freecad_connection
136 | 
137 | 
138 | # Helper function to safely add screenshot to response
139 | def add_screenshot_if_available(response, screenshot):
140 |     """Safely add screenshot to response only if it's available"""
141 |     if screenshot is not None and not _only_text_feedback:
142 |         response.append(ImageContent(type="image", data=screenshot, mimeType="image/png"))
143 |     elif not _only_text_feedback:
144 |         # Add an informative message that will be seen by the AI model and user
145 |         response.append(TextContent(
146 |             type="text", 
147 |             text="Note: Visual preview is unavailable in the current view type (such as TechDraw or Spreadsheet). "
148 |                  "Switch to a 3D view to see visual feedback."
149 |         ))
150 |     return response
151 | 
152 | 
153 | @mcp.tool()
154 | def create_document(ctx: Context, name: str) -> list[TextContent]:
155 |     """Create a new document in FreeCAD.
156 | 
157 |     Args:
158 |         name: The name of the document to create.
159 | 
160 |     Returns:
161 |         A message indicating the success or failure of the document creation.
162 | 
163 |     Examples:
164 |         If you want to create a document named "MyDocument", you can use the following data.
165 |         ```json
166 |         {
167 |             "name": "MyDocument"
168 |         }
169 |         ```
170 |     """
171 |     freecad = get_freecad_connection()
172 |     try:
173 |         res = freecad.create_document(name)
174 |         if res["success"]:
175 |             return [
176 |                 TextContent(type="text", text=f"Document '{res['document_name']}' created successfully")
177 |             ]
178 |         else:
179 |             return [
180 |                 TextContent(type="text", text=f"Failed to create document: {res['error']}")
181 |             ]
182 |     except Exception as e:
183 |         logger.error(f"Failed to create document: {str(e)}")
184 |         return [
185 |             TextContent(type="text", text=f"Failed to create document: {str(e)}")
186 |         ]
187 | 
188 | 
189 | @mcp.tool()
190 | def create_object(
191 |     ctx: Context,
192 |     doc_name: str,
193 |     obj_type: str,
194 |     obj_name: str,
195 |     analysis_name: str | None = None,
196 |     obj_properties: dict[str, Any] = None,
197 | ) -> list[TextContent | ImageContent]:
198 |     """Create a new object in FreeCAD.
199 |     Object type is starts with "Part::" or "Draft::" or "PartDesign::" or "Fem::".
200 | 
201 |     Args:
202 |         doc_name: The name of the document to create the object in.
203 |         obj_type: The type of the object to create (e.g. 'Part::Box', 'Part::Cylinder', 'Draft::Circle', 'PartDesign::Body', etc.).
204 |         obj_name: The name of the object to create.
205 |         obj_properties: The properties of the object to create.
206 | 
207 |     Returns:
208 |         A message indicating the success or failure of the object creation and a screenshot of the object.
209 | 
210 |     Examples:
211 |         If you want to create a cylinder with a height of 30 and a radius of 10, you can use the following data.
212 |         ```json
213 |         {
214 |             "doc_name": "MyCylinder",
215 |             "obj_name": "Cylinder",
216 |             "obj_type": "Part::Cylinder",
217 |             "obj_properties": {
218 |                 "Height": 30,
219 |                 "Radius": 10,
220 |                 "Placement": {
221 |                     "Base": {
222 |                         "x": 10,
223 |                         "y": 10,
224 |                         "z": 0
225 |                     },
226 |                     "Rotation": {
227 |                         "Axis": {
228 |                             "x": 0,
229 |                             "y": 0,
230 |                             "z": 1
231 |                         },
232 |                         "Angle": 45
233 |                     }
234 |                 },
235 |                 "ViewObject": {
236 |                     "ShapeColor": [0.5, 0.5, 0.5, 1.0]
237 |                 }
238 |             }
239 |         }
240 |         ```
241 | 
242 |         If you want to create a circle with a radius of 10, you can use the following data.
243 |         ```json
244 |         {
245 |             "doc_name": "MyCircle",
246 |             "obj_name": "Circle",
247 |             "obj_type": "Draft::Circle",
248 |         }
249 |         ```
250 | 
251 |         If you want to create a FEM analysis, you can use the following data.
252 |         ```json
253 |         {
254 |             "doc_name": "MyFEMAnalysis",
255 |             "obj_name": "FemAnalysis",
256 |             "obj_type": "Fem::AnalysisPython",
257 |         }
258 |         ```
259 | 
260 |         If you want to create a FEM constraint, you can use the following data.
261 |         ```json
262 |         {
263 |             "doc_name": "MyFEMConstraint",
264 |             "obj_name": "FemConstraint",
265 |             "obj_type": "Fem::ConstraintFixed",
266 |             "analysis_name": "MyFEMAnalysis",
267 |             "obj_properties": {
268 |                 "References": [
269 |                     {
270 |                         "object_name": "MyObject",
271 |                         "face": "Face1"
272 |                     }
273 |                 ]
274 |             }
275 |         }
276 |         ```
277 | 
278 |         If you want to create a FEM mechanical material, you can use the following data.
279 |         ```json
280 |         {
281 |             "doc_name": "MyFEMAnalysis",
282 |             "obj_name": "FemMechanicalMaterial",
283 |             "obj_type": "Fem::MaterialCommon",
284 |             "analysis_name": "MyFEMAnalysis",
285 |             "obj_properties": {
286 |                 "Material": {
287 |                     "Name": "MyMaterial",
288 |                     "Density": "7900 kg/m^3",
289 |                     "YoungModulus": "210 GPa",
290 |                     "PoissonRatio": 0.3
291 |                 }
292 |             }
293 |         }
294 |         ```
295 | 
296 |         If you want to create a FEM mesh, you can use the following data.
297 |         The `Part` property is required.
298 |         ```json
299 |         {
300 |             "doc_name": "MyFEMMesh",
301 |             "obj_name": "FemMesh",
302 |             "obj_type": "Fem::FemMeshGmsh",
303 |             "analysis_name": "MyFEMAnalysis",
304 |             "obj_properties": {
305 |                 "Part": "MyObject",
306 |                 "ElementSizeMax": 10,
307 |                 "ElementSizeMin": 0.1,
308 |                 "MeshAlgorithm": 2
309 |             }
310 |         }
311 |         ```
312 |     """
313 |     freecad = get_freecad_connection()
314 |     try:
315 |         obj_data = {"Name": obj_name, "Type": obj_type, "Properties": obj_properties or {}, "Analysis": analysis_name}
316 |         res = freecad.create_object(doc_name, obj_data)
317 |         screenshot = freecad.get_active_screenshot()
318 |         
319 |         if res["success"]:
320 |             response = [
321 |                 TextContent(type="text", text=f"Object '{res['object_name']}' created successfully"),
322 |             ]
323 |             return add_screenshot_if_available(response, screenshot)
324 |         else:
325 |             response = [
326 |                 TextContent(type="text", text=f"Failed to create object: {res['error']}"),
327 |             ]
328 |             return add_screenshot_if_available(response, screenshot)
329 |     except Exception as e:
330 |         logger.error(f"Failed to create object: {str(e)}")
331 |         return [
332 |             TextContent(type="text", text=f"Failed to create object: {str(e)}")
333 |         ]
334 | 
335 | 
336 | @mcp.tool()
337 | def edit_object(
338 |     ctx: Context, doc_name: str, obj_name: str, obj_properties: dict[str, Any]
339 | ) -> list[TextContent | ImageContent]:
340 |     """Edit an object in FreeCAD.
341 |     This tool is used when the `create_object` tool cannot handle the object creation.
342 | 
343 |     Args:
344 |         doc_name: The name of the document to edit the object in.
345 |         obj_name: The name of the object to edit.
346 |         obj_properties: The properties of the object to edit.
347 | 
348 |     Returns:
349 |         A message indicating the success or failure of the object editing and a screenshot of the object.
350 |     """
351 |     freecad = get_freecad_connection()
352 |     try:
353 |         res = freecad.edit_object(doc_name, obj_name, {"Properties": obj_properties})
354 |         screenshot = freecad.get_active_screenshot()
355 | 
356 |         if res["success"]:
357 |             response = [
358 |                 TextContent(type="text", text=f"Object '{res['object_name']}' edited successfully"),
359 |             ]
360 |             return add_screenshot_if_available(response, screenshot)
361 |         else:
362 |             response = [
363 |                 TextContent(type="text", text=f"Failed to edit object: {res['error']}"),
364 |             ]
365 |             return add_screenshot_if_available(response, screenshot)
366 |     except Exception as e:
367 |         logger.error(f"Failed to edit object: {str(e)}")
368 |         return [
369 |             TextContent(type="text", text=f"Failed to edit object: {str(e)}")
370 |         ]
371 | 
372 | 
373 | @mcp.tool()
374 | def delete_object(ctx: Context, doc_name: str, obj_name: str) -> list[TextContent | ImageContent]:
375 |     """Delete an object in FreeCAD.
376 | 
377 |     Args:
378 |         doc_name: The name of the document to delete the object from.
379 |         obj_name: The name of the object to delete.
380 | 
381 |     Returns:
382 |         A message indicating the success or failure of the object deletion and a screenshot of the object.
383 |     """
384 |     freecad = get_freecad_connection()
385 |     try:
386 |         res = freecad.delete_object(doc_name, obj_name)
387 |         screenshot = freecad.get_active_screenshot()
388 |         
389 |         if res["success"]:
390 |             response = [
391 |                 TextContent(type="text", text=f"Object '{res['object_name']}' deleted successfully"),
392 |             ]
393 |             return add_screenshot_if_available(response, screenshot)
394 |         else:
395 |             response = [
396 |                 TextContent(type="text", text=f"Failed to delete object: {res['error']}"),
397 |             ]
398 |             return add_screenshot_if_available(response, screenshot)
399 |     except Exception as e:
400 |         logger.error(f"Failed to delete object: {str(e)}")
401 |         return [
402 |             TextContent(type="text", text=f"Failed to delete object: {str(e)}")
403 |         ]
404 | 
405 | 
406 | @mcp.tool()
407 | def execute_code(ctx: Context, code: str) -> list[TextContent | ImageContent]:
408 |     """Execute arbitrary Python code in FreeCAD.
409 | 
410 |     Args:
411 |         code: The Python code to execute.
412 | 
413 |     Returns:
414 |         A message indicating the success or failure of the code execution, the output of the code execution, and a screenshot of the object.
415 |     """
416 |     freecad = get_freecad_connection()
417 |     try:
418 |         res = freecad.execute_code(code)
419 |         screenshot = freecad.get_active_screenshot()
420 |         
421 |         if res["success"]:
422 |             response = [
423 |                 TextContent(type="text", text=f"Code executed successfully: {res['message']}"),
424 |             ]
425 |             return add_screenshot_if_available(response, screenshot)
426 |         else:
427 |             response = [
428 |                 TextContent(type="text", text=f"Failed to execute code: {res['error']}"),
429 |             ]
430 |             return add_screenshot_if_available(response, screenshot)
431 |     except Exception as e:
432 |         logger.error(f"Failed to execute code: {str(e)}")
433 |         return [
434 |             TextContent(type="text", text=f"Failed to execute code: {str(e)}")
435 |         ]
436 | 
437 | 
438 | @mcp.tool()
439 | def get_view(ctx: Context, view_name: Literal["Isometric", "Front", "Top", "Right", "Back", "Left", "Bottom", "Dimetric", "Trimetric"]) -> list[ImageContent | TextContent]:
440 |     """Get a screenshot of the active view.
441 | 
442 |     Args:
443 |         view_name: The name of the view to get the screenshot of.
444 |         The following views are available:
445 |         - "Isometric"
446 |         - "Front"
447 |         - "Top"
448 |         - "Right"
449 |         - "Back"
450 |         - "Left"
451 |         - "Bottom"
452 |         - "Dimetric"
453 |         - "Trimetric"
454 | 
455 |     Returns:
456 |         A screenshot of the active view.
457 |     """
458 |     freecad = get_freecad_connection()
459 |     screenshot = freecad.get_active_screenshot(view_name)
460 |     
461 |     if screenshot is not None:
462 |         return [ImageContent(type="image", data=screenshot, mimeType="image/png")]
463 |     else:
464 |         return [TextContent(type="text", text="Cannot get screenshot in the current view type (such as TechDraw or Spreadsheet)")]
465 | 
466 | 
467 | @mcp.tool()
468 | def insert_part_from_library(ctx: Context, relative_path: str) -> list[TextContent | ImageContent]:
469 |     """Insert a part from the parts library addon.
470 | 
471 |     Args:
472 |         relative_path: The relative path of the part to insert.
473 | 
474 |     Returns:
475 |         A message indicating the success or failure of the part insertion and a screenshot of the object.
476 |     """
477 |     freecad = get_freecad_connection()
478 |     try:
479 |         res = freecad.insert_part_from_library(relative_path)
480 |         screenshot = freecad.get_active_screenshot()
481 |         
482 |         if res["success"]:
483 |             response = [
484 |                 TextContent(type="text", text=f"Part inserted from library: {res['message']}"),
485 |             ]
486 |             return add_screenshot_if_available(response, screenshot)
487 |         else:
488 |             response = [
489 |                 TextContent(type="text", text=f"Failed to insert part from library: {res['error']}"),
490 |             ]
491 |             return add_screenshot_if_available(response, screenshot)
492 |     except Exception as e:
493 |         logger.error(f"Failed to insert part from library: {str(e)}")
494 |         return [
495 |             TextContent(type="text", text=f"Failed to insert part from library: {str(e)}")
496 |         ]
497 | 
498 | 
499 | @mcp.tool()
500 | def get_objects(ctx: Context, doc_name: str) -> list[dict[str, Any]]:
501 |     """Get all objects in a document.
502 |     You can use this tool to get the objects in a document to see what you can check or edit.
503 | 
504 |     Args:
505 |         doc_name: The name of the document to get the objects from.
506 | 
507 |     Returns:
508 |         A list of objects in the document and a screenshot of the document.
509 |     """
510 |     freecad = get_freecad_connection()
511 |     try:
512 |         screenshot = freecad.get_active_screenshot()
513 |         response = [
514 |             TextContent(type="text", text=json.dumps(freecad.get_objects(doc_name))),
515 |         ]
516 |         return add_screenshot_if_available(response, screenshot)
517 |     except Exception as e:
518 |         logger.error(f"Failed to get objects: {str(e)}")
519 |         return [
520 |             TextContent(type="text", text=f"Failed to get objects: {str(e)}")
521 |         ]
522 | 
523 | 
524 | @mcp.tool()
525 | def get_object(ctx: Context, doc_name: str, obj_name: str) -> dict[str, Any]:
526 |     """Get an object from a document.
527 |     You can use this tool to get the properties of an object to see what you can check or edit.
528 | 
529 |     Args:
530 |         doc_name: The name of the document to get the object from.
531 |         obj_name: The name of the object to get.
532 | 
533 |     Returns:
534 |         The object and a screenshot of the object.
535 |     """
536 |     freecad = get_freecad_connection()
537 |     try:
538 |         screenshot = freecad.get_active_screenshot()
539 |         response = [
540 |             TextContent(type="text", text=json.dumps(freecad.get_object(doc_name, obj_name))),
541 |         ]
542 |         return add_screenshot_if_available(response, screenshot)
543 |     except Exception as e:
544 |         logger.error(f"Failed to get object: {str(e)}")
545 |         return [
546 |             TextContent(type="text", text=f"Failed to get object: {str(e)}")
547 |         ]
548 | 
549 | 
550 | @mcp.tool()
551 | def get_parts_list(ctx: Context) -> list[str]:
552 |     """Get the list of parts in the parts library addon.
553 |     """
554 |     freecad = get_freecad_connection()
555 |     parts = freecad.get_parts_list()
556 |     if parts:
557 |         return [
558 |             TextContent(type="text", text=json.dumps(parts))
559 |         ]
560 |     else:
561 |         return [
562 |             TextContent(type="text", text=f"No parts found in the parts library. You must add parts_library addon.")
563 |         ]
564 | 
565 | 
566 | @mcp.prompt()
567 | def asset_creation_strategy() -> str:
568 |     return """
569 | Asset Creation Strategy for FreeCAD MCP
570 | 
571 | When creating content in FreeCAD, always follow these steps:
572 | 
573 | 0. Before starting any task, always use get_objects() to confirm the current state of the document.
574 | 
575 | 1. Utilize the parts library:
576 |    - Check available parts using get_parts_list().
577 |    - If the required part exists in the library, use insert_part_from_library() to insert it into your document.
578 | 
579 | 2. If the appropriate asset is not available in the parts library:
580 |    - Create basic shapes (e.g., cubes, cylinders, spheres) using create_object().
581 |    - Adjust and define detailed properties of the shapes as necessary using edit_object().
582 | 
583 | 3. Always assign clear and descriptive names to objects when adding them to the document.
584 | 
585 | 4. Explicitly set the position, scale, and rotation properties of created or inserted objects using edit_object() to ensure proper spatial relationships.
586 | 
587 | 5. After editing an object, always verify that the set properties have been correctly applied by using get_object().
588 | 
589 | 6. If detailed customization or specialized operations are necessary, use execute_code() to run custom Python scripts.
590 | 
591 | Only revert to basic creation methods in the following cases:
592 | - When the required asset is not available in the parts library.
593 | - When a basic shape is explicitly requested.
594 | - When creating complex shapes requires custom scripting.
595 | """
596 | 
597 | 
598 | def main():
599 |     """Run the MCP server"""
600 |     global _only_text_feedback
601 |     import argparse
602 |     parser = argparse.ArgumentParser()
603 |     parser.add_argument("--only-text-feedback", action="store_true", help="Only return text feedback")
604 |     args = parser.parse_args()
605 |     _only_text_feedback = args.only_text_feedback
606 |     logger.info(f"Only text feedback: {_only_text_feedback}")
607 |     mcp.run()
```