-
Notifications
You must be signed in to change notification settings - Fork 68
Creating Custom Tools
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.
The fastest path is Settings > User Tools > New... — it prompts for a function name, writes a typed-example template that already passes validation, and opens it in your configured editor (FreeCAD's docked Python editor by default, or your OS-default editor if you've ticked "Use external editor" in Settings > Editor).
If you'd rather drop in an existing file, place a .py file directly in <FreeCADAI dir>/tools/ and click Reload, or use Settings > User Tools > Add... to copy a file in.
A typical hand-written tool:
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"After saving, click Reload in Settings > User Tools (no restart needed). In Act mode, ask the LLM to use it: "create a bolt circle with 150mm diameter and 6 holes" — it calls user_bolt_circle with 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:
- New... -- prompt for a function name, write a typed-example starter template, and open it in the configured editor.
-
Add... -- pick a
.pyor.FCMacrofile to copy into the tools directory. - Edit... -- open the selected file in the configured editor (see Settings > Editor to choose between FreeCAD's docked Python editor and your OS-default editor).
- 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 |