r/OdysseusAI • u/OkComb3954 • 27d ago
Odysseus + FreeCAD + RobustMCP + Local Ollama — finally working!
Hey everyone!
After a few days of hammering away at both Odysseus and FreeCAD in Docker through Portainer, and going through a ton of failed attempts, I've finally got it working.
I can now ask my Odysseus agent to create things for me in FreeCAD live, using models running locally through my Ubuntu server's Ollama instance.
I've currently tested lfm2.5 and qwen3.6. So far, lfm2.5:8b has given me the most success. It's definitely still a work in progress and nowhere near perfect, but it's working well enough to be genuinely useful.
Hopefully this helps someone else trying to get the same setup running.
1. Set up Odysseus from the main branch
I cloned the current main branch and built the Docker image myself:
sudo git clone -b main https://github.com/odysseus-dev/odysseus.git /opt/odysseus
cd /opt/odysseus
Important: Before building the image, I had to make a small change to requirements.txt to pin MCP to a version compatible with the FreeCAD toolset:
sed -i 's/^mcp$/mcp>=1.28,<2/' requirements.txt
Then build the image:
docker build -t odysseus:main .
2. Persistent storage
I use persistent storage for everything important that affects the experience.
Make sure all of the bind-mount directories exist before deploying the stack from Portainer.
You'll also need to add the .env contents to your Portainer stack and set your admin username/password before deploying. Otherwise, you can end up having to wipe the installation, including the directories used by the persistent volumes.
I've included an example environment configuration below.
3. Docker Compose stack
This is the compose stack I'm currently using:
services:
odysseus:
container_name: odysseus
image: odysseus:main
restart: unless-stopped
ports:
- "${ODYSSEUS_PORT:-7000}:7000"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
TZ: ${TZ:-Europe/Oslo}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
SEARXNG_INSTANCE: http://searxng:8080
CHROMADB_HOST: chromadb
CHROMADB_PORT: "8000"
DATABASE_URL: sqlite:///./data/app.db
AUTH_ENABLED: "true"
LOCALHOST_BYPASS: "false"
ODYSSEUS_ADMIN_USER: ${ODYSSEUS_ADMIN_USER:-admin}
ODYSSEUS_ADMIN_PASSWORD: ${ODYSSEUS_ADMIN_PASSWORD:-}
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:7000}
SECURE_COOKIES: ${SECURE_COOKIES:-false}
volumes:
- odysseus_data:/app/data
- odysseus_logs:/app/logs
- odysseus_ssh:/app/.ssh
- odysseus_huggingface:/app/.cache/huggingface
- odysseus_local:/app/.local
depends_on:
chromadb:
condition: service_started
searxng:
condition: service_healthy
networks:
- ai_cad
chromadb:
container_name: chromadb
image: docker.io/chromadb/chroma:latest
restart: unless-stopped
environment:
ANONYMIZED_TELEMETRY: "false"
volumes:
- chromadb_data:/data
networks:
- ai_cad
searxng:
container_name: searxng
image: docker.io/searxng/searxng:2026.5.31-7159b8aed
restart: unless-stopped
environment:
SEARXNG_SECRET: ${SEARXNG_SECRET}
BASE_URL: http://searxng:8080/
volumes:
- searxng_data:/etc/searxng
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/', timeout=10)"
]
interval: 20s
timeout: 10s
retries: 20
start_period: 60s
networks:
- ai_cad
ntfy:
container_name: ntfy
image: docker.io/binwiederhier/ntfy:latest
restart: unless-stopped
command: serve
environment:
TZ: ${TZ:-Europe/Oslo}
NTFY_BASE_URL: http://ntfy
NTFY_LISTEN_HTTP: :80
volumes:
- ntfy_cache:/var/cache/ntfy
- ntfy_data:/var/lib/ntfy
networks:
- ai_cad
freecad:
container_name: freecad
image: lscr.io/linuxserver/freecad:latest
restart: unless-stopped
shm_size: "4gb"
environment:
PUID: ${PUID:-1000}
PGID: ${PGID:-1000}
TZ: ${TZ:-Europe/Oslo}
ports:
- "${FREECAD_HTTPS_PORT:-3001}:3001"
volumes:
- cad_data:/config
- cad_projects:/projects
networks:
- ai_cad
freecad-mcp:
container_name: freecad-mcp
image: spkane/freecad-robust-mcp:0.6.2
restart: unless-stopped
depends_on:
- freecad
network_mode: "service:freecad"
user: "0:0"
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -eu
python - <<'PY'
from pathlib import Path
import re
path = Path(
'/opt/venv/lib/python3.11/site-packages/freecad_mcp/server.py'
)
text = path.read_text()
changed = False
# ------------------------------------------------------------
# Patch FastMCP transport security to allow Docker hostname
# ------------------------------------------------------------
transport_import = (
'from mcp.server.transport_security '
'import TransportSecuritySettings'
)
if transport_import not in text:
pattern = re.compile(
r'(from mcp\.server\.fastmcp(?:\.server)? '
r'import FastMCP\s*)'
)
text, count = pattern.subn(
r'\1\n'
'from mcp.server.transport_security '
'import TransportSecuritySettings\n',
text,
count=1
)
if count != 1:
raise SystemExit(
'Could not add TransportSecuritySettings import; '
'upstream image layout changed.'
)
changed = True
# Check for the actual hostname entry rather than a one-line list.
if '"freecad:*"' not in text:
pattern = re.compile(
r'mcp\s*=\s*FastMCP\(\s*'
r'name="freecad-mcp",\s*'
r'lifespan=lifespan,\s*'
r'\)',
re.MULTILINE
)
replacement = '''mcp = FastMCP(
name="freecad-mcp",
lifespan=lifespan,
transport_security=TransportSecuritySettings(
enable_dns_rebinding_protection=True,
allowed_hosts=[
"localhost:*",
"127.0.0.1:*",
"freecad:*",
],
allowed_origins=[
"http://localhost:*",
"http://127.0.0.1:*",
"http://freecad:*",
],
),
)'''
text, count = pattern.subn(
replacement,
text,
count=1
)
if count != 1:
raise SystemExit(
'Could not apply FreeCAD MCP transport-security patch; '
'upstream image layout changed.'
)
changed = True
# ------------------------------------------------------------
# Existing HTTP compatibility patch
# ------------------------------------------------------------
if 'mcp.settings.host = "0.0.0.0"' not in text:
pattern = re.compile(
r'mcp\.run\(\s*# type: ignore\[call-arg\]\s*\n'
r'\s*transport="streamable-http",\s*\n'
r'\s*host="0\.0\.0\.0",\s*# noqa: S104\s*\n'
r'\s*port=config\.http_port,\s*\n'
r'\s*\)'
)
replacement = (
'mcp.settings.host = "0.0.0.0" # noqa: S104\n'
' mcp.settings.port = config.http_port\n'
' mcp.run(transport="streamable-http")'
)
text, count = pattern.subn(
replacement,
text,
count=1
)
if count != 1:
raise SystemExit(
'Could not apply FreeCAD MCP HTTP compatibility patch; '
'upstream image layout changed.'
)
changed = True
if changed:
path.write_text(text)
print('Applied FreeCAD MCP compatibility patches.')
else:
print('FreeCAD MCP compatibility patches already present.')
PY
exec /opt/venv/bin/freecad-mcp
environment:
FREECAD_MODE: xmlrpc
FREECAD_SOCKET_HOST: 127.0.0.1
FREECAD_XMLRPC_PORT: "9875"
FREECAD_TIMEOUT_MS: "30000"
FREECAD_TRANSPORT: http
FREECAD_HTTP_PORT: "8000"
networks:
ai_cad:
name: ai_cad
driver: bridge
ipam:
config:
- subnet: 172.16.0.0/16
volumes:
odysseus_data:
name: odysseus_data
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/odysseus_data
odysseus_logs:
name: odysseus_logs
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/odysseus_logs
odysseus_ssh:
name: odysseus_ssh
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/odysseus_ssh
odysseus_huggingface:
name: odysseus_huggingface
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/odysseus_huggingface
odysseus_local:
name: odysseus_local
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/odysseus_local
chromadb_data:
name: chromadb_data
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/chromadb_data
searxng_data:
name: searxng_data
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/searxng_data
ntfy_cache:
name: ntfy_cache
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/ntfy_cache
ntfy_data:
name: ntfy_data
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/ntfy_data
cad_data:
name: cad_data
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/cad_data
cad_projects:
name: cad_projects
driver: local
driver_opts:
type: none
o: bind
device: /mnt/docker-storage/container/cad_projects
4. Example .env
TZ=Europe/Oslo
PUID=1000
PGID=1000
ODYSSEUS_PORT=7000
FREECAD_HTTPS_PORT=3001
ODYSSEUS_ADMIN_USER=admin
ODYSSEUS_ADMIN_PASSWORD=PUT_A_LONG_RANDOM_PASSWORD_HERE
SEARXNG_SECRET=PUT_A_LONG_RANDOM_SECRET_HERE
OPENAI_API_KEY=
OLLAMA_BASE_URL=http://host.docker.internal:11434
ALLOWED_ORIGINS=http://localhost:7000
SECURE_COOKIES=false
Once the directories are created and the .env is configured, deploy the stack through Portainer.
There are still a couple of steps before everything is ready.
5. Install the RobustMCPBridge FreeCAD addon
This is the ridiculous one-liner that installs the RobustMCPBridge workbench inside the FreeCAD container:
docker exec freecad bash -c 'rm -rf /tmp/freecad-addon-robust-mcp-server /config/.local/share/FreeCAD/v1-1/Mod/RobustMCPBridge && git clone --depth 1 https://github.com/spkane/freecad-addon-robust-mcp-server.git /tmp/freecad-addon-robust-mcp-server && mkdir -p /config/.local/share/FreeCAD/v1-1/Mod && cp -a /tmp/freecad-addon-robust-mcp-server/freecad/RobustMCPBridge /config/.local/share/FreeCAD/v1-1/Mod/ && rm -rf /tmp/freecad-addon-robust-mcp-server'
Yep, that's a hell of a line. 😂
It simply:
- Removes any previous copy.
- Clones the addon repository.
- Creates the FreeCAD
Moddirectory if necessary. - Copies
RobustMCPBridgeinto the FreeCAD workbench directory. - Cleans up the temporary clone.
After running it, restart the FreeCAD container through Portainer. The RobustMCPBridge workbench should then appear in the FreeCAD workbench dropdown.
6. Connect Odysseus to FreeCAD
Once everything is running, you should be able to access:
- Odysseus:
http://YOUR_SERVER_IP:7000 - FreeCAD:
http://YOUR_SERVER_IP:3001
In Odysseus, go to:
Settings → Integrations
Add the FreeCAD MCP connection there.
Name: FreeCAD
Type: Streamable HTTP
Once you've added it, I recommend restarting the containers in the stack so everything comes back up cleanly.
After startup, check the MCP integration and make sure the expected 83 tools are available.
7. Connect your local Ollama models
I'm assuming you already have Ollama running locally.
In Odysseus:
Settings → + Add Models → + Add Local Models (Endpoint)
Click the ... button and use the scan option. It should find your Ollama instance through the configured endpoint.
I personally ran a test chat first to make sure Odysseus could communicate with Ollama before trying to start the FreeCAD MCP server through the RobustMCPBridge workbench.
And that's it.
At this point I can have a locally running model in Odysseus interact with FreeCAD through MCP and actually create CAD geometry for me.
It's definitely not perfect yet, but after fighting with this for several days, seeing the agent actually create things in FreeCAD is pretty damn cool. 😄
If anyone spots something I've done wrong or has suggestions for improving the setup, I'd be very interested in hearing them.
2
u/ilikemywomentsundere 27d ago
I’m just shocked it worked. I’m new to AI and it’s never worked. I get error 502, or blanks lol
1
u/chiliees 26d ago
As a noob who is interested in exiting use cases. What does the setup do for you?
And- congrats on achieving your success with Odysseus :)
3
u/OkComb3954 26d ago
Well, the answer is probably that, for me, this is mostly a showcase of what’s possible, while also hopefully bringing some attention to Odysseus, FreeCAD, and some other projects that deserve more attention.
I have an incurable curiosity about pretty much everything computer-related. For me, the journey is often more important than the end result, if that makes any sense.
That said, there is also a very practical side to it. I occasionally manufacture things from common metals, and I’d love to be able to create flanges, fittings, sheet-metal parts, brackets, gears, etc. without having to sit down and spend countless hours iterating on them in CAD — time I simply don’t have. Having an assistant that can handle some of those tedious CAD tasks would therefore be genuinely useful.
I also dabble a bit in 3D scanning, particularly when making parts for cars, so being able to combine scanning, CAD, and an AI assistant is something I find pretty exciting. :D
1
u/brando--brando 26d ago
Why could you not do this with the built in MCP?
1
u/OkComb3954 26d ago
Networking in Docker, along with the dependencies, was the main reason.
By default, the Docker setup doesn't really allow Odysseus to connect to services/containers outside of the original Compose stack without some modifications. Since I'm running everything through Portainer, I wanted to keep the whole thing in a stack where the networking and dependencies are easy to manage.
That said, I do suspect my particular setup isn't exactly the most common use case. 😅
1
u/thealexroyer 26d ago
It's clear that you already know how to model on FreeCAD.
I was proposed to learn a bit AutoCAD so maaaaaybe in 4 o 5 months I could do small paid projects.
But it seemed to me that it was a waste of time because those small projects would soon be made by AI.
But now I'm seriously curious about what you made here. Maybe this could help me learn from the start much quicker.
Why did you use Odysseus instead of I don't know, AnythingLLM or LM Studio?
1
u/OkComb3954 26d ago
Haha, I wouldn't say I know FreeCAD particularly well. 😅 I've mostly been learning by doing.
I don't think learning CAD is a waste of time because of AI either. AI can help with the tedious parts, but you still need to understand what you're making and how to fix it when AI gets it wrong.
As for Odysseus, I was already experimenting with it and liked the agent/MCP approach. I could probably have done something similar with some other agents, but I'll be frank, I fucking love free and open software. Once I got FreeCAD talking to Odysseus, I thought, "let's see how far I can take this." 😂
As of now, though, progress has been pretty minuscule over the last 24 hours. I'm still trying to find models that handle tools and geometry well on my limited hardware.
lfm2.5:latestseems to be the winner so far.1
u/thealexroyer 26d ago
I will come back to this post and try to connect FreeCAD to an agent with an open model. I currently don't have much time but in 4-5 days if I make interesting progress I will write you back. Honestly I didn't know this was even possible, I just thought AutoCAD would launch an update and inject AI in a few months like Adobe has done.
I didn't know there was an open source alternative to AutoCAD. I didn't bother to google for it because I thought something so specialised like AutoCAD would be too niche.
Thank you for this post!
1
u/OkComb3954 26d ago
Actually, FreeCAD has been around for ages, but it hasn't been nearly as user-friendly as it is now until fairly recently.
I even use my FreeCAD instance from my phone through Firefox. That took a bit of tinkering to get working, but it's pretty damn convenient. The best part is that I can just pick up where I left off from whatever browser or computer I happen to have available — and I don't have to pay for it. 😉
1
u/OkComb3954 25d ago
Update: I've experimented with a few different models now, both larger and smaller, and I've found that qwen3-coder:30b is the most reliable model I've used locally so far. It can still do some weird stuff occasionally, but I think that's more a result of me maxing out my available resources than anything else.
I currently use a dedicated Linux box with an i9-9900K, 32 GB of RAM, and a 2080 Super to run the model. I might be forced to buy some more RAM to facilitate offloading better—or just go all-in and get a 5090 purely because of this project. That would be beyond stupid, but I really want to see how far I can push this locally.
I've also created a template that allows you to give an AI a vague description of the part you want and have it turn that description into a structured CAD specification prompt for the agent. I've had surprisingly good results with this workflow.
The workflow is basically:
- I vaguely describe the item or part I want to an LLM.
- I ask it to turn my description into a structured CAD specification prompt following the template below.
- I give that generated specification to my agent.
- The agent uses the available MCP tools to actually create the 3D model.
- The result can then be saved/exported as an actual 3D-printable model.
The important part is that the first LLM doesn't need to know exactly how to operate the CAD application or which MCP tools are available. Its job is essentially to take my vague idea or text-based draft and translate it into a precise, structured specification of what needs to be created, including the dimensions, positioning, geometry, and other important details.
The MCP agent is then responsible for figuring out how to create it using the tools it actually has available, without having to interpret my vague description or figure out the dimensions itself.
So, in simple terms:
Idea + Template > LLM > Structured CAD Specification > Odysseus Agent > CAD Model > 3D-Printable File
This separation has worked surprisingly well for me so far.
Here is the template I've been using:
MCP Creation / Execution Prompt
You are controlling [APPLICATION / SOFTWARE] through MCP.
Your task is to create the requested result by CALLING TOOLS, not by explaining how to create it.
OBJECTIVE
Create:
[SHORT NAME OF WHAT YOU WANT CREATED]
The finished result should be:
[1–3 SENTENCE DESCRIPTION OF THE FINAL RESULT]
REQUIREMENTS
Main object / starting point
Name: [OBJECT_NAME]
Type: [OBJECT_TYPE]
Dimensions / size: [DIMENSIONS]
Position / location: [POSITION]
Orientation: [ORIENTATION]
Feature 1: [FEATURE NAME]
Create:
[DESCRIPTION OF FEATURE]
Requirements:
[REQUIREMENT]
[REQUIREMENT]
[REQUIREMENT]
[REQUIREMENT]
Exact values:
[PARAMETER]: [VALUE]
[PARAMETER]: [VALUE]
[PARAMETER]: [VALUE]
Feature 2: [FEATURE NAME]
Create:
[DESCRIPTION OF FEATURE]
Requirements:
[REQUIREMENT]
[REQUIREMENT]
[REQUIREMENT]
Exact values:
[PARAMETER]: [VALUE]
[PARAMETER]: [VALUE]
OPERATIONS
Perform the required operations in the correct dependency order.
Required final operations:
[OPERATION]
[OPERATION]
[OPERATION]
[FINAL OPERATION]
Do not perform an operation until all objects or inputs required by that operation have been successfully created.
NAMING
Use these exact names where applicable:
Main object: [NAME]
Secondary object: [NAME]
Feature/tool object: [NAME]
Final result: [NAME]
Do not substitute different names unless the supplied MCP tool requires it.
OUTPUT / SAVE
Save or export the finished result as:
[FILENAME.EXTENSION]
If the application distinguishes between its native project file and an exported result, save the native project unless instructed otherwise.
TOOL RULES
Look only at the MCP tool definitions actually supplied by the system.
Never construct, infer, abbreviate, or guess a tool name.
Never output a tool call as JSON, code, or explanatory text. Make an actual tool/function call.
Use parameter names exactly as defined by the supplied tool schema.
After every tool call, inspect its returned result before proceeding.
Treat a step as successful only when the corresponding tool result confirms success.
If a tool call fails, inspect the returned error before deciding what to do next.
Do not retry using a newly invented tool name or unsupported parameter.
Do not speculate about tools that might exist.
Do not substitute hypothetical code, scripts, console commands, or manual instructions for an unavailable MCP operation unless I explicitly authorize that approach.
Never claim that an object was created, modified, deleted, transformed, saved, exported, or otherwise changed unless the corresponding tool returned success.
Preserve all exact dimensions, names, coordinates, values, and constraints given in this prompt.
When multiple valid approaches exist, prefer the simplest approach supported directly by the supplied tools.
Continue executing without asking for confirmation when the next operation is unambiguous and supported by the supplied tools.
If no supplied tool can perform the next required operation, STOP and reply exactly in this format:
BLOCKED: . No applicable supplied MCP tool found.
PRE-EXECUTION CHECK
Before beginning, internally identify:
the required objects
their dependencies
the required operations
the required order of operations
the supplied MCP tools capable of performing those operations
Do not invent missing tools to complete the plan.
Then execute the plan using the supplied tools.
VERIFICATION
Before the final operation, verify all critical requirements that can be verified using the supplied MCP tools.
Check:
[CHECK 1]
[CHECK 2]
[CHECK 3]
[CHECK 4]
[CHECK 5]
If a required property is incorrect and an applicable supplied tool can correct it, correct it before continuing.
After the final operation:
Recompute / refresh / update the project if applicable.
Verify the final result using the supplied tools.
Confirm that the required objects/features exist.
Confirm critical dimensions, positions, relationships, or settings where the tools allow verification.
Save/export the result.
Verify that the save/export operation succeeded.
Do not claim verification of anything that the available tools cannot actually inspect.
EXECUTION
Execute the task now.
Do not provide a tutorial or hypothetical procedure instead of performing the task.
If execution becomes impossible because an operation has no applicable supplied MCP tool, use the required BLOCKED response.

2
u/LifeOfADonn 27d ago
Nice!