-
Notifications
You must be signed in to change notification settings - Fork 68
Creating Custom Tools
alf edited this page May 1, 2026
·
3 revisions
User extension tools let you register your own Python functions as tools that the LLM can call. They are also available to skills and the MCP server.
- Create a
.pyfile in<FreeCADAI dir>/tools/:
import math
def bolt_circle(diameter: float, count: int = 8, bolt_size: float = 6.5) -> str:
"""Create a bolt hole circle pattern on the XY plane."""
import Part
import FreeCAD as App
doc = App.ActiveDocument
if doc is None:
return "Error: no active document"
radius = diameter / 2
bolt_r = bolt_size / 2
shapes = []
for i in range(count):
angle = math.radians(i * 360.0 / count)
cx = radius * math.cos(angle)
cy = radius * math.sin(angle)
hole = Part.makeCylinder(bolt_r, 100, App.Vector(cx, cy, -50))
shapes.append(hole)
compound = shapes[0]
for s in shapes[1:]:
compound = compound.fuse(s)
obj = doc.addObject("Part::Feature", "BoltCircle")
obj.Shape = compound
doc.recompute()
return f"Created {count} x {bolt_size}mm bolt holes on {diameter}mm PCD"- Restart FreeCAD (or click Reload in Settings > User Tools).
- In Act mode, ask the LLM to use it: "create a bolt circle with 150mm diameter and 6 holes"
- The LLM calls
user_bolt_circlewith the extracted parameters.
User tools use plain Python conventions -- no decorators, no special imports:
def my_tool(param1: float, param2: int = 5, param3: str = "hello") -> str:
"""Short description of what this tool does."""
# Your code here
return "Success message"| Rule | Details |
|---|---|
| Name | Function name becomes the tool name, prefixed with user_ (e.g., bolt_circle → user_bolt_circle). |
| Description | First line of docstring. Used by the LLM to decide when to call the tool. |
| Parameters | Must have type hints. Supported types: float, int, str, bool. |
| Defaults | Parameters with default values become optional. Parameters without defaults are required. |
| Return | Return a str (success message) or a dict with output and/or data keys. Exceptions are caught and returned as errors. |
| Private functions | Functions starting with _ are skipped (use them as helpers). |
# Simple string return
def my_tool(x: float) -> str:
return f"Created object with size {x}"
# Dict return with structured data
def my_tool(x: float) -> dict:
return {
"output": "Created object", # shown to LLM
"data": {"volume": 123.4}, # structured data
}
# Exceptions become errors automatically
def my_tool(x: float) -> str:
if x <= 0:
raise ValueError("Size must be positive")
return "OK"| Python Type | JSON Schema Type | Example |
|---|---|---|
float |
number |
diameter: float |
int |
integer |
count: int = 8 |
str |
string |
name: str = "Part" |
bool |
boolean |
centered: bool = True |
Other types (e.g., list, dict, tuple) are not supported and will be skipped with a warning.
Both .py and .FCMacro files are supported. A single file can contain multiple tool functions.
<FreeCADAI dir>/tools/
bolt_circle.py # one tool function
my_shapes.py # multiple tool functions
legacy_macro.FCMacro # FreeCAD macro with typed functions
Files are validated using AST parsing (no execution) before being loaded:
- Syntax check -- file must be valid Python.
- Type hints required -- at least one public function with type-hinted parameters.
-
Supported types -- all parameter types must be
float,int,str, orbool.
Warnings (non-blocking):
- Missing docstring
- Unsupported parameter types (function still loads if it has other valid params)
Check validation results in Settings > User Tools. Each file shows its status with details on hover.
Open Settings > User Tools to:
-
Add... -- pick a
.pyor.FCMacrofile to copy into the tools directory. - Remove -- delete the selected file.
- Reload -- re-scan and re-validate all files (useful after editing a tool file externally).
- Scan FreeCAD macros -- also discover compatible functions from FreeCAD's built-in macro directory.
You can also manage files directly:
# Add a tool
cp my_tool.py <FreeCADAI dir>/tools/
# Remove a tool
rm <FreeCADAI dir>/tools/my_tool.py
# Disable without removing (in config.json)
"user_tools_disabled": ["my_tool.py"]- On startup (first Act-mode message), the tool registry scans
<FreeCADAI dir>/tools/. - Each
.py/.FCMacrofile is validated with AST (no execution). - Valid files are imported and functions are introspected.
- Each function becomes a
ToolDefinitionwith auser_prefix. - Tools are registered alongside built-in tools in the
ToolRegistry. - The LLM sees them in the tool schema and can call them like any other tool.
- Results are wrapped in undo transactions for safe rollback.
- Import FreeCAD modules inside the function, not at module level. The file is imported during registry setup, before FreeCAD is fully initialized.
-
Use
_helper()functions (underscore prefix) for shared logic -- they won't be registered as tools. - Keep descriptions clear and specific -- the LLM uses the docstring to decide when to call your tool.
- Test locally first -- run your function in FreeCAD's Python console before registering it as a tool.
-
One file per domain -- group related tools in the same file (e.g.,
fasteners.pywithbolt_circle,nut_hole,washer_recess).
| User Tools | Skills | |
|---|---|---|
| Invoked by | LLM (automatic) | User (/command) |
| Format | Python function with type hints | SKILL.md + optional handler.py |
| Parameters | Extracted from type hints | Parsed from user text |
| Best for | Specific operations the LLM should call | Multi-step workflows with instructions |
| Example | bolt_circle(diameter=150, count=6) |
/mast-flange BD=250 BT=6 HD=80 |