2 Commits
234 changed files with 24646 additions and 44066 deletions
-199
View File
@@ -1,199 +0,0 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
# CodeRabbit Configuration
# https://docs.coderabbit.ai/guides/configure-coderabbit
language: en-US
# Tone instructions for reviews
tone_instructions: |
Be concise and direct. Focus on actionable feedback.
Python project using modern tooling (uv, ruff, mypy).
Integrates with FreeCAD CAD software via MCP protocol.
early_access: false
# Enable or disable reviews
reviews:
# Enable automated code reviews
auto_review:
enabled: true
# Don't review drafts until ready
drafts: false
# Review base branches (PRs to main)
base_branches:
- main
- master
# Review profile - assertive catches more issues
profile: assertive
# Request changes when issues found
request_changes_workflow: true
# High-level summary in PR comments
high_level_summary: true
high_level_summary_placeholder: "@coderabbitai summary"
# Add poem to review (disabled - keep it professional)
poem: false
# Collapse walkthrough in large PRs
collapse_walkthrough: true
# Review labeling
labeling_instructions:
- label: "security"
instructions: "Apply when security vulnerabilities are found"
- label: "breaking-change"
instructions: "Apply when changes break backward compatibility"
- label: "documentation"
instructions: "Apply when documentation changes are needed"
# Paths to always review carefully
path_instructions:
- path: "src/freecad_mcp/**/*.py"
instructions: >
This is the core MCP server code. Pay attention to:
- Async/await patterns
- Error handling for FreeCAD operations
- Type hints and Pydantic models
- Security of execute_python functionality
- path: "src/freecad_mcp/tools/**/*.py"
instructions: >
These are MCP tool implementations. Ensure:
- Proper docstrings for tool discovery
- Consistent error handling patterns
- GUI-safe checks (FreeCAD.GuiUp) for view operations
- path: "addon/FreecadRobustMCPBridge/**/*.py"
instructions: >
This code runs inside FreeCAD's Python environment as a workbench addon.
It cannot import packages from the project's virtualenv (mcp, pydantic).
Watch for accidental imports of project dependencies.
- path: ".github/actions/**/*.yaml"
instructions: >
Custom GitHub Actions (composite actions). Check for:
- Proper input/output definitions
- Shell script correctness
- Cross-platform compatibility (x86_64/aarch64)
- path: "tests/**/*.py"
instructions: >
Test files. Ensure good test coverage and clear assertions.
Integration tests require FreeCAD Robust MCP Bridge to be running.
- path: ".github/workflows/**/*.yaml"
instructions: >
GitHub Actions workflows. Check for:
- Proper caching configuration
- Security of secrets handling
- Correct job dependencies
INTENTIONAL PATTERNS (do not flag as issues):
- continue-on-error: true on test steps is intentional during stabilization
- upload-artifact@v6 and download-artifact@v7 work on GitHub-hosted runners
- Actions version jumps (v4 to v6/v7) are intentional Dependabot updates
- path: "pyproject.toml"
instructions: >
Project configuration. Watch for:
- Dependency version constraints
- Tool configuration consistency (ruff, mypy, pytest)
CRITICAL: Python 3.11 is required - FreeCAD bundles libpython3.11.
Using a different Python version causes ABI incompatibility crashes.
INTENTIONAL PATTERN - Dependency version ranges:
- Dependencies use '>=' minimum version constraints (e.g., pydantic>=2.0)
- This is correct for Python libraries per PEP 440 best practices
- Exact versions are pinned in uv.lock for reproducible builds
- Do NOT suggest changing '>=' to '==' - this would break library usability
- The combination of '>=' in pyproject.toml + uv.lock is the modern standard
- path: "README.md"
instructions: >
Main documentation for the FreeCAD Robust MCP Server addon.
The PyPI package name is "freecad-robust-mcp".
- path: "Dockerfile"
instructions: >
Docker image for running FreeCAD MCP Server in containers.
Image name is "freecad-robust-mcp".
- path: ".mise.toml"
instructions: >
Tool version management via mise. All versions use fuzzy matching:
- "0.9" means "0.9.x" (allows patch updates)
- "1.43" means "1.43.x" (allows patch updates)
This is consistent and intentional. Do not flag as inconsistent pinning.
CRITICAL: Python must stay at 3.11 to match FreeCAD's bundled Python.
- path: "justfile"
instructions: >
Main task runner configuration using just (https://just.systems/).
Imports modules from the just/ directory.
- path: "just/*.just"
instructions: >
Just module files for task automation.
INTENTIONAL PATTERN - Heredoc indentation:
- Heredoc content inside recipes IS indented with 4 spaces (matching recipe body)
- This is CORRECT - just automatically strips leading indentation from heredocs
- Do NOT suggest adding more indentation to heredoc content
- Do NOT suggest removing indentation from heredoc content
- The 4-space indent prevents just from parsing embedded code as justfile syntax
- Example: Python code in heredocs uses 4-space indent, just outputs it unindented
INTENTIONAL PATTERN - Module paths:
- Modules use `project_root := justfile_directory()` to get project root
- Do NOT suggest using $(pwd) - it returns the wrong directory in modules
# Tools to use for analysis
tools:
# Python-specific tools
ruff:
enabled: true
# General tools
shellcheck:
enabled: true
markdownlint:
enabled: true
yamllint:
enabled: true
hadolint:
enabled: true
# Files and paths to ignore in reviews (! prefix = exclude)
path_filters:
# Lock files - auto-generated
- "!uv.lock"
- "!poetry.lock"
- "!package-lock.json"
# Generated/cached files
- "!**/.mypy_cache/**"
- "!**/.pytest_cache/**"
- "!**/.ruff_cache/**"
- "!**/__pycache__/**"
- "!**/*.pyc"
# Virtual environments
- "!.venv/**"
- "!venv/**"
# IDE settings (except shared configs)
- "!.idea/**"
# Build artifacts
- "!dist/**"
- "!build/**"
- "!*.egg-info/**"
# Documentation build output
- "!site/**"
# Secrets baseline (reviewed separately)
- "!.secrets.baseline"
# Chat configuration
chat:
auto_reply: true
# Knowledge base for context
knowledge_base:
opt_out: false
learnings:
scope: auto
issues:
scope: auto
jira:
project_keys: []
pull_requests:
scope: auto
-64
View File
@@ -1,64 +0,0 @@
# Technical terms that are not typos
justfile
sigsegv
mise
gitleaks
trufflehog
linting
linted
linter
linters
markdownlint
ruff
mypy
bandit
codespell
commitizen
hadolint
shellcheck
freecad
fcstd
mcp
impl
vertexes
Vertexes
recomputation
heredoc
heredocs
docstrings
virtualenv
isinstance
pylance
pyside
changelog
partdesign
pytest
# GitHub usernames and project names
spkane
neka
bonninr
contextform
blockchained
jango
# File extensions and paths
md
js
# X11/Display/CI terms
AppImage
Xvfb
xvfb-run
xcb
ConfigureNotify
Expose
MapNotify
openbox
xdotool
qmlscene
ppoll
eventfd
# Environment variables
QT_QPA_PLATFORM
-96
View File
@@ -1,96 +0,0 @@
# Git
.git
.gitignore
# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.env
.venv
env/
venv/
ENV/
# Testing
.tox
.nox
.coverage
.coverage.*
htmlcov/
.pytest_cache/
.hypothesis/
# Type checking
.mypy_cache/
.dmypy.json
.pytype/
.pyre/
# Linting
.ruff_cache/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Documentation
docs/
site/
*.md
!README.md
# Local configuration
.env.local
.env.*.local
.mcp.json
.claude/
.secrets.baseline
# FreeCAD files
*.FCStd
*.FCStd1
*.FCBak
# OS
.DS_Store
Thumbs.db
# Tests (not needed in runtime container)
tests/
# But allow CI test scripts for GUI testing container
!tests/ci-test/
# Development files
justfile
.pre-commit-config.yaml
.mise.toml
.gitleaks.toml
.markdownlint.yaml
.codespell-ignore-words.txt
mkdocs.yml
ARCHITECTURE-MCP.md
CLAUDE.md
**/RELEASE_NOTES.md
-5
View File
@@ -1,5 +0,0 @@
# These are supported funding model platforms
github: spkane
buy_me_a_coffee: spkane
thanks_dev: u/gh/spkane
-128
View File
@@ -1,128 +0,0 @@
name: Bug Report
description: Report a bug or unexpected behavior
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug! Please fill out this form as completely as possible.
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is.
placeholder: Describe the bug...
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Start FreeCAD with '...'
2. Run tool '....'
3. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What did you expect to happen?
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
description: What actually happened? Include any error messages.
validations:
required: true
- type: dropdown
id: connection-mode
attributes:
label: Connection Mode
description: Which connection mode are you using?
options:
- xmlrpc (recommended)
- socket
- embedded
- Not sure
validations:
required: true
- type: dropdown
id: freecad-mode
attributes:
label: FreeCAD Mode
description: How are you running FreeCAD?
options:
- GUI mode (FreeCAD with window)
- Headless mode (FreeCADCmd)
- Docker container
- Not sure
validations:
required: true
- type: input
id: freecad-version
attributes:
label: FreeCAD Version
description: FreeCAD version (e.g., 1.0.0, 0.21.2)
placeholder: "1.0.0"
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating System
options:
- macOS
- Linux (Ubuntu/Debian)
- Linux (Fedora/RHEL)
- Linux (Other)
- Windows
validations:
required: true
- type: input
id: python-version
attributes:
label: Python Version
description: Python version (e.g., 3.11.9)
placeholder: "3.11.9"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Log Output
description: |
Please copy and paste any relevant log output. This will be automatically formatted as code.
Include FreeCAD console output and/or MCP server logs if available.
render: shell
- type: textarea
id: additional
attributes:
label: Additional Context
description: Add any other context, screenshots, or files about the problem here.
- type: checkboxes
id: terms
attributes:
label: Checklist
options:
- label: I have searched the existing issues to make sure this is not a duplicate
required: true
- label: I have read the README and documentation
required: true
-5
View File
@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Documentation
url: https://github.com/spkane/freecad-robust-mcp-and-more#readme
about: Check the README for usage instructions and troubleshooting
-104
View File
@@ -1,104 +0,0 @@
name: Feature Request
description: Suggest a new feature or enhancement
title: "[Feature]: "
labels: ["enhancement", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting a feature! Please describe your idea clearly so we can evaluate it.
- type: textarea
id: problem
attributes:
label: Problem Statement
description: |
Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is.
placeholder: I'm frustrated when...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe the solution you'd like. Be as specific as possible.
placeholder: I would like to be able to...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Describe any alternative solutions or features you've considered.
- type: dropdown
id: category
attributes:
label: Feature Category
description: What area does this feature relate to?
options:
- New MCP Tool
- Existing Tool Enhancement
- FreeCAD Plugin
- Docker/Deployment
- Documentation
- Developer Experience
- Performance
- Other
validations:
required: true
- type: dropdown
id: impact
attributes:
label: Expected Impact
description: How significant would this feature be for your workflow?
options:
- Critical - Cannot use the project without it
- High - Would significantly improve my workflow
- Medium - Nice to have, would use regularly
- Low - Minor improvement
validations:
required: true
- type: textarea
id: use-case
attributes:
label: Use Case
description: |
Describe the use case for this feature. How would you use it?
Include any relevant context about your workflow.
placeholder: |
In my workflow, I need to...
This feature would help by...
validations:
required: true
- type: textarea
id: implementation
attributes:
label: Implementation Ideas
description: |
If you have ideas about how this could be implemented, share them here.
This is optional but can help speed up development.
- type: textarea
id: additional
attributes:
label: Additional Context
description: Add any other context, mockups, or examples about the feature request here.
- type: checkboxes
id: terms
attributes:
label: Checklist
options:
- label: I have searched the existing issues to make sure this is not a duplicate
required: true
- label: I am willing to help test this feature if implemented
required: false
- label: I am interested in contributing to implement this feature
required: false
-215
View File
@@ -1,215 +0,0 @@
name: Setup FreeCAD
description: Downloads and sets up FreeCAD AppImage for CI testing (supports x86_64 and ARM64 runners)
inputs:
freecad-tag:
description: "Specific FreeCAD release tag to install (e.g., '1.0.2'). If empty, uses latest stable release."
required: false
default: ""
outputs:
freecad-tag:
description: The FreeCAD release tag that was installed
value: ${{ steps.freecad-release.outputs.tag }}
runs:
using: composite
steps:
- name: Get latest FreeCAD release info
id: freecad-release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
INPUT_FREECAD_TAG: ${{ inputs.freecad-tag }}
run: |
# Detect runner architecture
RUNNER_ARCH=$(uname -m)
case "$RUNNER_ARCH" in
x86_64)
ARCH_SUFFIX="x86_64"
;;
aarch64|arm64)
ARCH_SUFFIX="aarch64"
;;
*)
echo "ERROR: Unsupported architecture: $RUNNER_ARCH"
exit 1
;;
esac
echo "Detected architecture: $RUNNER_ARCH -> $ARCH_SUFFIX"
echo "arch=$ARCH_SUFFIX" >> "$GITHUB_OUTPUT"
# Get FreeCAD release info from GitHub API
# Use GitHub token to avoid rate limiting
if [ -n "$INPUT_FREECAD_TAG" ]; then
echo "Using specified FreeCAD tag: $INPUT_FREECAD_TAG"
RELEASE_INFO=$(curl -s -H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/FreeCAD/FreeCAD/releases/tags/$INPUT_FREECAD_TAG")
else
echo "Fetching latest FreeCAD release..."
RELEASE_INFO=$(curl -s -H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/FreeCAD/FreeCAD/releases/latest)
fi
# Validate response has required fields
TAG_NAME=$(echo "$RELEASE_INFO" | jq -r '.tag_name')
if [ -z "$TAG_NAME" ] || [ "$TAG_NAME" = "null" ]; then
echo "ERROR: Could not get tag_name from release info"
echo "Response: $RELEASE_INFO"
exit 1
fi
echo "FreeCAD release: $TAG_NAME"
echo "tag=$TAG_NAME" >> "$GITHUB_OUTPUT"
# Find the Linux AppImage asset URL for the detected architecture
# Priority: conda build with py311 > any conda build > any AppImage
# The conda builds are the officially recommended AppImages
APPIMAGE_URL=""
APPIMAGE_NAME=""
# First try: conda build with py311 (most specific, preferred)
APPIMAGE_URL=$(echo "$RELEASE_INFO" | jq -r ".assets[] | select(.name | test(\"conda-Linux-${ARCH_SUFFIX}-py311\\\\.AppImage$\")) | .browser_download_url" | head -1)
APPIMAGE_NAME=$(echo "$RELEASE_INFO" | jq -r ".assets[] | select(.name | test(\"conda-Linux-${ARCH_SUFFIX}-py311\\\\.AppImage$\")) | .name" | head -1)
# Second try: any conda build for the architecture
if [ -z "$APPIMAGE_URL" ] || [ "$APPIMAGE_URL" = "null" ]; then
echo "Note: py311 conda build not found, trying any conda build..."
APPIMAGE_URL=$(echo "$RELEASE_INFO" | jq -r ".assets[] | select(.name | test(\"conda-Linux-${ARCH_SUFFIX}.*\\\\.AppImage$\")) | .browser_download_url" | head -1)
APPIMAGE_NAME=$(echo "$RELEASE_INFO" | jq -r ".assets[] | select(.name | test(\"conda-Linux-${ARCH_SUFFIX}.*\\\\.AppImage$\")) | .name" | head -1)
fi
# Third try: any AppImage for the architecture (fallback)
if [ -z "$APPIMAGE_URL" ] || [ "$APPIMAGE_URL" = "null" ]; then
echo "Note: conda build not found, trying any ${ARCH_SUFFIX} AppImage..."
APPIMAGE_URL=$(echo "$RELEASE_INFO" | jq -r ".assets[] | select(.name | test(\"Linux-${ARCH_SUFFIX}.*\\\\.AppImage$\")) | .browser_download_url" | head -1)
APPIMAGE_NAME=$(echo "$RELEASE_INFO" | jq -r ".assets[] | select(.name | test(\"Linux-${ARCH_SUFFIX}.*\\\\.AppImage$\")) | .name" | head -1)
fi
if [ -z "$APPIMAGE_URL" ] || [ "$APPIMAGE_URL" = "null" ]; then
echo "ERROR: Could not find Linux ${ARCH_SUFFIX} AppImage in release assets"
echo "Available assets:"
echo "$RELEASE_INFO" | jq -r '.assets[].name'
exit 1
fi
echo "AppImage URL: $APPIMAGE_URL"
echo "AppImage name: $APPIMAGE_NAME"
echo "url=$APPIMAGE_URL" >> "$GITHUB_OUTPUT"
echo "name=$APPIMAGE_NAME" >> "$GITHUB_OUTPUT"
- name: Cache FreeCAD AppImage
id: cache-freecad
uses: actions/cache@v5
with:
path: ~/freecad-appimage
key: ${{ runner.os }}-${{ steps.freecad-release.outputs.arch }}-freecad-appimage-${{ steps.freecad-release.outputs.tag }}
- name: Download FreeCAD AppImage
if: steps.cache-freecad.outputs.cache-hit != 'true'
shell: bash
run: |
mkdir -p ~/freecad-appimage
echo "Downloading FreeCAD ${{ steps.freecad-release.outputs.tag }}..."
# Use retry, timeout, and fail-fast flags for reliable downloads
curl -L --retry 3 --retry-delay 5 --connect-timeout 30 --max-time 600 \
-f -o ~/freecad-appimage/FreeCAD.AppImage \
"${{ steps.freecad-release.outputs.url }}"
chmod +x ~/freecad-appimage/FreeCAD.AppImage
- name: Setup FreeCAD AppImage
shell: bash
run: |
# Make AppImage executable (in case restored from cache)
chmod +x ~/freecad-appimage/FreeCAD.AppImage
# Extract AppImage for headless use (AppImages need FUSE which isn't available in CI)
# Skip extraction if already extracted (from cache)
cd ~/freecad-appimage
if [ ! -d "squashfs-root" ]; then
echo "Extracting AppImage..."
if ! ./FreeCAD.AppImage --appimage-extract > /dev/null 2>&1; then
echo "ERROR: AppImage extraction failed"
exit 1
fi
# Verify extraction produced expected structure
if [ ! -d "squashfs-root/usr/bin" ]; then
echo "ERROR: Extracted AppImage missing expected structure"
exit 1
fi
else
echo "Using cached extracted AppImage"
fi
# Create wrapper scripts that use AppRun to properly set up the environment
# The conda-based FreeCAD AppImage has complex environment requirements
# Using AppRun ensures all paths and variables are correctly configured
APPIMAGE_DIR="$HOME/freecad-appimage/squashfs-root"
# Check if AppRun exists and show its structure
echo "Checking AppImage structure..."
ls -la "$APPIMAGE_DIR/" | head -20
if [ -f "$APPIMAGE_DIR/AppRun" ]; then
echo "AppRun found, will use it for wrappers"
else
echo "WARNING: AppRun not found, falling back to direct execution"
fi
# Create freecadcmd wrapper - use AppRun with freecadcmd as argument
{
echo '#!/bin/bash'
echo "export APPDIR=\"$APPIMAGE_DIR\""
echo "export APPIMAGE_EXTRACT_AND_RUN=1"
echo "# Use AppRun if available, otherwise direct execution"
echo "if [ -f \"\$APPDIR/AppRun\" ]; then"
echo " exec \"\$APPDIR/AppRun\" freecadcmd \"\$@\""
echo "else"
echo " export LD_LIBRARY_PATH=\"\$APPDIR/usr/lib:\$LD_LIBRARY_PATH\""
echo " exec \"\$APPDIR/usr/bin/freecadcmd\" \"\$@\""
echo "fi"
} | sudo tee /usr/local/bin/freecadcmd > /dev/null
sudo chmod +x /usr/local/bin/freecadcmd
# Create freecad (GUI) wrapper - use AppRun with freecad as argument
{
echo '#!/bin/bash'
echo "export APPDIR=\"$APPIMAGE_DIR\""
echo "export APPIMAGE_EXTRACT_AND_RUN=1"
echo "# Use AppRun if available, otherwise direct execution"
echo "if [ -f \"\$APPDIR/AppRun\" ]; then"
echo " exec \"\$APPDIR/AppRun\" freecad \"\$@\""
echo "else"
echo " export LD_LIBRARY_PATH=\"\$APPDIR/usr/lib:\$LD_LIBRARY_PATH\""
echo " exec \"\$APPDIR/usr/bin/freecad\" \"\$@\""
echo "fi"
} | sudo tee /usr/local/bin/freecad > /dev/null
sudo chmod +x /usr/local/bin/freecad
echo "Created wrapper scripts for freecad and freecadcmd"
- name: Verify FreeCAD installation
shell: bash
run: |
echo "Checking FreeCAD installation..."
# Only verify with freecadcmd (headless) - freecad --version displays a GUI dialog
# and would hang without a window manager (Xvfb + openbox)
if freecadcmd --version; then
echo "FreeCAD version check passed"
else
echo "ERROR: freecadcmd --version failed"
echo ""
echo "=== Diagnostic Information ==="
echo "--- which freecadcmd ---"
which freecadcmd || echo "freecadcmd not found in PATH"
echo "--- which freecad ---"
which freecad || echo "freecad not found in PATH"
echo "--- FreeCAD AppImage bin directory ---"
ls -la ~/freecad-appimage/squashfs-root/usr/bin/ | head -20 || echo "Directory not found"
echo "--- PATH ---"
echo "$PATH"
echo "=== End Diagnostic Information ==="
exit 1
fi
which freecadcmd
which freecad
# Show Python version bundled with FreeCAD
freecadcmd -c "import sys; print(f'FreeCAD Python: {sys.version}')" || true
-60
View File
@@ -1,60 +0,0 @@
version: 2
updates:
# Python dependencies via pip/uv
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "python"
commit-message:
prefix: "deps(python)"
groups:
development:
patterns:
- "pytest*"
- "ruff"
- "mypy"
- "pre-commit"
- "bandit"
- "codespell"
update-types:
- "minor"
- "patch"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "deps(actions)"
# Docker dependencies
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 3
labels:
- "dependencies"
- "docker"
commit-message:
prefix: "deps(docker)"
ignore:
# CRITICAL: Python version must match FreeCAD's bundled Python (currently 3.11)
# Using a different Python version causes ABI incompatibility crashes.
# Only allow patch updates (e.g., 3.11-slim to 3.11.x-slim), not minor/major.
- dependency-name: "python"
update-types:
- "version-update:semver-major"
- "version-update:semver-minor"
-50
View File
@@ -1,50 +0,0 @@
name: CodeQL Analysis
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
schedule:
# Run weekly on Sundays at midnight UTC
- cron: "0 0 * * 0"
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [python]
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# Use default queries plus security-extended
queries: security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
-170
View File
@@ -1,170 +0,0 @@
name: Docker Build
# Build and test Docker images on pushes to main and pull requests.
# Note: Release tags (robust-mcp-server-v*) trigger mcp-server-release.yaml
# which handles Docker Hub releases. This workflow is for CI testing only.
on:
push:
branches: [main, master]
paths:
- "Dockerfile"
- ".dockerignore"
- "src/**/*.py"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/docker.yaml"
pull_request:
branches: [main, master]
paths:
- "Dockerfile"
- ".dockerignore"
- "src/**/*.py"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/docker.yaml"
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: freecad-robust-mcp
jobs:
build:
name: Build Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-
- name: Get version for setuptools-scm
id: version
run: |
# Get version from git describe (matches setuptools-scm behavior)
# For tagged releases: v1.0.0 -> 1.0.0
# For dev builds: v1.0.0-5-g1234567 -> 1.0.0.dev5+g1234567
if git describe --tags --exact-match 2>/dev/null; then
TAG=$(git describe --tags --exact-match)
VERSION="${TAG#v}"
else
# Get the latest tag or use 0.0.0 if none exists
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
BASE_VERSION="${LATEST_TAG#v}"
# Count commits since tag
COMMITS=$(git rev-list "${LATEST_TAG}..HEAD" --count 2>/dev/null || echo "0")
SHORT_SHA=$(git rev-parse --short HEAD)
VERSION="${BASE_VERSION}.dev${COMMITS}+g${SHORT_SHA}"
fi
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
echo "Detected version: $VERSION"
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.version.outputs.VERSION }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Test Docker image
run: |
# Build for current platform only for testing (--no-cache ensures fresh build)
docker build --no-cache --build-arg VERSION=${{ steps.version.outputs.VERSION }} -t ${{ env.IMAGE_NAME }}:test .
# Test that the container starts and responds to MCP initialize
echo '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | \
timeout 30 docker run --rm -i \
-e FREECAD_MODE=xmlrpc \
${{ env.IMAGE_NAME }}:test 2>&1 | \
grep -q '"result"' && echo "Container test passed" || echo "Container test completed"
- name: Cache Trivy vulnerability database
uses: actions/cache@v4
with:
path: .trivy-cache
# Cache key based on OS and workflow file hash; refreshes when workflow changes
key: trivy-db-${{ runner.os }}-${{ hashFiles('.github/workflows/docker.yaml') }}
restore-keys: |
trivy-db-${{ runner.os }}-
- name: Scan for HIGH/CRITICAL vulnerabilities (fail build)
uses: aquasecurity/trivy-action@0.33.1
with:
image-ref: ${{ env.IMAGE_NAME }}:test
version: "v0.68.2"
severity: "HIGH,CRITICAL"
exit-code: "1"
format: "table"
cache-dir: .trivy-cache
trivyignores: ".trivyignore"
- name: Scan for MEDIUM/LOW vulnerabilities (warning only)
uses: aquasecurity/trivy-action@0.33.1
with:
image-ref: ${{ env.IMAGE_NAME }}:test
version: "v0.68.2"
severity: "MEDIUM,LOW"
exit-code: "0"
format: "table"
cache-dir: .trivy-cache
trivyignores: ".trivyignore"
continue-on-error: true
- name: Generate SARIF report for GitHub Security
if: github.event_name != 'pull_request'
uses: aquasecurity/trivy-action@0.33.1
with:
image-ref: ${{ env.IMAGE_NAME }}:test
version: "v0.68.2"
format: "sarif"
output: "trivy-results.sarif"
cache-dir: .trivy-cache
trivyignores: ".trivyignore"
continue-on-error: true
- name: Upload Trivy scan results
if: github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: "trivy-results.sarif"
continue-on-error: true
-125
View File
@@ -1,125 +0,0 @@
# Documentation deployment workflow
# Deploys MkDocs documentation to GitHub Pages with versioning via mike
#
# Deployment strategy:
# - Push to main: Deploys as "latest" (always the current main branch docs)
# - MCP server release tag: Deploys versioned docs (e.g., "1.0.0") as permanent snapshots
#
# The "latest" alias always points to the main branch documentation.
# Tagged releases get permanent versioned documentation (e.g., "1.0.0", "1.1.0").
# Users can switch between versions using the version selector in the docs.
name: Deploy Documentation
on:
push:
branches:
- main
paths:
- "docs/**"
- "mkdocs.yaml"
- "src/**"
- ".github/workflows/docs.yaml"
tags:
# Deploy versioned docs when MCP server is released
- "robust-mcp-server-v*"
workflow_dispatch:
inputs:
version:
description: "Version to deploy (e.g., 1.0.0, dev)"
required: false
default: "dev"
set_latest:
description: "Set this version as 'latest' alias"
required: false
default: false
type: boolean
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: write
pages: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0 # Full history needed for git-revision-date plugin
- name: Configure Git for mike
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install dependencies
run: uv sync --all-extras --frozen
- name: Determine version to deploy
id: version
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
# Manual dispatch - use provided version
VERSION="${{ github.event.inputs.version }}"
SET_LATEST="${{ github.event.inputs.set_latest }}"
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
# Tag push - extract version from tag
# robust-mcp-server-v1.0.0 -> 1.0.0
TAG="${{ github.ref_name }}"
VERSION="${TAG#robust-mcp-server-v}"
# Tagged releases never set latest (main branch is always latest)
SET_LATEST="false"
else
# Push to main - deploy as "latest"
VERSION="latest"
SET_LATEST="true"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "set_latest=${SET_LATEST}" >> "$GITHUB_OUTPUT"
echo "Deploying version: ${VERSION} (set_latest: ${SET_LATEST})"
- name: Deploy latest documentation (main branch)
if: steps.version.outputs.version == 'latest'
run: |
# Deploy main branch docs as "latest" and set as default
# Run deploy without --push, then set-default with --push for single git push
uv run mike deploy --update-aliases latest
uv run mike set-default --push latest
- name: Deploy versioned documentation (tagged release)
if: steps.version.outputs.version != 'latest'
run: |
VERSION="${{ steps.version.outputs.version }}"
SET_LATEST="${{ steps.version.outputs.set_latest }}"
if [[ "$SET_LATEST" == "true" ]]; then
# Manual dispatch requested setting as latest
# Run deploy without --push, then set-default with --push for single git push
uv run mike deploy --update-aliases "$VERSION" latest
uv run mike set-default --push latest
else
# Deploy version only (tagged releases don't update latest)
uv run mike deploy --push "$VERSION"
fi
- name: List deployed versions
run: uv run mike list
-437
View File
@@ -1,437 +0,0 @@
name: Robust MCP Server Release
# Trigger on tag push matching the component-specific pattern
on:
push:
tags:
- 'robust-mcp-server-v*'
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same tag
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DOCKERHUB_REPO: spkane/freecad-robust-mcp
jobs:
validate-tag:
name: Validate Tag Format
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- name: Validate semantic version tag
id: version
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Extract version from tag (robust-mcp-server-v1.2.3 -> 1.2.3)
if [[ ! "$TAG" =~ ^robust-mcp-server-v([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?)$ ]]; then
echo "ERROR: Tag '$TAG' does not match expected format (robust-mcp-server-vX.Y.Z or robust-mcp-server-vX.Y.Z-prerelease)"
exit 1
fi
VERSION="${BASH_REMATCH[1]}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# Check if this is a prerelease
if [[ "$VERSION" =~ -[a-zA-Z0-9.]+ ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
IS_PRERELEASE="true"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
IS_PRERELEASE="false"
fi
echo "Parsed version: $VERSION (prerelease: $IS_PRERELEASE)"
build:
name: Build Distribution
needs: validate-tag
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
- name: Set up Python
run: uv python install 3.11
- name: Cache uv dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/uv
.venv
key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml', 'uv.lock') }}
restore-keys: |
${{ runner.os }}-uv-
- name: Install build dependencies
run: uv sync --all-extras
- name: Build package
env:
# Override setuptools-scm version for component-specific tag
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ needs.validate-tag.outputs.version }}
run: uv build
- name: Verify built package version
env:
TAG_VERSION: ${{ needs.validate-tag.outputs.version }}
run: |
# Extract version from built wheel filename
shopt -s nullglob
WHEEL_FILES=(dist/*.whl)
if [ ${#WHEEL_FILES[@]} -eq 0 ]; then
echo "ERROR: No wheel files found in dist/"
exit 1
fi
WHEEL_FILE="${WHEEL_FILES[0]}"
WHEEL_NAME=$(basename "$WHEEL_FILE")
# Extract version from wheel filename
WHEEL_VERSION=$(echo "$WHEEL_NAME" | sed -E 's/^[^-]+-([^-]+)-.*/\1/')
echo "Built wheel: $WHEEL_NAME"
echo "Wheel version: $WHEEL_VERSION"
echo "Expected tag version: $TAG_VERSION"
# Normalize the tag version for comparison (PEP 440)
NORMALIZED_TAG=$(echo "$TAG_VERSION" | sed -E '
s/-alpha\.([0-9]+)/a\1/
s/-alpha/a0/
s/-beta\.([0-9]+)/b\1/
s/-beta/b0/
s/-rc\.([0-9]+)/rc\1/
s/-rc/rc0/
')
echo "Normalized tag version: $NORMALIZED_TAG"
if [ "$WHEEL_VERSION" != "$NORMALIZED_TAG" ]; then
echo "ERROR: Built package version does not match git tag!"
echo " Wheel version: $WHEEL_VERSION"
echo " Expected (normalized): $NORMALIZED_TAG"
exit 1
fi
echo "Version verification passed!"
- name: Check package
run: uv run twine check dist/*
- name: List built artifacts
run: ls -la dist/
- name: Upload distribution artifacts
uses: actions/upload-artifact@v6
with:
name: python-package-distributions
path: dist/
test-install:
name: Test Installation
needs: [validate-tag, build]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.11"]
steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Install package from wheel
run: pip install dist/*.whl
- name: Verify installation
run: |
set -e
pip show freecad-robust-mcp
freecad-mcp --help
python -c "import freecad_mcp; print(f'freecad-robust-mcp version: {freecad_mcp.__version__ if hasattr(freecad_mcp, \"__version__\") else \"unknown\"}')"
publish-testpypi:
name: Publish to TestPyPI
needs: [validate-tag, build, test-install]
runs-on: ubuntu-latest
if: contains(needs.validate-tag.outputs.version, '-')
environment:
name: testpypi
url: https://test.pypi.org/p/freecad-robust-mcp
permissions:
id-token: write
steps:
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
publish-pypi:
name: Publish to PyPI
needs: [validate-tag, build, test-install]
runs-on: ubuntu-latest
if: ${{ !contains(needs.validate-tag.outputs.version, '-') }}
environment:
name: pypi
url: https://pypi.org/p/freecad-robust-mcp
permissions:
id-token: write
steps:
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
docker-release:
name: Build and Push Docker Image
needs: [validate-tag, build, test-install]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup FreeCAD for integration test
uses: ./.github/actions/setup-freecad
- name: Install uv for bridge startup
uses: astral-sh/setup-uv@v7
with:
version: "latest"
- name: Install project dependencies
run: |
uv python install 3.11
uv sync --all-extras
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKERHUB_REPO }}
tags: |
type=raw,value=${{ needs.validate-tag.outputs.version }}
type=raw,value=${{ needs.validate-tag.outputs.version }},suffix=-mcp-server
type=raw,value=latest,enable=${{ needs.validate-tag.outputs.is_prerelease == 'false' }}
labels: |
org.opencontainers.image.title=FreeCAD Robust MCP Server
org.opencontainers.image.description=Model Context Protocol server for FreeCAD integration
org.opencontainers.image.vendor=spkane
org.opencontainers.image.version=${{ needs.validate-tag.outputs.version }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Start FreeCAD headless with MCP bridge
run: |
# Start FreeCAD headless with MCP bridge in background
setsid freecadcmd addon/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py > /tmp/freecad_bridge.log 2>&1 &
FREECAD_PID=$!
echo "FREECAD_PID=$FREECAD_PID" >> "$GITHUB_ENV"
# Wait for bridge to be ready
echo "Waiting for FreeCAD MCP bridge to start..."
MAX_RETRIES=60
for i in $(seq 1 $MAX_RETRIES); do
if curl -s --connect-timeout 1 http://localhost:9875 > /dev/null 2>&1; then
# Try to ping the bridge
if uv run python -c "import xmlrpc.client; print(xmlrpc.client.ServerProxy('http://localhost:9875').ping())" 2>/dev/null | grep -q "pong"; then
echo "FreeCAD MCP bridge is ready!"
break
fi
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "ERROR: FreeCAD MCP bridge did not start"
cat /tmp/freecad_bridge.log || true
kill "$FREECAD_PID" 2>/dev/null || true
exit 1
fi
sleep 1
done
- name: Test pushed image with MCP communication
run: |
# Pull the image to verify it was pushed successfully
docker pull ${{ env.DOCKERHUB_REPO }}:${{ needs.validate-tag.outputs.version }}
# Test MCP communication with actual FreeCAD backend
# Use --network=host so container can reach localhost:9875 where FreeCAD bridge is running
echo '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | \
timeout 30 docker run --rm -i \
--network=host \
-e FREECAD_MODE=xmlrpc \
-e FREECAD_SOCKET_HOST=localhost \
${{ env.DOCKERHUB_REPO }}:${{ needs.validate-tag.outputs.version }} 2>&1 | \
grep -q '"result"'
echo "Container MCP communication test passed"
- name: Stop FreeCAD
if: always()
run: |
if [ -n "$FREECAD_PID" ]; then
kill -- "-$FREECAD_PID" 2>/dev/null || kill "$FREECAD_PID" 2>/dev/null || true
fi
create-github-release:
name: Create GitHub Release
needs: [validate-tag, build, test-install]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download distribution artifacts
uses: actions/download-artifact@v7
with:
name: python-package-distributions
path: dist/
- name: Extract release notes section
id: changelog
run: |
VERSION="${{ needs.validate-tag.outputs.version }}"
RELEASE_NOTES="src/freecad_mcp/RELEASE_NOTES.md"
# Extract section for this version from RELEASE_NOTES.md
# Format: ## Version X.Y.Z (date)
# Extract everything between this version header and the next ## Version header
CHANGELOG_CONTENT=$(awk -v version="$VERSION" '
BEGIN { found=0 }
/^## Version / {
if (found) exit
if (index($0, version) > 0) { found=1; next }
}
found { print }
' "$RELEASE_NOTES")
# Write to file for the release body (handles multiline)
echo "$CHANGELOG_CONTENT" > changelog_section.md
# Check if we got content
if [ -n "$CHANGELOG_CONTENT" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
echo "Extracted release notes for version $VERSION"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No release notes found for version $VERSION (will use auto-generated notes)"
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "Robust MCP Server v${{ needs.validate-tag.outputs.version }}"
tag_name: ${{ github.ref_name }}
prerelease: ${{ needs.validate-tag.outputs.is_prerelease == 'true' }}
generate_release_notes: true
body_path: ${{ steps.changelog.outputs.found == 'true' && 'changelog_section.md' || '' }}
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
generate-summary:
name: Generate Release Summary
needs: [validate-tag, publish-pypi, publish-testpypi, docker-release, create-github-release]
if: always()
runs-on: ubuntu-latest
steps:
- name: Generate release summary
env:
VERSION: ${{ needs.validate-tag.outputs.version }}
IS_PRERELEASE: ${{ needs.validate-tag.outputs.is_prerelease }}
run: |
{
echo "## Robust MCP Server Release Summary"
echo ""
echo "**Version:** $VERSION"
echo "**Prerelease:** $IS_PRERELEASE"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
if [[ "$VERSION" == *"-"* ]]; then
{
echo "### Install from TestPyPI"
echo ""
echo "\`\`\`bash"
echo "pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ freecad-robust-mcp"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
else
{
echo "### Install from PyPI"
echo ""
echo "\`\`\`bash"
echo "pip install freecad-robust-mcp"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
{
echo ""
echo "### Docker"
echo ""
echo "\`\`\`bash"
echo "docker pull spkane/freecad-robust-mcp:$VERSION"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
@@ -1,246 +0,0 @@
name: MCP Workbench Release
# Trigger on tag push matching the component-specific pattern
on:
push:
tags:
- 'robust-mcp-workbench-v*'
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same tag
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
validate-and-release:
name: Validate Tag and Create Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Validate semantic version tag
id: version
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# Extract version from tag (robust-mcp-workbench-v1.2.3 -> 1.2.3)
if [[ ! "$TAG" =~ ^robust-mcp-workbench-v([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?)$ ]]; then
echo "ERROR: Tag '$TAG' does not match expected format (robust-mcp-workbench-vX.Y.Z or robust-mcp-workbench-vX.Y.Z-prerelease)"
exit 1
fi
VERSION="${BASH_REMATCH[1]}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# Check if this is a prerelease
if [[ "$VERSION" =~ -[a-zA-Z0-9.]+ ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi
echo "Parsed version: $VERSION"
- name: Verify version in source files
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Verifying version in source files matches tag: $VERSION"
# Check __version__ in __init__.py
INIT_FILE="addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py"
INIT_VERSION=$(grep -o '__version__ = "[^"]*"' "$INIT_FILE" | cut -d'"' -f2)
if [ "$INIT_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $INIT_FILE"
echo " Expected: $VERSION"
echo " Found: $INIT_VERSION"
echo ""
echo "The version in source files must be updated before tagging."
echo "Run: just release::bump-workbench $VERSION"
exit 1
fi
echo "✓ $INIT_FILE: $INIT_VERSION"
# Check wiki-source.txt
WIKI_FILE="addon/FreecadRobustMCPBridge/wiki-source.txt"
if [ -f "$WIKI_FILE" ]; then
WIKI_VERSION=$(grep -o '|Version=[^|]*' "$WIKI_FILE" | cut -d= -f2 | tr -d '\n')
if [ "$WIKI_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in $WIKI_FILE"
echo " Expected: $VERSION"
echo " Found: $WIKI_VERSION"
echo ""
echo "Run: just release::bump-workbench $VERSION"
exit 1
fi
echo "✓ $WIKI_FILE: $WIKI_VERSION"
fi
# Check package.xml
PKG_VERSION=$(awk '/<workbench>/,/<\/workbench>/' package.xml | grep -o '<version>[^<]*</version>' | head -1 | sed 's/<[^>]*>//g')
if [ "$PKG_VERSION" != "$VERSION" ]; then
echo "ERROR: Version mismatch in package.xml (workbench section)"
echo " Expected: $VERSION"
echo " Found: $PKG_VERSION"
exit 1
fi
echo "✓ package.xml (workbench): $PKG_VERSION"
echo ""
echo "Version verification passed!"
- name: Create workbench archive
run: |
VERSION="${{ steps.version.outputs.version }}"
# Create staging directory
mkdir -p "staging/freecad-mcp-workbench-${VERSION}"
# Copy workbench files
cp -r addon/FreecadRobustMCPBridge/* "staging/freecad-mcp-workbench-${VERSION}/"
# Copy LICENSE
cp LICENSE "staging/freecad-mcp-workbench-${VERSION}/"
# Create README for the archive
cat > "staging/freecad-mcp-workbench-${VERSION}/README.md" << EOF
# Robust MCP Bridge Workbench
**Version:** ${VERSION}
## Installation
### Via FreeCAD Addon Manager (Recommended)
1. Open FreeCAD
2. Go to **Tools → Addon Manager**
3. Search for "Robust MCP Bridge"
4. Click **Install**
5. Restart FreeCAD
### Manual Installation
Copy the contents of this archive to your FreeCAD Mod directory:
- **macOS**: \`~/Library/Application Support/FreeCAD/Mod/FreecadRobustMCPBridge/\`
- **Linux**: \`~/.local/share/FreeCAD/Mod/FreecadRobustMCPBridge/\`
- **Windows**: \`%APPDATA%/FreeCAD/Mod/FreecadRobustMCPBridge/\`
## Usage
1. Switch to the **Robust MCP Bridge** workbench
2. Click **Start MCP Bridge** in the toolbar
3. Connect your MCP client (Claude Code, etc.)
## Documentation
Full documentation: https://github.com/spkane/freecad-robust-mcp-and-more
## License
MIT License - see LICENSE file
EOF
# Create tar.gz archive
cd staging
tar -czvf "freecad-mcp-workbench-${VERSION}.tar.gz" "freecad-mcp-workbench-${VERSION}"
# Create zip archive
zip -r "freecad-mcp-workbench-${VERSION}.zip" "freecad-mcp-workbench-${VERSION}"
mv "freecad-mcp-workbench-${VERSION}.tar.gz" ../
mv "freecad-mcp-workbench-${VERSION}.zip" ../
cd ..
echo "Created archives:"
ls -la "freecad-mcp-workbench-${VERSION}."*
- name: Extract release notes section
id: changelog
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_NOTES="addon/FreecadRobustMCPBridge/RELEASE_NOTES.md"
# Extract section for this version from RELEASE_NOTES.md
# Format: ## Version X.Y.Z (date)
# Use exact version match to avoid substring issues (e.g., 1.0.0 matching 1.0.0-alpha)
CHANGELOG_CONTENT=$(awk -v version="$VERSION" '
BEGIN { found=0 }
/^## Version / {
if (found) exit
# Extract version field (3rd word) and compare exactly
split($0, parts, " ")
if (parts[3] == version) { found=1; next }
}
found { print }
' "$RELEASE_NOTES" 2>/dev/null || echo "")
# Build release body with changelog content if available
cat > release_body.md << 'STATIC_EOF'
## Robust MCP Bridge Workbench v${{ steps.version.outputs.version }}
This release contains the Robust MCP Bridge workbench for FreeCAD.
### Installation
**Recommended:** Install via FreeCAD's Addon Manager (search for "Robust MCP Bridge").
**Manual:** Download and extract to your FreeCAD Mod directory.
### What's Included
- Robust MCP Bridge workbench for GUI and headless FreeCAD
- XML-RPC and JSON-RPC server support
- Toolbar commands for bridge control
See the [full documentation](https://github.com/spkane/freecad-robust-mcp-and-more) for usage instructions.
STATIC_EOF
if [ -n "$CHANGELOG_CONTENT" ]; then
{
echo ""
echo "### Changelog"
echo ""
echo "$CHANGELOG_CONTENT"
} >> release_body.md
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "Robust MCP Bridge Workbench v${{ steps.version.outputs.version }}"
tag_name: ${{ github.ref_name }}
prerelease: ${{ steps.version.outputs.is_prerelease == 'true' }}
generate_release_notes: true
body_path: release_body.md
files: |
freecad-mcp-workbench-${{ steps.version.outputs.version }}.tar.gz
freecad-mcp-workbench-${{ steps.version.outputs.version }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate summary
run: |
VERSION="${{ steps.version.outputs.version }}"
{
echo "## Robust MCP Bridge Workbench Release"
echo ""
echo "**Version:** ${VERSION}"
echo ""
echo "### Downloads"
echo ""
echo "- \`freecad-mcp-workbench-${VERSION}.tar.gz\`"
echo "- \`freecad-mcp-workbench-${VERSION}.zip\`"
echo ""
echo "### Installation"
echo ""
echo "Install via FreeCAD Addon Manager or extract to your Mod directory."
} >> "$GITHUB_STEP_SUMMARY"
-58
View File
@@ -1,58 +0,0 @@
name: Pre-commit Checks
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
pre-commit:
name: Run Pre-commit Hooks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install mise
uses: jdx/mise-action@v3
- name: Cache uv dependencies
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- name: Install dependencies
run: uv sync --all-extras
- name: Cache pre-commit hooks
uses: actions/cache@v5
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
pre-commit-${{ runner.os }}-
- name: Run pre-commit on all files
env:
# Skip hooks that don't work well in CI:
# - no-commit-to-branch: Always fails in CI (we're on main/master)
# - trufflehog: Has wasm/go-re2 panic bug in GitHub Actions environment
# - safety: Skipped if SAFETY_API_KEY secret is not configured
# Note: shellcheck, hadolint, trivy use mise-managed binaries which ARE
# installed by mise-action above, so they should work in CI.
SKIP: no-commit-to-branch,trufflehog${{ secrets.SAFETY_API_KEY == '' && ',safety' || '' }}
# Safety CLI API key for dependency vulnerability scanning
# Get your key at: https://safetycli.com/ (free account)
# Add as repository secret: Settings → Secrets → Actions → SAFETY_API_KEY
SAFETY_API_KEY: ${{ secrets.SAFETY_API_KEY }}
run: uv run pre-commit run --all-files --show-diff-on-failure
-55
View File
@@ -1,55 +0,0 @@
name: Tests
on:
push:
branches: [main, master]
paths:
- "src/**/*.py"
- "addon/**/*.py"
- "tests/**/*.py"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/test.yaml"
pull_request:
branches: [main, master]
paths:
- "src/**/*.py"
- "addon/**/*.py"
- "tests/**/*.py"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/test.yaml"
workflow_dispatch:
workflow_call:
# Cancel in-progress runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Python Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install mise
uses: jdx/mise-action@v3
- name: Cache uv dependencies
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- name: Install dependencies
run: uv sync --all-extras
- name: Run unit tests
run: uv run pytest tests/unit/ -v --tb=short
- name: Run type checking
run: uv run mypy src/
-124
View File
@@ -1,124 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
# Generated version file (hatch-vcs)
src/freecad_mcp/_version.py
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# IDE
.idea/
*.swp
*.swo
*~
# Ruff
.ruff_cache/
# Trivy (pre-commit and CI cache)
.pre-commit-trivy-cache/
.trivy-cache/
trivy-results.sarif
# UV
# Note: uv.lock is committed for reproducible CI builds
# OS
.DS_Store
Thumbs.db
# Local configuration
.env.local
.env.*.local
# FreeCAD files
*.FCStd
*.FCStd1
*.FCBak
freecad-headless.log
# Local configuration
.mcp.json
.claude
-125
View File
@@ -1,125 +0,0 @@
# Gitleaks Configuration
# https://github.com/gitleaks/gitleaks
#
# This configuration extends the default rules with project-specific settings.
title = "FreeCAD Robust MCP Gitleaks Configuration"
[extend]
# Extend the default gitleaks configuration
useDefault = true
# ============================================================================
# Custom Rules
# ============================================================================
[[rules]]
id = "freecad-api-key"
description = "FreeCAD or related API key"
regex = '''(?i)(freecad|fcstd|fc)[-_]?(api)?[-_]?(key|token|secret)[\s]*[=:]\s*['"]?([a-zA-Z0-9_\-]{16,})['"]?'''
keywords = ["freecad", "fcstd"]
[[rules]]
id = "generic-api-key-assignment"
description = "Generic API key assignment in code"
regex = '''(?i)(api[_-]?key|apikey|api[_-]?secret|api[_-]?token)[\s]*[=:]\s*['"]([a-zA-Z0-9_\-]{20,})['"]'''
keywords = ["api_key", "apikey", "api-key", "api_secret", "api_token"]
[[rules]]
id = "jwt-token"
description = "JSON Web Token"
regex = '''eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*'''
keywords = ["eyJ"]
[[rules]]
id = "base64-encoded-secret"
description = "Base64 encoded secret (high entropy)"
regex = '''(?i)(secret|password|token|key)[\s]*[=:]\s*['"]([A-Za-z0-9+/]{40,}={0,2})['"]'''
keywords = ["secret", "password", "token", "key"]
entropy = 4.0
[[rules]]
id = "connection-string"
description = "Database connection string"
regex = '''(?i)(mongodb|postgres|mysql|redis|amqp|mssql)://[^\s'"]+'''
keywords = ["mongodb://", "postgres://", "mysql://", "redis://", "amqp://", "mssql://"]
[[rules]]
id = "private-key-header"
description = "Private key file content"
regex = '''-----BEGIN (RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY( BLOCK)?-----'''
keywords = ["BEGIN", "PRIVATE KEY"]
[[rules]]
id = "oauth-token"
description = "OAuth access or refresh token"
regex = '''(?i)(oauth|access|refresh)[-_]?token[\s]*[=:]\s*['"]([a-zA-Z0-9_\-\.]{20,})['"]'''
keywords = ["oauth", "access_token", "refresh_token"]
# ============================================================================
# Allowlist - Paths, Commits, and Patterns to Ignore
# ============================================================================
[allowlist]
description = "Global allowlist"
# Paths to ignore
paths = [
'''\.gitleaks\.toml$''',
'''\.pre-commit-config\.yaml$''',
'''(^|/)tests?/''',
'''(^|/)test_.*\.py$''',
'''(^|/).*_test\.py$''',
'''(^|/)conftest\.py$''',
'''(^|/)fixtures/''',
'''(^|/)mocks?/''',
'''\.md$''', # Documentation files
'''go\.sum$''',
'''package-lock\.json$''',
'''yarn\.lock$''',
'''uv\.lock$''',
'''poetry\.lock$''',
]
# Regex patterns to ignore (for false positives)
regexes = [
# Example/placeholder values
'''(?i)(example|sample|placeholder|dummy|fake|test|mock)''',
# Documentation patterns
'''your[-_]?(api)?[-_]?(key|token|secret)[-_]?here''',
'''<.*?(key|token|secret|password).*?>''',
'''xxx+''',
'''CHANGE[-_]?ME''',
# Common false positives
'''(?i)public[-_]?key''', # Public keys are not secrets
'''sk-\.\.\.''', # Truncated keys in docs
]
# Specific strings to ignore
stopwords = [
"AKIAIOSFODNN7EXAMPLE", # AWS example key
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", # AWS example secret
"ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", # GitHub placeholder
]
# ============================================================================
# Rule-specific Allowlists
# ============================================================================
# Allow specific patterns for certain rules
[[rules]]
id = "generic-api-key"
[rules.allowlist]
regexes = [
'''(?i)example''',
'''(?i)placeholder''',
'''(?i)your[-_]key[-_]here''',
]
# ============================================================================
# Entropy Settings
# ============================================================================
# Minimum entropy threshold for entropy-based detection
# Higher values = fewer false positives but may miss some secrets
# Default is 3.5, we use 4.0 for fewer false positives
-222
View File
@@ -1,222 +0,0 @@
# Markdownlint Configuration
# https://github.com/DavidAnson/markdownlint
# Rule reference: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
# Default state for all rules
default: true
# ============================================================================
# Headings
# ============================================================================
# MD001 - Heading levels should only increment by one level at a time
MD001: true
# MD002 - First heading should be a top-level heading (deprecated, use MD041)
MD002: false
# MD003 - Heading style
MD003:
style: "atx" # Use # style headings (not === or ---)
# MD022 - Headings should be surrounded by blank lines
MD022:
lines_above: 1
lines_below: 1
# MD023 - Headings must start at the beginning of the line
MD023: true
# MD024 - Multiple headings with the same content
MD024:
siblings_only: true # Allow same heading in different sections
# MD025 - Multiple top-level headings in the same document
MD025:
front_matter_title: "^\\s*title\\s*[:=]"
# MD026 - Trailing punctuation in heading
MD026:
punctuation: ".,;:!。,;:!"
# MD041 - First line in a file should be a top-level heading
MD041:
front_matter_title: "^\\s*title\\s*[:=]"
level: 1
# ============================================================================
# Lists
# ============================================================================
# MD004 - Unordered list style
MD004:
style: "dash" # Use - for unordered lists
# MD005 - Inconsistent indentation for list items
MD005: true
# MD006 - Consider starting bulleted lists at the beginning of the line
MD006: true
# MD007 - Unordered list indentation
MD007:
indent: 2
start_indented: false
# MD029 - Ordered list item prefix
MD029:
style: "one_or_ordered" # Allow both 1. 1. 1. and 1. 2. 3.
# MD030 - Spaces after list markers
MD030:
ul_single: 1
ol_single: 1
ul_multi: 1
ol_multi: 1
# MD032 - Lists should be surrounded by blank lines
MD032: true
# ============================================================================
# Code Blocks
# ============================================================================
# MD014 - Dollar signs used before commands without showing output
MD014: true
# MD031 - Fenced code blocks should be surrounded by blank lines
MD031:
list_items: true
# MD038 - Spaces inside code span elements
MD038: true
# MD040 - Fenced code blocks should have a language specified
MD040:
allowed_languages: [] # Allow any language
language_only: false
# MD046 - Code block style
MD046:
style: "fenced" # Use ``` not indentation
# MD048 - Code fence style
MD048:
style: "backtick" # Use ``` not ~~~
# ============================================================================
# Line Length and Whitespace
# ============================================================================
# MD009 - Trailing spaces
MD009:
br_spaces: 2 # Allow 2 trailing spaces for line breaks
list_item_empty_lines: false
strict: false
# MD010 - Hard tabs
MD010:
code_blocks: true
ignore_code_languages: ["makefile", "make"]
spaces_per_tab: 4
# MD012 - Multiple consecutive blank lines
MD012:
maximum: 2
# MD013 - Line length
# Disabled: Let text wrap naturally, especially for documentation
MD013: false
# MD047 - Files should end with a single newline character
MD047: true
# ============================================================================
# Links and Images
# ============================================================================
# MD011 - Reversed link syntax
MD011: true
# MD034 - Bare URL used
MD034: true
# MD039 - Spaces inside link text
MD039: true
# MD042 - No empty links
MD042: true
# MD045 - Images should have alternate text (alt text)
MD045: true
# MD051 - Link fragments should be valid
MD051: true
# MD052 - Reference links and images should use a label that is defined
MD052: true
# MD053 - Link and image reference definitions should be needed
MD053:
ignored_definitions: ["//"]
# ============================================================================
# HTML and Raw Content
# ============================================================================
# MD033 - Inline HTML
# Disabled: Allow HTML for things like <details>, <summary>, badges, etc.
MD033: false
# ============================================================================
# Emphasis and Formatting
# ============================================================================
# MD035 - Horizontal rule style
MD035:
style: "---"
# MD036 - Emphasis used instead of a heading
MD036:
punctuation: ".,;:!?。,;:!?"
# MD037 - Spaces inside emphasis markers
MD037: true
# MD049 - Emphasis style
MD049:
style: "asterisk" # Use *text* not _text_
# MD050 - Strong style
MD050:
style: "asterisk" # Use **text** not __text__
# ============================================================================
# Block Quotes
# ============================================================================
# MD027 - Multiple spaces after blockquote symbol
MD027: true
# MD028 - Blank line inside blockquote
MD028: true
# ============================================================================
# Other
# ============================================================================
# MD018 - No space after hash on atx style heading
MD018: true
# MD019 - Multiple spaces after hash on atx style heading
MD019: true
# MD020 - No space inside hashes on closed atx style heading
MD020: true
# MD021 - Multiple spaces inside hashes on closed atx style heading
MD021: true
# MD044 - Proper names should have the correct capitalization
# Disabled: Causes false positives in GitHub URLs containing lowercase project names
MD044: false
-48
View File
@@ -1,48 +0,0 @@
# mise tool configuration
# https://mise.jdx.dev/
[tools]
# Python 3.11 is required for embedded FreeCAD mode - FreeCAD bundles libpython3.11
# Using a different Python version causes ABI incompatibility crashes
python = "3.11"
# Pin tool versions for reproducible CI builds
# Update these periodically with: mise upgrade
uv = "0.9" # uv package manager
just = "1.43" # task runner
pre-commit = "4.5" # pre-commit hooks
github-cli = "2.74" # GitHub CLI for PR/issue management
# Security and code quality tools
trivy = "0.68" # container vulnerability scanner
gitleaks = "8.30" # secrets scanner
actionlint = "1.7" # GitHub Actions linter
# Note: hadolint and shellcheck are managed by their pre-commit repos
# (hadolint-py and shellcheck-py auto-download binaries)
# Markdown tools
markdownlint-cli2 = "0.20" # markdown linter
[env]
# FreeCAD connection mode:
# "xmlrpc" - XML-RPC protocol (recommended, works on all platforms)
# "socket" - JSON-RPC socket protocol (alternative)
# "embedded" - In-process FreeCAD (Linux only, crashes on macOS/Windows)
FREECAD_MODE = "xmlrpc"
# Path to FreeCAD's lib directory (for embedded mode only)
# macOS: "/Applications/FreeCAD.app/Contents/Resources/lib"
# Linux: "/usr/lib/freecad/lib"
# Windows: "C:/Program Files/FreeCAD/lib"
# FREECAD_PATH = ""
# Connection settings (for xmlrpc and socket modes)
FREECAD_SOCKET_HOST = "localhost"
FREECAD_SOCKET_PORT = "9876"
FREECAD_XMLRPC_PORT = "9875"
# Execution limits
FREECAD_TIMEOUT_MS = "30000"
FREECAD_MAX_OUTPUT_SIZE = "1000000"
[settings]
experimental = true
-316
View File
@@ -1,316 +0,0 @@
# Pre-commit hooks configuration
# https://pre-commit.com/
default_language_version:
python: python3.11 # Must match FreeCAD's bundled Python version
repos:
# ==========================================================================
# General File Hygiene
# ==========================================================================
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: \.md$ # Allow trailing spaces in markdown for line breaks
- id: end-of-file-fixer
exclude: \.safety-project\.ini$ # Safety CLI manages its own formatting
- id: check-xml
- id: check-yaml
args: [--unsafe]
- id: check-toml
- id: check-json
exclude: ^\.vscode/.*\.json$ # VS Code uses JSONC (JSON with Comments)
- id: check-added-large-files
args: [--maxkb=1000]
- id: check-merge-conflict
- id: check-case-conflict
- id: check-symlinks
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: detect-private-key
- id: mixed-line-ending
args: [--fix=lf]
- id: no-commit-to-branch
args: [--branch, main, --branch, master]
- id: check-ast # Check Python syntax
types: [text]
files: \.(py|FCMacro)$
# JSON5/JSONC validation for VS Code config files (supports comments)
- repo: https://github.com/maresb/check-json5
rev: v1.0.1
hooks:
- id: check-json5
files: ^\.vscode/.*\.json$ # Only check VS Code JSONC files
# ==========================================================================
# Python - Linting and Formatting
# ==========================================================================
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.11
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, text]
files: \.(py|FCMacro)$
- id: ruff-format
types_or: [python, text]
files: \.(py|FCMacro)$
# ==========================================================================
# Python - Type Checking
# ==========================================================================
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
hooks:
- id: mypy
additional_dependencies:
- pydantic>=2.10.0
- pydantic-settings>=2.7.0
- mcp>=1.25.0
# Note: mypy doesn't natively support .FCMacro, so we skip those files
# FCMacro files are checked by ruff and bandit instead
args: [--config-file=pyproject.toml]
# ==========================================================================
# Python - Security Scanning
# ==========================================================================
- repo: https://github.com/PyCQA/bandit
rev: 1.9.2
hooks:
- id: bandit
args: [-c, pyproject.toml, -r, src, macros]
additional_dependencies: ["bandit[toml]"]
types: [text]
files: \.(py|FCMacro)$
# Safety - Dependency vulnerability scanning
# Checks installed packages against known security vulnerabilities
# Local: Requires free safetycli.com account. Run `uv run safety auth` first.
# CI: Uses SAFETY_API_KEY secret passed via environment variable.
# Config: .safety-policy.yml (excludes .venv, node_modules, etc.)
- repo: local
hooks:
- id: safety
name: safety (dependency vulnerabilities)
entry: uv run safety scan --policy-file .safety-policy.yml --detailed-output
language: system
pass_filenames: false
files: ^(pyproject\.toml|uv\.lock|\.safety-policy\.yml)$
# ==========================================================================
# Secrets Detection - Multi-Layer Approach
# ==========================================================================
# Layer 1: Gitleaks - Fast, comprehensive secrets scanner
# Scans git history and current files using regex patterns
# Config: .gitleaks.toml
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.0
hooks:
- id: gitleaks
name: gitleaks (secrets scanner)
args: [--config, .gitleaks.toml, --verbose]
# Layer 2: detect-secrets - Yelp's enterprise-grade secrets detector
# Uses baseline file to track known/approved secrets
# Config: .secrets.baseline
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
name: detect-secrets (baseline scan)
args:
- --baseline
- .secrets.baseline
- --exclude-files
- '\.secrets\.baseline$'
- --exclude-files
- '\.gitleaks\.toml$'
- --exclude-files
- 'uv\.lock$'
- --exclude-files
- 'poetry\.lock$'
- --exclude-files
- 'package-lock\.json$'
# Layer 3: TruffleHog - Deep secrets scanner with verification
# Verifies secrets are actually valid (e.g., tests AWS keys)
# Note: TruffleHog has wasm/go-re2 panic bugs in GitHub Actions.
# It's skipped in CI (via SKIP env var) but runs locally.
# See: https://github.com/trufflesecurity/trufflehog/issues/3321
- repo: https://github.com/trufflesecurity/trufflehog
rev: v3.92.4
hooks:
- id: trufflehog
name: trufflehog (verified secrets scan)
args:
- --no-update
exclude: '(^|/)uv\.lock$|\.secrets\.baseline$'
# ==========================================================================
# Markdown Linting
# ==========================================================================
# markdownlint-cli2 - Comprehensive markdown linter with auto-fix
# Config: .markdownlint.yaml
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.20.0
hooks:
- id: markdownlint-cli2
name: markdownlint (linter)
args: [--fix]
# Tertiary: md-toc - Table of contents generator
# Automatically updates TOC between <!--TOC--> markers
- repo: https://github.com/frnmst/md-toc
rev: 9.0.0
hooks:
- id: md-toc
name: md-toc (table of contents)
args: ["-p", "github", "-l", "6"] # GitHub parser, max 6 levels
files: ^(README|docs/development/architecture-detailed)\.md$
# ==========================================================================
# Spell Checking
# ==========================================================================
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
additional_dependencies:
- tomli
args:
- --ignore-words
- .codespell-ignore-words.txt
- --skip
- "*.lock,*.json,.secrets.baseline"
# ==========================================================================
# Configuration Validation
# ==========================================================================
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
hooks:
- id: validate-pyproject
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.36.0
hooks:
- id: check-github-workflows
name: validate GitHub workflows
- id: check-dependabot
name: validate Dependabot config
# ==========================================================================
# GitHub Actions Linting
# ==========================================================================
- repo: https://github.com/rhysd/actionlint
rev: v1.7.10
hooks:
- id: actionlint
name: actionlint (GitHub Actions linter)
# ==========================================================================
# Shell Script Linting
# ==========================================================================
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck
name: shellcheck (shell linter)
args: [--severity=warning]
# ==========================================================================
# Dockerfile Linting
# ==========================================================================
# hadolint-py: Python wrapper that auto-downloads hadolint binary
# No Docker or system installation required
- repo: https://github.com/AleksaC/hadolint-py
rev: v2.14.0
hooks:
- id: hadolint
# ==========================================================================
# Dockerfile Security Scanning (Misconfigurations)
# ==========================================================================
# Uses mise-managed trivy binary instead of pre-commit repo.
# This avoids case-conflicting git refs in pre-commit-trivy repo that break
# `pre-commit autoupdate` on case-insensitive filesystems (macOS).
# Version is managed in .mise.toml - update with `mise upgrade trivy`
- repo: local
hooks:
- id: trivy
name: trivy (Dockerfile misconfig)
entry: trivy
args:
- config
- --severity
- HIGH,CRITICAL
- --exit-code
- "1"
language: system
files: (Dockerfile|\.dockerfile)$
pass_filenames: true
# ==========================================================================
# Documentation Build Validation
# ==========================================================================
- repo: local
hooks:
- id: mkdocs-build
name: mkdocs (documentation build)
entry: uv run mkdocs build --strict
language: system
pass_filenames: false
files: ^(docs/|mkdocs\.yaml)
# ==========================================================================
# Commit Message Linting
# ==========================================================================
- repo: https://github.com/commitizen-tools/commitizen
rev: v4.11.1
hooks:
- id: commitizen
name: commitizen (commit format)
stages: [commit-msg]
# ==========================================================================
# AI Code Review (Local Only)
# ==========================================================================
# CodeRabbit CLI - AI-powered code review
# https://www.coderabbit.ai/cli
#
# SETUP REQUIRED:
# 1. Install: just coderabbit::install
# 2. Authenticate: just coderabbit::login
#
# USAGE:
# - Run manually: just coderabbit::review
# - Run via pre-commit: uv run pre-commit run coderabbit --all-files
#
# NOTE: This hook uses 'manual' stage so it doesn't run automatically.
# The CodeRabbit GitHub App already reviews PRs, so CLI is for local use.
# Rate limits: Free=1/hour, Lite=1/hour, Pro=5/hour
- repo: local
hooks:
- id: coderabbit
name: coderabbit (AI code review)
entry: coderabbit review --plain --type uncommitted
language: system
pass_filenames: false
stages: [manual]
verbose: true
# ==========================================================================
# CI Configuration
# ==========================================================================
ci:
autoupdate_schedule: monthly
autoupdate_commit_msg: "chore(deps): update pre-commit hooks"
skip:
- mypy # Needs dependencies installed
- trivy # Uses mise-managed binary (local repo)
- trufflehog # Can be slow in CI
- coderabbit # GitHub App handles PR reviews; CLI is for local use
-4
View File
@@ -1,4 +0,0 @@
# Empty config file for pytest-watch
# This exists to prevent pytest-watch from parsing pyproject.toml as INI
# (pytest-watch incorrectly uses configparser which can't handle TOML arrays)
[pytest-watch]
-19
View File
@@ -1,19 +0,0 @@
# Safety CLI 3.x Policy File
# https://docs.safetycli.com/safety-docs/administration/safety-policy-files
version: "3.0"
scanning-settings:
# Maximum directory depth to scan
max-depth: 6
# Exclude virtual environments and other non-project directories
exclude:
- ".venv"
- "venv"
- ".git"
- "node_modules"
- "__pycache__"
- "*.egg-info"
- "dist"
- "build"
- "site" # MkDocs build output
-5
View File
@@ -1,5 +0,0 @@
[project]
id = freecad-addon-robust-mcp-server
url = /codebases/freecad-addon-robust-mcp-server/findings
name = freecad-addon-robust-mcp-server
-131
View File
@@ -1,131 +0,0 @@
{
"version": "1.5.0",
"plugins_used": [
{
"name": "ArtifactoryDetector"
},
{
"name": "AWSKeyDetector"
},
{
"name": "AzureStorageKeyDetector"
},
{
"name": "Base64HighEntropyString",
"limit": 4.5
},
{
"name": "BasicAuthDetector"
},
{
"name": "CloudantDetector"
},
{
"name": "DiscordBotTokenDetector"
},
{
"name": "GitHubTokenDetector"
},
{
"name": "GitLabTokenDetector"
},
{
"name": "HexHighEntropyString",
"limit": 3.0
},
{
"name": "IbmCloudIamDetector"
},
{
"name": "IbmCosHmacDetector"
},
{
"name": "IPPublicDetector"
},
{
"name": "JwtTokenDetector"
},
{
"name": "KeywordDetector",
"keyword_exclude": ""
},
{
"name": "MailchimpDetector"
},
{
"name": "NpmDetector"
},
{
"name": "OpenAIDetector"
},
{
"name": "PrivateKeyDetector"
},
{
"name": "PypiTokenDetector"
},
{
"name": "SendGridDetector"
},
{
"name": "SlackDetector"
},
{
"name": "SoftlayerDetector"
},
{
"name": "SquareOAuthDetector"
},
{
"name": "StripeDetector"
},
{
"name": "TelegramBotTokenDetector"
},
{
"name": "TwilioKeyDetector"
}
],
"filters_used": [
{
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
},
{
"path": "detect_secrets.filters.common.is_baseline_file",
"filename": "/Users/spkane/dev/spkane/FreeCAD-Components/mine/freecad-addon-robust-mcp-server/.secrets.baseline"
},
{
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
"min_level": 2
},
{
"path": "detect_secrets.filters.heuristic.is_indirect_reference"
},
{
"path": "detect_secrets.filters.heuristic.is_likely_id_string"
},
{
"path": "detect_secrets.filters.heuristic.is_lock_file"
},
{
"path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
},
{
"path": "detect_secrets.filters.heuristic.is_potential_uuid"
},
{
"path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
},
{
"path": "detect_secrets.filters.heuristic.is_sequential_string"
},
{
"path": "detect_secrets.filters.heuristic.is_swagger_file"
},
{
"path": "detect_secrets.filters.heuristic.is_templated_secret"
}
],
"results": {},
"generated_at": "2026-01-12T21:58:08Z"
}
-44
View File
@@ -1,44 +0,0 @@
# Trivy Vulnerability Ignore File
# ================================
# This file tells trivy to skip specific CVEs during image scanning.
# Use this for vulnerabilities that:
# - Have no fix available yet (check Alpine security tracker)
# - Are false positives for our use case
# - Are accepted risks with documented justification
#
# Format: One CVE ID per line, optionally with comment
# Docs: https://aquasecurity.github.io/trivy/latest/docs/configuration/filtering/
#
# Before adding a CVE here:
# 1. Verify no fix is available: https://security.alpinelinux.org/
# 2. Document the reason and expected fix date if known
# 3. Set a reminder to revisit when fixes become available
#
# To check if fixes are now available, run:
# just docker::scan
#
# =============================================================================
# IGNORED VULNERABILITIES
# =============================================================================
# --- Alpine Base Image CVEs (No Fix Available) ---
# These are in the python:3.11-alpine base image's system packages.
# We run `apk upgrade --no-cache` in the Dockerfile to get the latest patches,
# but some CVEs may not have fixes yet.
# CVE-2026-22184 - zlib vulnerability (alpine/zlib 1.3.1-r2)
# Status: No fix available as of 2025-01-12
# Tracker: https://security.alpinelinux.org/
# CVE-2026-22184
# CVE-2025-60876 - busybox vulnerability (alpine/busybox 1.37.0-r30)
# Status: No fix available as of 2025-01-12
# Tracker: https://security.alpinelinux.org/
# CVE-2025-60876
# =============================================================================
# NOTES
# =============================================================================
# - Uncomment CVE lines above ONLY if you've verified no fix is available
# - Review this file monthly to remove CVEs that now have fixes
# - The `apk upgrade` in Dockerfile should auto-fix most CVEs when rebuilding
-36
View File
@@ -1,36 +0,0 @@
{
"recommendations": [
// Python development
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.mypy-type-checker",
"charliermarsh.ruff",
// Just (command runner)
"skellock.just",
// Docker
"ms-azuretools.vscode-docker",
"exiasr.hadolint",
// GitHub Actions
"GitHub.vscode-github-actions",
// Configuration files
"tamasfe.even-better-toml",
"redhat.vscode-yaml",
// Markdown
"DavidAnson.vscode-markdownlint",
// Shell scripts
"timonwong.shellcheck",
// Git
"eamodio.gitlens",
// Spell checking
"streetsidesoftware.code-spell-checker"
]
}
-114
View File
@@ -1,114 +0,0 @@
{
"cSpell.words": [
"asyncio",
"bandit",
"blockchained",
"bonninr",
"BREP",
"brep",
"changelog",
"codespell",
"commitizen",
"contextform",
"docstrings",
"dylib",
"exiasr",
"fcstd",
"freecad",
"FreeCAD",
"gitleaks",
"Gitleaks",
"hadolint",
"heredoc",
"heredocs",
"IGES",
"iges",
"impl",
"isinstance",
"jango",
"justfile",
"Justfile",
"libpython",
"lifespan",
"linted",
"linter",
"linters",
"linting",
"Makefile",
"markdownlint",
"Markdownlint",
"mcp",
"MCP",
"mise",
"mypy",
"neka",
"partdesign",
"PartDesign",
"pydantic",
"Pylance",
"pyside",
"PySide",
"pytest",
"recomputation",
"recompute",
"rpath",
"ruff",
"Ruff",
"shellcheck",
"SIGSEGV",
"sigsegv",
"spkane",
"subagent",
"topods",
"trufflehog",
"TruffleHog",
"uncommitted",
"uuidgen",
"vertexes",
"virtualenv",
"XMLRPC",
"xmlrpc"
],
"cSpell.ignorePaths": [
".git",
"*.lock",
"uv.lock",
"node_modules",
".venv",
"__pycache__",
"*.pyc",
".secrets.baseline",
".gitleaks.toml",
".codespell-ignore-words.txt"
],
"spellright.language": [],
"spellright.documentTypes": [],
"ltex.enabled": false,
"editor.suggest.showWords": true,
"cSpell.enableFiletypes": [
"python",
"markdown",
"yaml",
"toml",
"json",
"plaintext"
],
"cSpell.language": "en",
"cSpell.allowCompoundWords": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
},
"[markdown]": {
"editor.wordWrap": "on",
"editor.quickSuggestions": {
"comments": "off",
"strings": "off",
"other": "off"
}
}
}
-1864
View File
File diff suppressed because it is too large Load Diff
-116
View File
@@ -1,116 +0,0 @@
# syntax=docker/dockerfile:1.7
# FreeCAD Robust MCP Server Dockerfile
# Multi-stage build with BuildKit optimizations for multi-arch support
#
# Uses Alpine Linux for minimal image size and reduced CVE surface.
# Alpine has significantly fewer vulnerabilities than Debian-based images.
#
# Build:
# docker build -t freecad-mcp .
#
# Build multi-arch:
# docker buildx build --platform linux/amd64,linux/arm64 -t freecad-mcp .
#
# Run:
# docker run --rm -i freecad-mcp
# =============================================================================
# Stage 1: Builder - Install dependencies and build the package
# =============================================================================
FROM python:3.11-alpine AS builder
# Install build dependencies for compiling Python packages with native extensions
# hadolint ignore=DL3018
RUN apk add --no-cache \
build-base \
libffi-dev
# Set up working directory
WORKDIR /app
# Upgrade pip to fix CVE-2025-8869, then install uv for fast dependency management
# hadolint ignore=DL3013
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir --upgrade "pip>=25.3" && \
pip install --no-cache-dir --no-compile uv
# Copy only dependency files first for better layer caching
# Include uv.lock for reproducible builds with locked dependency versions
COPY pyproject.toml uv.lock README.md ./
COPY src/ ./src/
# Version for setuptools-scm when building without git (e.g., in Docker)
# This can be overridden at build time with --build-arg VERSION=x.y.z
ARG VERSION=0.0.0.dev0
ENV SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION}
# Create virtual environment and install dependencies using locked versions
# Using uv cache mount for faster rebuilds
# --frozen ensures uv.lock is used exactly without updates
RUN --mount=type=cache,target=/root/.cache/uv \
uv venv /opt/venv && \
UV_PROJECT_ENVIRONMENT=/opt/venv uv sync --frozen --no-dev --no-editable
# =============================================================================
# Stage 2: Runtime - Minimal image for running the server
# =============================================================================
FROM python:3.11-alpine AS runtime
# Labels for container metadata (OCI Image Spec)
# Note: version, revision, and created are set dynamically in CI/CD workflows
LABEL org.opencontainers.image.title="FreeCAD Robust MCP Server" \
org.opencontainers.image.description="Robust MCP Server for FreeCAD integration with AI assistants" \
org.opencontainers.image.url="https://github.com/spkane/freecad-robust-mcp-and-more" \
org.opencontainers.image.source="https://github.com/spkane/freecad-robust-mcp-and-more" \
org.opencontainers.image.documentation="https://github.com/spkane/freecad-robust-mcp-and-more#readme" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.vendor="Sean P. Kane" \
org.opencontainers.image.authors="Sean P. Kane <spkane@gmail.com>" \
org.opencontainers.image.base.name="python:3.11-alpine"
# Upgrade all Alpine packages to fix CVEs in base image (zlib, busybox, etc.)
# This ensures we get security patches even if the base image is slightly stale
# hadolint ignore=DL3018
RUN apk upgrade --no-cache
# Create non-root user for security (Alpine uses addgroup/adduser)
RUN addgroup -g 1000 mcpuser && \
adduser -u 1000 -G mcpuser -s /bin/sh -D mcpuser
# Remove pip, setuptools, and wheel from system Python to fix CVEs
# - The base image has pip with CVE-2025-8869
# - setuptools vendors jaraco.context 5.3.0 with GHSA-58pv-8j8x-9vj2
# Since we use a pre-built venv, we don't need these in system Python at runtime.
# This eliminates the vulnerabilities without affecting functionality.
# hadolint ignore=DL3013
RUN pip uninstall -y pip setuptools wheel 2>/dev/null || true && \
rm -rf /usr/local/lib/python3.11/site-packages/pip* \
/usr/local/lib/python3.11/site-packages/setuptools* \
/usr/local/lib/python3.11/site-packages/wheel* \
/usr/local/lib/python3.11/site-packages/pkg_resources*
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
# Set environment variables
ENV PATH="/opt/venv/bin:$PATH" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
# Default to xmlrpc mode (requires FreeCAD running externally)
FREECAD_MODE="xmlrpc" \
FREECAD_SOCKET_HOST="host.docker.internal" \
FREECAD_SOCKET_PORT="9876" \
FREECAD_XMLRPC_PORT="9875" \
FREECAD_TIMEOUT_MS="30000"
# Switch to non-root user
USER mcpuser
WORKDIR /home/mcpuser
# Health check - verify the server can start
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import freecad_mcp; print('ok')" || exit 1
# Default command - run the MCP server in stdio mode
ENTRYPOINT ["freecad-mcp"]
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-693
View File
@@ -1,693 +0,0 @@
# FreeCAD Robust MCP Server
[![CI Tests](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/test.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/test.yaml)
[![Docker Build](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/docker.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/docker.yaml)
[![Pre-commit](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/pre-commit.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/pre-commit.yaml)
[![CodeQL](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/codeql.yaml/badge.svg)](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/codeql.yaml)
[![PyPI Version](https://img.shields.io/pypi/v/freecad-robust-mcp)](https://pypi.org/project/freecad-robust-mcp/)
[![Python Versions](https://img.shields.io/pypi/pyversions/freecad-robust-mcp)](https://pypi.org/project/freecad-robust-mcp/)
[![Docker Image Version](https://img.shields.io/docker/v/spkane/freecad-robust-mcp?sort=semver&label=docker)](https://hub.docker.com/r/spkane/freecad-robust-mcp)
[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://spkane.github.io/freecad-addon-robust-mcp-server/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that enables integration between AI assistants (Claude, GPT, and other MCP-compatible tools) and [FreeCAD](https://www.freecadweb.org/), allowing AI-assisted development and debugging of 3D models, macros, and workbenches.
## Table of Contents
<!--TOC-->
- [FreeCAD Robust MCP Server](#freecad-robust-mcp-server)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Requirements](#requirements)
- [For Users](#for-users)
- [Quick Links](#quick-links)
- [Robust MCP Server](#robust-mcp-server)
- [Installation](#installation)
- [Using pip (recommended)](#using-pip-recommended)
- [Using mise and just (from source)](#using-mise-and-just-from-source)
- [Using Docker](#using-docker)
- [Configuration](#configuration)
- [Environment Variables](#environment-variables)
- [Connection Modes](#connection-modes)
- [MCP Client Configuration](#mcp-client-configuration)
- [Usage](#usage)
- [Starting the MCP Bridge in FreeCAD](#starting-the-mcp-bridge-in-freecad)
- [Option A: Using the Workbench (Recommended)](#option-a-using-the-workbench-recommended)
- [Option B: Using just commands (from source)](#option-b-using-just-commands-from-source)
- [Uninstalling the MCP Bridge](#uninstalling-the-mcp-bridge)
- [Checking for Legacy Components](#checking-for-legacy-components)
- [Manual Cleanup (if needed)](#manual-cleanup-if-needed)
- [Running Modes](#running-modes)
- [XML-RPC Mode (Recommended)](#xml-rpc-mode-recommended)
- [Socket Mode (JSON-RPC)](#socket-mode-json-rpc)
- [Headless Mode](#headless-mode)
- [Embedded Mode (Linux Only)](#embedded-mode-linux-only)
- [Available Tools](#available-tools)
- [Execution & Debugging (5 tools)](#execution--debugging-5-tools)
- [Document Management (7 tools)](#document-management-7-tools)
- [Object Creation - Primitives (8 tools)](#object-creation---primitives-8-tools)
- [Object Management (12 tools)](#object-management-12-tools)
- [PartDesign - Sketching (14 tools)](#partdesign---sketching-14-tools)
- [PartDesign - Patterns & Edges (5 tools)](#partdesign---patterns--edges-5-tools)
- [View & Display (11 tools)](#view--display-11-tools)
- [Undo/Redo (3 tools)](#undoredo-3-tools)
- [Export/Import (7 tools)](#exportimport-7-tools)
- [Macro Management (6 tools)](#macro-management-6-tools)
- [Parts Library (2 tools)](#parts-library-2-tools)
- [For Developers](#for-developers)
- [Robust MCP Server Development](#robust-mcp-server-development)
- [Prerequisites](#prerequisites)
- [Initial Setup](#initial-setup)
- [MCP Client Configuration (Development)](#mcp-client-configuration-development)
- [Development Workflow](#development-workflow)
- [Running FreeCAD with the MCP Bridge](#running-freecad-with-the-mcp-bridge)
- [GUI Mode (recommended for development)](#gui-mode-recommended-for-development)
- [Headless Mode (for automation/CI)](#headless-mode-for-automationci)
- [Running Tests](#running-tests)
- [Code Quality](#code-quality)
- [Architecture](#architecture)
- [Acknowledgements](#acknowledgements)
- [Related Projects](#related-projects)
- [License](#license)
<!--TOC-->
> The macros that were originally in this repo under the `/macros` directory have been permanently moved to two new GitHub repos:
>
> - [spkane/freecad-macro-cut-for-magnets](https://github.com/spkane/freecad-macro-cut-for-magnets)
> - [spkane/freecad-macro-3d-print-multi-export](https://github.com/spkane/freecad-macro-3d-print-multi-export)
## Features
- **82+ MCP Tools**: Comprehensive CAD operations including primitives, PartDesign, booleans, export
- **Multiple Connection Modes**: XML-RPC (recommended), JSON-RPC socket, or embedded
- **GUI & Headless Support**: Full modeling in headless mode, plus screenshots/colors in GUI mode
- **Macro Development**: Create, edit, run, and template FreeCAD macros via MCP
## Requirements
- [FreeCAD](https://www.freecadweb.org/) 0.21+ or 1.0+
- Python 3.11 (required for FreeCAD ABI compatibility)
---
## For Users
This section covers installation and usage for end users who want to use the Robust MCP Server with AI assistants.
### Quick Links
| Resource | Description |
| ------------------------------------------------------------------------------------- | --------------------------------------------- |
| [**Documentation**](https://spkane.github.io/freecad-addon-robust-mcp-server/) | Full documentation, guides, and API reference |
| [Docker Hub](https://hub.docker.com/r/spkane/freecad-robust-mcp) | Pre-built Docker images for easy deployment |
| [PyPI](https://pypi.org/project/freecad-robust-mcp/) | Python package for pip installation |
| [GitHub Releases](https://github.com/spkane/freecad-addon-robust-mcp-server/releases) | Release archives and changelogs |
## Robust MCP Server
> **Note**: The Linux container and PyPI package are both named `freecad-robust-mcp` which differs slightly from this git repository name.
### Installation
#### Using pip (recommended)
```bash
pip install freecad-robust-mcp
```
#### Using mise and just (from source)
```bash
git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
cd freecad-addon-robust-mcp-server
# Install mise via the Official mise installer script (if not already installed)
curl https://mise.run | sh
mise trust
mise install
just setup
```
#### Using Docker
Run the Robust MCP Server in a container. This is useful for isolated environments or when you don't want to install Python dependencies on your host.
```bash
# Pull from Docker Hub (when published)
docker pull spkane/freecad-robust-mcp
# Or build locally
git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
cd freecad-addon-robust-mcp-server
docker build -t freecad-robust-mcp .
# Or use just commands (if you have mise/just installed)
just docker::build # Build for local architecture
just docker::build-multi # Build multi-arch (amd64 + arm64)
```
**Note:** The containerized Robust MCP Server only supports `xmlrpc` and `socket` modes since FreeCAD runs on your host machine (not in the container). The container connects to FreeCAD via `host.docker.internal`.
### Configuration
#### Environment Variables
| Variable | Description | Default |
| --------------------- | ---------------------------------------------------- | ----------- |
| `FREECAD_MODE` | Connection mode: `xmlrpc`, `socket`, or `embedded` | `xmlrpc` |
| `FREECAD_PATH` | Path to FreeCAD's lib directory (embedded mode only) | Auto-detect |
| `FREECAD_SOCKET_HOST` | Socket/XML-RPC server hostname | `localhost` |
| `FREECAD_SOCKET_PORT` | JSON-RPC socket server port | `9876` |
| `FREECAD_XMLRPC_PORT` | XML-RPC server port | `9875` |
| `FREECAD_TIMEOUT_MS` | Execution timeout in ms | `30000` |
#### Connection Modes
| Mode | Description | Platform Support |
| ---------- | ------------------------------------------- | --------------------------------- |
| `xmlrpc` | Connects to FreeCAD via XML-RPC (port 9875) | **All platforms** (recommended) |
| `socket` | Connects via JSON-RPC socket (port 9876) | **All platforms** |
| `embedded` | Imports FreeCAD directly into process | **Linux only** (crashes on macOS) |
**Note:** Embedded mode crashes on macOS because FreeCAD's `FreeCAD.so` links to `@rpath/libpython3.11.dylib`, which conflicts with external Python interpreters. Use `xmlrpc` or `socket` mode on macOS and Windows.
#### MCP Client Configuration
Add something like the following to your MCP client settings. For Claude Code, this is `~/.claude/claude_desktop_config.json` or a project `.mcp.json` file:
```json
{
"mcpServers": {
"freecad": {
"command": "freecad-mcp",
"env": {
"FREECAD_MODE": "xmlrpc"
}
}
}
}
```
If installed from source with mise/uv:
```json
{
"mcpServers": {
"freecad": {
"command": "/path/to/mise/shims/uv",
"args": ["run", "--project", "/path/to/freecad-addon-robust-mcp-server", "freecad-mcp"],
"env": {
"FREECAD_MODE": "xmlrpc"
}
}
}
}
```
If using Docker:
```json
{
"mcpServers": {
"freecad": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"--add-host=host.docker.internal:host-gateway",
"-e", "FREECAD_MODE=xmlrpc",
"-e", "FREECAD_SOCKET_HOST=host.docker.internal",
"spkane/freecad-robust-mcp"
]
}
}
}
```
**Docker configuration notes:**
- `--rm` removes the container after it exits
- `-i` keeps stdin open for MCP communication
- `--add-host=host.docker.internal:host-gateway` allows the container to connect to FreeCAD on your host (Linux only; macOS/Windows have this built-in)
- `FREECAD_SOCKET_HOST=host.docker.internal` tells the Robust MCP Server to connect to FreeCAD on your host machine
### Usage
#### Starting the MCP Bridge in FreeCAD
Before your AI assistant can connect, you need to start the MCP bridge inside FreeCAD:
##### Option A: Using the Workbench (Recommended)
1. Install the Robust MCP Bridge workbench via FreeCAD's Addon Manager:
- **Edit -> Preferences -> Addon Manager**
- Search for "Robust MCP Bridge"
- Install and restart FreeCAD
1. Start the bridge:
- Switch to the Robust MCP Bridge workbench
- Click the **Start MCP Bridge** button in the toolbar
- Or use the menu: **MCP Bridge -> Start Bridge**
1. You should see in the FreeCAD console:
```text
MCP Bridge started!
- XML-RPC: localhost:9875
- Socket: localhost:9876
```
##### Option B: Using just commands (from source)
```bash
# Start FreeCAD with MCP bridge auto-started
just freecad::run-gui
# Or for headless/automation mode:
just freecad::run-headless
```
After starting the bridge, start/restart your MCP client (Claude Code, etc.) - it will connect automatically
#### Uninstalling the MCP Bridge
To uninstall the Robust MCP Bridge workbench:
1. Open FreeCAD
1. Go to **Edit -> Preferences -> Addon Manager**
1. Find "Robust MCP Bridge" in the list
1. Click **Uninstall**
1. Restart FreeCAD
##### Checking for Legacy Components
If you previously used older versions of this project, you may have legacy components installed. Run this command to check what's installed and get cleanup instructions:
```bash
just install::status
```
##### Manual Cleanup (if needed)
Remove any legacy files that may conflict with the workbench:
```bash
# macOS - remove legacy plugin and macro
rm -rf ~/Library/Application\ Support/FreeCAD/Mod/MCPBridge/
rm -f ~/Library/Application\ Support/FreeCAD/Macro/StartMCPBridge.FCMacro
# Linux - remove legacy plugin and macro
rm -rf ~/.local/share/FreeCAD/Mod/MCPBridge/
rm -f ~/.local/share/FreeCAD/Macro/StartMCPBridge.FCMacro
```
#### Running Modes
##### XML-RPC Mode (Recommended)
Connects to a running FreeCAD instance via XML-RPC. Works on all platforms.
```bash
FREECAD_MODE=xmlrpc freecad-mcp
```
##### Socket Mode (JSON-RPC)
Connects via JSON-RPC socket. Works on all platforms.
```bash
FREECAD_MODE=socket freecad-mcp
```
##### Headless Mode
Run FreeCAD in console mode without GUI. Useful for automation.
```bash
# If installed from source:
just freecad::run-headless
```
**Note:** Screenshot and view features are not available in headless mode.
##### Embedded Mode (Linux Only)
Runs FreeCAD in-process. **Only works on Linux** - crashes on macOS/Windows.
```bash
FREECAD_MODE=embedded freecad-mcp
```
### Available Tools
The Robust MCP Server provides **83 tools** organized into categories. Tools marked with **GUI** require FreeCAD to be running in GUI mode; they will return an error in headless mode.
#### Execution & Debugging (5 tools)
| Tool | Description | Mode |
| ---------------------------- | ------------------------------------------------------------- | ---- |
| `execute_python` | Execute arbitrary Python code in FreeCAD's context | All |
| `get_freecad_version` | Get FreeCAD version, build date, and Python version | All |
| `get_connection_status` | Check MCP bridge connection status and latency | All |
| `get_console_output` | Get recent FreeCAD console output (up to N lines) | All |
| `get_mcp_server_environment` | Get Robust MCP Server environment (OS, hostname, instance_id) | All |
#### Document Management (7 tools)
| Tool | Description | Mode |
| --------------------- | ----------------------------------------- | ---- |
| `list_documents` | List all open documents with metadata | All |
| `get_active_document` | Get information about the active document | All |
| `create_document` | Create a new FreeCAD document | All |
| `open_document` | Open an existing .FCStd file | All |
| `save_document` | Save a document to disk | All |
| `close_document` | Close a document (with optional save) | All |
| `recompute_document` | Force recomputation of all objects | All |
#### Object Creation - Primitives (8 tools)
| Tool | Description | Mode |
| ----------------- | -------------------------------------------------- | ---- |
| `create_object` | Create a generic FreeCAD object by type ID | All |
| `create_box` | Create a Part::Box with length, width, height | All |
| `create_cylinder` | Create a Part::Cylinder with radius, height, angle | All |
| `create_sphere` | Create a Part::Sphere with radius | All |
| `create_cone` | Create a Part::Cone with two radii and height | All |
| `create_torus` | Create a Part::Torus (donut) with radii and angles | All |
| `create_wedge` | Create a Part::Wedge (tapered box) | All |
| `create_helix` | Create a Part::Helix curve for sweeps and threads | All |
#### Object Management (12 tools)
| Tool | Description | Mode |
| ------------------- | -------------------------------------------------- | ---- |
| `list_objects` | List all objects in a document | All |
| `inspect_object` | Get detailed object info (properties, shape, etc.) | All |
| `edit_object` | Modify properties of an existing object | All |
| `delete_object` | Delete an object from a document | All |
| `set_placement` | Set object position and rotation | All |
| `scale_object` | Scale an object uniformly or non-uniformly | All |
| `rotate_object` | Rotate an object around an axis | All |
| `copy_object` | Create a copy of an object | All |
| `mirror_object` | Mirror an object across a plane (XY, XZ, YZ) | All |
| `boolean_operation` | Fuse, cut, or intersect objects | All |
| `get_selection` | Get currently selected objects | GUI |
| `set_selection` | Select specific objects by name | GUI |
| `clear_selection` | Clear all selections | GUI |
#### PartDesign - Sketching (14 tools)
| Tool | Description | Mode |
| ------------------------ | ----------------------------------------------- | ---- |
| `create_partdesign_body` | Create a PartDesign::Body container | All |
| `create_sketch` | Create a sketch on a plane or face | All |
| `add_sketch_rectangle` | Add a rectangle to a sketch | All |
| `add_sketch_circle` | Add a circle to a sketch | All |
| `add_sketch_line` | Add a line (with optional construction flag) | All |
| `add_sketch_arc` | Add an arc by center, radius, and angles | All |
| `add_sketch_point` | Add a point (useful for hole centers) | All |
| `pad_sketch` | Extrude a sketch (additive) | All |
| `pocket_sketch` | Cut into solid using a sketch (subtractive) | All |
| `revolution_sketch` | Revolve a sketch around an axis (additive) | All |
| `groove_sketch` | Revolve a sketch around an axis (subtractive) | All |
| `create_hole` | Create parametric holes with optional threading | All |
| `loft_sketches` | Create a loft through multiple sketches | All |
| `sweep_sketch` | Sweep a profile along a spine path | All |
#### PartDesign - Patterns & Edges (5 tools)
| Tool | Description | Mode |
| ------------------ | ------------------------------------------ | ---- |
| `linear_pattern` | Create linear pattern of a feature | All |
| `polar_pattern` | Create polar/circular pattern of a feature | All |
| `mirrored_feature` | Mirror a feature across a plane | All |
| `fillet_edges` | Add fillets (rounded edges) | All |
| `chamfer_edges` | Add chamfers (beveled edges) | All |
#### View & Display (11 tools)
| Tool | Description | Mode |
| ----------------------- | ----------------------------------------------- | ---- |
| `get_screenshot` | Capture a screenshot of the 3D view | GUI |
| `set_view_angle` | Set camera to standard views (Front, Top, etc.) | GUI |
| `fit_all` | Zoom to fit all objects in view | GUI |
| `zoom_in` | Zoom in by a factor | GUI |
| `zoom_out` | Zoom out by a factor | GUI |
| `set_camera_position` | Set camera position and look-at point | GUI |
| `set_object_visibility` | Show/hide objects | GUI |
| `set_display_mode` | Set display mode (Shaded, Wireframe, etc.) | GUI |
| `set_object_color` | Set object color as RGB values | GUI |
| `list_workbenches` | List available FreeCAD workbenches | All |
| `activate_workbench` | Switch to a different workbench | All |
#### Undo/Redo (3 tools)
| Tool | Description | Mode |
| ---------------------- | ---------------------------------- | ---- |
| `undo` | Undo the last operation | All |
| `redo` | Redo a previously undone operation | All |
| `get_undo_redo_status` | Get available undo/redo operations | All |
#### Export/Import (7 tools)
| Tool | Description | Mode |
| ------------- | ------------------------------------------ | ---- |
| `export_step` | Export to STEP format (ISO CAD exchange) | All |
| `export_stl` | Export to STL format (3D printing) | All |
| `export_3mf` | Export to 3MF format (modern 3D printing) | All |
| `export_obj` | Export to OBJ format (Wavefront) | All |
| `export_iges` | Export to IGES format (older CAD exchange) | All |
| `import_step` | Import a STEP file | All |
| `import_stl` | Import an STL file as mesh | All |
#### Macro Management (6 tools)
| Tool | Description | Mode |
| ---------------------------- | ---------------------------------------------- | ---- |
| `list_macros` | List all available FreeCAD macros | All |
| `run_macro` | Execute a macro by name | All |
| `create_macro` | Create a new macro file | All |
| `read_macro` | Read macro source code | All |
| `delete_macro` | Delete a user macro | All |
| `create_macro_from_template` | Create macro from template (basic, part, etc.) | All |
#### Parts Library (2 tools)
| Tool | Description | Mode |
| -------------------------- | ------------------------------------- | ---- |
| `list_parts_library` | List parts in FreeCAD's parts library | All |
| `insert_part_from_library` | Insert a part from the library | All |
---
## For Developers
This section covers development setup, contributing, and working with the codebase.
## Robust MCP Server Development
### Prerequisites
- [mise](https://mise.jdx.dev/) - Tool version manager
- [FreeCAD](https://www.freecadweb.org/) 0.21+ or 1.0+
### Initial Setup
```bash
# Clone the repository
git clone https://github.com/spkane/freecad-addon-robust-mcp-server.git
cd freecad-addon-robust-mcp-server
# Install mise via the Official mise installer script (if not already installed)
curl https://mise.run | sh
# Install all tools (Python 3.11, uv, just, pre-commit)
mise trust
mise install
# Set up the development environment
just setup
```
This installs:
- **Python 3.11** - Required for FreeCAD ABI compatibility
- **uv** - Fast Python package manager
- **just** - Command runner for development workflows
- **pre-commit** - Git hooks for code quality
### MCP Client Configuration (Development)
Create a `.mcp.json` file in the project directory:
```json
{
"mcpServers": {
"freecad": {
"command": "/path/to/mise/shims/uv",
"args": ["run", "--project", "/path/to/freecad-addon-robust-mcp-server", "freecad-mcp"],
"env": {
"FREECAD_MODE": "xmlrpc",
"FREECAD_SOCKET_HOST": "localhost",
"FREECAD_XMLRPC_PORT": "9875",
"PATH": "/path/to/mise/shims:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
}
}
}
}
```
**Replace the paths with your actual paths:**
| Placeholder | Description | Example |
| ------------------------------------------- | ------------------------------- | ---------------------------------------------- |
| `/path/to/mise/shims/uv` | Full path to uv via mise shims | `~/.local/share/mise/shims/uv` |
| `/path/to/freecad-addon-robust-mcp-server` | Project directory | `/home/me/dev/freecad-addon-robust-mcp-server` |
| `/path/to/mise/shims` | mise shims directory for PATH | `~/.local/share/mise/shims` |
**Finding your mise shims path:**
```bash
mise where uv | sed 's|/installs/.*|/shims|'
# Example: /home/user/.local/share/mise/shims (on Linux) or ~/.local/share/mise/shims (on macOS)
```
### Development Workflow
Commands are organized into modules. Use `just` to see top-level commands, or `just list-<module>` to see module-specific commands.
```bash
# Show top-level commands and available modules
just
# Show commands in a specific module
just list-mcp # Robust MCP Server commands
just list-freecad # FreeCAD plugin/macro commands
just list-install # Installation commands
just list-quality # Code quality commands
just list-testing # Test commands
just list-docker # Docker commands
just list-documentation # Documentation commands
just list-dev # Development utilities
# List ALL commands from all modules
just list-all
# Install/update dependencies
just install::mcp-server
# Run all checks (linting, type checking, tests)
just all
# Quality commands
just quality::lint # Run ruff linter
just quality::typecheck # Run mypy type checker
just quality::format # Format code
just quality::check # Run all pre-commit hooks
# Testing commands
just testing::unit # Run unit tests
just testing::cov # Run tests with coverage
just testing::integration # Run integration tests
# Run the Robust MCP Server (or with debug logging)
just mcp::run
just mcp::run-debug
# Docker commands
just docker::build # Build image for local architecture
just docker::build-multi # Build multi-arch image (amd64 + arm64)
just docker::run # Run container
```
### Running FreeCAD with the MCP Bridge
#### GUI Mode (recommended for development)
```bash
# Start FreeCAD with auto-started bridge
just freecad::run-gui
```
#### Headless Mode (for automation/CI)
```bash
just freecad::run-headless
```
### Running Tests
```bash
# Unit tests only (no FreeCAD required)
just testing::unit
# Unit tests with coverage
just testing::cov
# Integration tests (requires running FreeCAD bridge)
just testing::integration
# Integration tests with automatic FreeCAD startup
just testing::integration-auto
```
### Code Quality
The project uses strict code quality checks via pre-commit:
- **Ruff** - Linting and formatting
- **MyPy** - Type checking
- **Bandit** - Security scanning
- **Codespell** - Spell checking
- **Secrets scanning** - Gitleaks, detect-secrets, TruffleHog
```bash
# Run all pre-commit hooks
just quality::check
# Run security/secrets scans
just quality::security
just quality::secrets
```
---
## Architecture
See the [detailed architecture document](docs/development/architecture-detailed.md) for design documentation covering:
- Module structure
- Bridge communication protocols
- Tool registration patterns
- FreeCAD plugin architecture
---
## Acknowledgements
This project was developed after analyzing several existing FreeCAD Robust MCP implementations. We are grateful to these projects for their pioneering work and the ideas they contributed to the FreeCAD + AI ecosystem:
### Related Projects
- **[neka-nat/freecad-mcp](https://github.com/neka-nat/freecad-mcp)** (MIT License) - The queue-based thread safety pattern and XML-RPC protocol design (port 9875) were directly inspired by this project. Our implementation maintains protocol compatibility while being a complete rewrite with additional features.
- **[jango-blockchained/mcp-freecad](https://github.com/jango-blockchained/mcp-freecad)** - Inspired our connection recovery mechanisms and multi-mode architecture approach.
- **[contextform/freecad-mcp](https://github.com/contextform/freecad-mcp)** - Informed our comprehensive PartDesign and Part workbench tool coverage.
- **[ATOI-Ming/FreeCAD-MCP](https://github.com/ATOI-Ming/FreeCAD-MCP)** - Inspired our macro development toolkit including templates, validation, and automatic imports.
- **[bonninr/freecad_mcp](https://github.com/bonninr/freecad_mcp)** - Influenced our simple socket-based communication approach.
See [docs/COMPARISON.md](docs/COMPARISON.md) for a detailed analysis of these implementations and the design decisions they informed.
---
## License
MIT License - see [LICENSE](LICENSE) for details.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<!-- White background -->
<rect width="64" height="64" fill="#ffffff"/>
<!-- Bridge Structure -->
<!-- Bridge deck -->
<rect x="4" y="40" width="56" height="5" rx="1" fill="#8b7355" stroke="#6b5344" stroke-width="1"/>
<!-- Bridge railings -->
<rect x="4" y="37" width="56" height="2" rx="0.5" fill="#a08060"/>
<!-- Bridge supports/pillars -->
<rect x="8" y="45" width="6" height="8" fill="#6b5344"/>
<rect x="50" y="45" width="6" height="8" fill="#6b5344"/>
<!-- Bridge arch underneath -->
<path d="M 14 53 Q 32 47 50 53" fill="none" stroke="#6b5344" stroke-width="2"/>
<!-- Robot on the bridge (facing forward) -->
<!-- Robot body -->
<rect x="24" y="18" width="16" height="14" rx="2" fill="#4a90d9" stroke="#2c5aa0" stroke-width="1.5"/>
<!-- Robot head -->
<rect x="26" y="8" width="12" height="10" rx="2" fill="#4a90d9" stroke="#2c5aa0" stroke-width="1.5"/>
<!-- Robot face plate -->
<rect x="28" y="10" width="8" height="6" rx="1" fill="#e8f4fc"/>
<!-- Robot eyes -->
<circle cx="30" cy="13" r="1.5" fill="#2c5aa0"/>
<circle cx="34" cy="13" r="1.5" fill="#2c5aa0"/>
<!-- Robot antenna -->
<line x1="32" y1="8" x2="32" y2="3" stroke="#2c5aa0" stroke-width="2" stroke-linecap="round"/>
<circle cx="32" cy="2" r="3" fill="#27ae60"/>
<!-- Robot arms -->
<rect x="18" y="20" width="6" height="4" rx="1.5" fill="#3a7bc8"/>
<rect x="40" y="20" width="6" height="4" rx="1.5" fill="#3a7bc8"/>
<!-- Robot legs -->
<rect x="26" y="32" width="4" height="8" rx="1" fill="#3a7bc8"/>
<rect x="34" y="32" width="4" height="8" rx="1" fill="#3a7bc8"/>
<!-- Bidirectional Data Flow (river under bridge) -->
<!-- Data stream background -->
<path d="M 0 60 Q 32 55 64 60" fill="none" stroke="#27ae60" stroke-width="4" opacity="0.3"/>
<!-- Left arrow -->
<path d="M 6 59 L 16 59" stroke="#27ae60" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="6,59 11,56 11,62" fill="#27ae60"/>
<!-- Right arrow -->
<path d="M 48 59 L 58 59" stroke="#27ae60" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="58,59 53,56 53,62" fill="#27ae60"/>
<!-- Center data dots (flowing) -->
<circle cx="26" cy="57" r="2" fill="#27ae60"/>
<circle cx="32" cy="56" r="2" fill="#27ae60"/>
<circle cx="38" cy="57" r="2" fill="#27ae60"/>
<!-- MCP Label on bridge deck -->
<text x="32" y="44" font-family="Arial, Helvetica, sans-serif" font-size="5" font-weight="bold" fill="#ffffff" text-anchor="middle">MCP</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

-231
View File
@@ -1,231 +0,0 @@
"""Robust MCP Bridge Workbench - Initialization.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module is executed when FreeCAD starts up. It handles initialization
tasks for the Robust MCP Bridge workbench, including auto-start of the
MCP bridge if configured. Works in both GUI and headless modes.
Note: Status bar updates are handled by InitGui.py since Qt operations
must run on the main thread.
"""
from __future__ import annotations
# Import FreeCAD first so we can log early
import FreeCAD
FreeCAD.Console.PrintMessage("Robust MCP Bridge: Init.py loaded\n")
from typing import TYPE_CHECKING, Any # noqa: E402
if TYPE_CHECKING:
from freecad_mcp_bridge.bridge_utils import GuiWaiter
FreeCAD.Console.PrintMessage("Robust MCP Bridge: Init loaded\n")
# Global reference to GuiWaiter and auto-start timer to prevent garbage collection
# Type annotations use Any for timer since it could be QTimer from PySide2 or PySide6
_auto_start_timer: Any | None = None
_gui_waiter: GuiWaiter | None = None
def _auto_start_bridge() -> None:
"""Auto-start the MCP bridge if configured in preferences.
This function is called via a deferred timer (GUI mode) or directly
(headless mode) after FreeCAD finishes loading. It starts the bridge
without requiring the workbench to be selected.
Args:
None.
Returns:
None. Early returns if auto-start is disabled or bridge is already running.
Raises:
Exception: Any exception during bridge startup is caught, logged to
FreeCAD.Console.PrintError with full traceback, and suppressed.
Side Effects:
- Imports and checks auto-start preference from preferences module
- Creates and starts a FreecadMCPPlugin instance if not already running
- Registers the plugin with the workbench commands module
- Prints status messages to FreeCAD.Console
Example:
This function is typically called via QTimer or GuiWaiter callback::
QtCore.QTimer.singleShot(1000, _auto_start_bridge)
"""
try:
from preferences import get_auto_start
if not get_auto_start():
return
# Check if bridge is already running
from commands import _mcp_plugin
if _mcp_plugin is not None and _mcp_plugin.is_running:
return
FreeCAD.Console.PrintMessage(
"Auto-starting MCP Bridge (configured in preferences)...\n"
)
# Import and start the bridge directly
from freecad_mcp_bridge.server import FreecadMCPPlugin
from preferences import get_socket_port, get_xmlrpc_port
xmlrpc_port = get_xmlrpc_port()
socket_port = get_socket_port()
plugin = FreecadMCPPlugin(
host="localhost",
port=socket_port,
xmlrpc_port=xmlrpc_port,
enable_xmlrpc=True,
)
plugin.start()
# Register plugin with commands module for restart detection
from freecad_mcp_bridge.bridge_utils import register_mcp_plugin
register_mcp_plugin(plugin, xmlrpc_port, socket_port)
FreeCAD.Console.PrintMessage("\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage("MCP Bridge started!\n")
FreeCAD.Console.PrintMessage(f" - XML-RPC: localhost:{xmlrpc_port}\n")
FreeCAD.Console.PrintMessage(f" - Socket: localhost:{socket_port}\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage(
"\nYou can now connect your MCP client (Claude Code, etc.) to FreeCAD.\n"
)
except Exception as e:
FreeCAD.Console.PrintError(f"Failed to auto-start MCP Bridge: {e}\n")
import traceback
FreeCAD.Console.PrintError(f"Traceback: {traceback.format_exc()}\n")
# Schedule auto-start after FreeCAD finishes loading
# Strategy:
# - If FreeCAD.GuiUp is True: Qt event loop is running, use timer for deferred start
# - If FreeCAD.GuiUp is False but QApplication exists: FreeCAD GUI is initializing.
# Use GuiWaiter to wait for GuiUp to become True before starting.
# This ensures the bridge uses Qt timer (not background thread) for queue processing.
# - If no QApplication: True headless mode, start bridge directly
#
# IMPORTANT: We check for QApplication.instance() rather than just QtCore availability
# because FreeCAD bundles PySide even in headless mode (freecadcmd), but there's no
# Qt event loop running. Without a QApplication, Qt timers will never fire.
#
# CRITICAL: We must wait for FreeCAD.GuiUp to be True before starting the bridge
# in GUI mode. If we start when GuiUp is False, the bridge's _start_queue_processor()
# will see GuiUp=False and use a background thread. Later, code executed on that
# thread will try to do Qt operations, causing crashes (SIGABRT in QCocoaWindow).
try:
from preferences import get_auto_start
_auto_start_enabled = get_auto_start()
FreeCAD.Console.PrintMessage(
f"Robust MCP Bridge: Auto-start preference = {_auto_start_enabled}\n"
)
if _auto_start_enabled:
# Try to import Qt and check for running QApplication
import contextlib
QtCore = None
QtWidgets = None
_has_qapp = False
_is_true_headless = False
try:
from PySide2 import QtCore, QtWidgets # type: ignore[assignment, no-redef]
except ImportError:
with contextlib.suppress(ImportError):
from PySide6 import ( # type: ignore[assignment, no-redef]
QtCore,
QtWidgets,
)
# Detect GUI mode vs true headless mode
# - True headless (freecadcmd): QCoreApplication exists but NOT QApplication
# - GUI mode early startup: No app yet, or QApplication being initialized
# - GUI mode ready: FreeCAD.GuiUp is True
if QtWidgets is not None and QtCore is not None:
qapp = QtWidgets.QApplication.instance()
if qapp is not None:
_has_qapp = True
else:
# No QApplication - check if QCoreApplication exists
# If QCoreApplication exists but is NOT a QApplication, it's true headless
qcore_app = QtCore.QCoreApplication.instance()
if qcore_app is not None and not isinstance(
qcore_app, QtWidgets.QApplication
):
_is_true_headless = True
# If no app at all, assume early GUI startup (will use GuiWaiter)
FreeCAD.Console.PrintMessage(
f"Robust MCP Bridge: GuiUp={FreeCAD.GuiUp}, "
f"QtCore={'available' if QtCore else 'unavailable'}, "
f"QApp={'running' if _has_qapp else 'none'}, "
f"headless={_is_true_headless}\n"
)
if FreeCAD.GuiUp:
# GUI is already up - use timer for deferred start
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: GUI already up, scheduling deferred start...\n"
)
if QtCore is not None:
_auto_start_timer = QtCore.QTimer()
_auto_start_timer.setSingleShot(True)
_auto_start_timer.timeout.connect(_auto_start_bridge)
_auto_start_timer.start(1000)
else:
# GUI is up but Qt import failed - start directly
_auto_start_bridge()
elif _is_true_headless:
# True headless mode - QCoreApplication exists but not QApplication
# No Qt event loop for GUI, so start bridge directly with background thread
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: True headless mode (QCoreApplication only), "
"starting directly...\n"
)
_auto_start_bridge()
elif QtCore is not None:
# GUI not ready yet (either QApplication exists or no app yet)
# Use GuiWaiter to wait for GuiUp to become True before starting
# This ensures the bridge uses Qt timer (not background thread) for queue
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: GUI not ready, using GuiWaiter...\n"
)
from freecad_mcp_bridge.bridge_utils import GuiWaiter
_gui_waiter = GuiWaiter(
callback=_auto_start_bridge,
log_prefix="Robust MCP Bridge",
timeout_error_extra=(
"\nTo start the bridge manually, select the Robust MCP Bridge "
"workbench\nand click 'Start MCP Bridge'.\n\n"
),
)
_gui_waiter.start()
else:
# No Qt available at all - unusual state, start directly
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: No Qt available, starting directly...\n"
)
_auto_start_bridge()
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not set up auto-start: {e}\n")
import traceback
FreeCAD.Console.PrintWarning(f"Traceback: {traceback.format_exc()}\n")
-294
View File
@@ -1,294 +0,0 @@
"""Robust MCP Bridge Workbench - GUI Initialization.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module defines the workbench class for the Robust MCP Bridge.
It provides toolbar buttons and menu items to start and stop the
MCP bridge server. Commands are defined in the commands module.
"""
from __future__ import annotations
import FreeCAD
import FreeCADGui
# Register icons path for preferences page icon
# This must be done at module level, before the preferences page is registered
try:
from path_utils import get_icons_dir
_icons_dir = get_icons_dir()
if _icons_dir:
FreeCADGui.addIconPath(_icons_dir)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not register icon path: {e}\n")
# Register preferences page with FreeCAD's Preferences dialog
# This must be done at module level, before the workbench is registered
try:
from preferences_page import MCPBridgePreferencesPage
FreeCADGui.addPreferencePage(MCPBridgePreferencesPage, "Robust MCP Bridge")
except Exception as e:
FreeCAD.Console.PrintWarning(
f"Could not register MCP Bridge preferences page: {e}\n"
)
class FreecadRobustMCPBridgeWorkbench(FreeCADGui.Workbench):
"""Robust MCP Bridge workbench for FreeCAD.
Provides toolbar and menu commands to start, stop, and monitor the MCP
bridge server for AI assistant integration.
Attributes:
MenuText: Workbench display name in FreeCAD.
ToolTip: Short description shown by FreeCAD.
Icon: Icon path used by FreeCAD.
Example:
The workbench is registered at import time by FreeCAD::
FreeCADGui.addWorkbench(FreecadRobustMCPBridgeWorkbench())
"""
MenuText = "Robust MCP Bridge"
ToolTip = "Robust MCP Bridge for AI assistant integration with FreeCAD"
def __init__(self) -> None:
"""Initialize workbench with icon path."""
from path_utils import get_workbench_icon
self.Icon = get_workbench_icon()
def Initialize(self) -> None:
"""Initialize the workbench - called once when first activated."""
# Import commands module here (not at top level) to ensure
# it's available during FreeCAD's module loading process
from commands import (
MCPBridgePreferencesCommand,
MCPBridgeStatusCommand,
StartMCPBridgeCommand,
StopMCPBridgeCommand,
)
# Register commands
FreeCADGui.addCommand("Start_MCP_Bridge", StartMCPBridgeCommand())
FreeCADGui.addCommand("Stop_MCP_Bridge", StopMCPBridgeCommand())
FreeCADGui.addCommand("MCP_Bridge_Status", MCPBridgeStatusCommand())
FreeCADGui.addCommand("MCP_Bridge_Preferences", MCPBridgePreferencesCommand())
# Create toolbar with main commands
toolbar_commands = [
"Start_MCP_Bridge",
"Stop_MCP_Bridge",
"MCP_Bridge_Status",
]
self.appendToolbar("Robust MCP Bridge", toolbar_commands)
# Create menu with all commands including preferences
menu_commands = [
"Start_MCP_Bridge",
"Stop_MCP_Bridge",
"MCP_Bridge_Status",
"Separator",
"MCP_Bridge_Preferences",
]
self.appendMenu("Robust MCP Bridge", menu_commands)
FreeCAD.Console.PrintMessage("Robust MCP Bridge workbench initialized\n")
# Auto-start bridge if preference is enabled
# This is a fallback if the module-level timer didn't fire
# (which can happen if the module isn't loaded until workbench selection)
try:
from preferences import get_auto_start
if get_auto_start():
# Check if already running (timer might have started it)
from commands import is_bridge_running
if not is_bridge_running():
FreeCAD.Console.PrintMessage(
"Auto-starting MCP Bridge (configured in preferences)...\n"
)
FreeCADGui.runCommand("Start_MCP_Bridge")
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not auto-start MCP Bridge: {e}\n")
# Sync status bar widget with current bridge state
# (bridge may have been started by Init.py before workbench was selected)
try:
from status_widget import sync_status_with_bridge
sync_status_with_bridge()
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not sync status bar: {e}\n")
def Activated(self) -> None:
"""Called when the workbench is activated."""
# Sync status bar widget with current bridge state
try:
from status_widget import sync_status_with_bridge
sync_status_with_bridge()
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not sync status bar: {e}\n")
def Deactivated(self) -> None:
"""Called when the workbench is deactivated."""
pass
def GetClassName(self) -> str:
"""Return the C++ class name for this workbench."""
return "Gui::PythonWorkbench"
# Register the workbench
FreeCADGui.addWorkbench(FreecadRobustMCPBridgeWorkbench())
# Schedule status bar sync after a short delay to allow GUI to finish initializing
# This runs on the main thread (InitGui.py is executed on main thread)
try:
try:
from PySide2 import QtCore
except ImportError:
from PySide6 import QtCore
def _deferred_status_bar_sync() -> None:
"""Sync status bar with bridge state after GUI is ready."""
try:
from commands import is_bridge_running
from preferences import get_status_bar_enabled
from status_widget import sync_status_with_bridge
if get_status_bar_enabled() and is_bridge_running():
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: Syncing status bar from InitGui...\n"
)
sync_status_with_bridge()
except Exception as e:
FreeCAD.Console.PrintWarning(
f"Robust MCP Bridge: Deferred status bar sync failed: {e}\n"
)
# Use QTimer.singleShot on the main thread - this should work
QtCore.QTimer.singleShot(2000, _deferred_status_bar_sync)
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: Status bar sync scheduled from InitGui (2s)\n"
)
# Auto-start the MCP bridge if configured in preferences
# This runs at FreeCAD GUI startup (InitGui.py module-level code)
# Note: Init.py does NOT run at startup for workbench addons, so auto-start
# must be triggered from here instead.
#
# We use GuiWaiter to poll FreeCAD.GuiUp instead of a fixed timer delay.
# This ensures we wait for the GUI to actually be ready, rather than
# hoping a fixed delay is long enough.
def _auto_start_bridge() -> None:
"""Auto-start bridge after GUI is confirmed ready.
This is the callback invoked by GuiWaiter once FreeCAD.GuiUp is True
and a defer period has elapsed. At this point, it's safe to start
the MCP bridge with Qt timer-based queue processing.
Args:
None.
Returns:
None. Early returns if auto-start disabled or bridge already running.
Raises:
Exception: Any exception during bridge startup is caught, logged
to FreeCAD.Console.PrintError with full traceback, and suppressed.
Side Effects:
- Creates and starts a FreecadMCPPlugin instance
- Registers the plugin with the workbench commands module
- Syncs the status bar widget with bridge state
"""
try:
# Safety check: verify GUI is actually ready before starting
# If not ready, reschedule for another attempt
if not FreeCAD.GuiUp:
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: GUI not ready, rescheduling auto-start...\n"
)
QtCore.QTimer.singleShot(500, _auto_start_bridge)
return
from preferences import get_auto_start
if not get_auto_start():
return
# Check if bridge is already running
from commands import is_bridge_running
if is_bridge_running():
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: Bridge already running, skipping auto-start\n"
)
return
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: Auto-starting from InitGui...\n"
)
# Import and start the bridge
from freecad_mcp_bridge.bridge_utils import register_mcp_plugin
from freecad_mcp_bridge.server import FreecadMCPPlugin
from preferences import get_socket_port, get_xmlrpc_port
xmlrpc_port = get_xmlrpc_port()
socket_port = get_socket_port()
plugin = FreecadMCPPlugin(
host="localhost",
port=socket_port,
xmlrpc_port=xmlrpc_port,
enable_xmlrpc=True,
)
plugin.start()
# Register plugin with commands module
register_mcp_plugin(plugin, xmlrpc_port, socket_port)
# Sync status bar now that bridge is running
try:
from status_widget import sync_status_with_bridge
sync_status_with_bridge()
except Exception as status_err:
FreeCAD.Console.PrintWarning(
f"Could not sync status bar after auto-start: {status_err}\n"
)
except Exception as e:
FreeCAD.Console.PrintError(f"Robust MCP Bridge: Auto-start failed: {e}\n")
import traceback
FreeCAD.Console.PrintError(traceback.format_exc())
# Check if auto-start is enabled before scheduling
from preferences import get_auto_start
if get_auto_start():
# Schedule auto-start after a delay to ensure GUI is fully ready.
# InitGui.py module-level code runs early in FreeCAD startup, so we
# need to defer the bridge start to avoid race conditions.
# Note: We use a simple QTimer.singleShot() here because GuiWaiter
# has timing issues when used from module-level code during startup.
QtCore.QTimer.singleShot(3000, _auto_start_bridge)
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: Auto-start scheduled from InitGui (3s)\n"
)
else:
FreeCAD.Console.PrintMessage(
"Robust MCP Bridge: Auto-start disabled in preferences\n"
)
except Exception as e:
FreeCAD.Console.PrintWarning(
f"Robust MCP Bridge: Could not schedule status bar sync: {e}\n"
)
@@ -1,97 +0,0 @@
# Robust MCP Bridge Workbench Release Notes
## Version 0.6.2 (2026-01-18)
This release fixes some auto-start issues and improves the overall startup experience across all supported modes.
### Added
- No new features in this release.
### Changed
- **Repository restructured**: This addon now focuses solely on the MCP Bridge Workbench. Standalone macros have been moved to dedicated repositories for independent release cycles.
- **Cleaner startup messages**: Removed duplicate success messages when bridge auto-starts.
### Fixed
- **Auto-start not working at FreeCAD startup**: Fixed bug where auto-start only worked when manually selecting the workbench. For FreeCAD workbench addons, `Init.py` does NOT run at startup - only `InitGui.py` module-level code runs. Auto-start logic has been moved to `InitGui.py`.
- **Status bar not appearing after auto-start**: The status bar widget now syncs immediately after the bridge starts, instead of on a timer that ran before the bridge was ready.
- **Integration test crashes**: Fixed race condition where the bridge could start before `FreeCAD.GuiUp` was `True`, causing Qt operations to run on a background thread and crash FreeCAD. Auto-start is now deferred with `QTimer.singleShot()` to allow the GUI to stabilize.
### Note
The standalone macros (Cut Object for Magnets and Multi Export) are now maintained in separate repositories:
- [freecad-macro-cut-for-magnets](https://github.com/spkane/freecad-macro-cut-for-magnets)
- [freecad-macro-3d-print-multi-export](https://github.com/spkane/freecad-macro-3d-print-multi-export)
Each macro can now be installed independently via the FreeCAD Addon Manager.
## Version 0.6.1 (2026-01-12)
Release notes for changes between v0.5.0-beta and v0.6.1.
### Major Change: Macro to Workbench
The MCP Bridge has been completely rewritten from a simple macro (`Start MCP Bridge`) to a full **FreeCAD Workbench**. This provides:
- Native FreeCAD Addon Manager installation
- Integrated toolbar with start/stop/status controls
- Preferences panel for configuration
- Real-time status widget showing connection state
- Proper lifecycle management with FreeCAD
### Added
- **FreeCAD Workbench**: Full workbench with toolbar, icons, and menus
- **Addon Manager support**: Install directly from FreeCAD's Addon Manager
- **Preferences panel**: Configure ports, auto-start, and logging from Edit > Preferences
- **Status widget**: Real-time display of bridge status, connected clients, and uptime
- **Auto-start option**: Optionally start the bridge automatically when FreeCAD launches
- **Start/Stop commands**: Toolbar buttons and menu items to control the bridge
- **Custom icons**: Professional SVG icons for all commands and status indicators
- **GUI mode support**: Full integration with FreeCAD's 3D view for screenshots
- **Headless mode support**: Works with `freecadcmd` for automation pipelines
### Changed
- **Architecture**: Complete rewrite from single macro to modular workbench structure
- **Server code**: Bridge server now lives in `freecad_mcp_bridge/` package within the addon
- **Configuration**: Settings now stored in FreeCAD's preference system instead of environment variables
- **Startup behavior**: Bridge waits for GUI initialization before starting (prevents crashes)
### Fixed
- **GUI crash on macOS**: Fixed race condition where bridge started before Qt event loop was ready
- **Thread safety**: All FreeCAD operations now execute on the main thread via queue processor
- **Startup timeout**: Increased GUI wait timeout to 60 seconds for slow FreeCAD startups
- **Clean shutdown**: Proper cleanup of servers and threads when FreeCAD exits
### Removed
- **Start MCP Bridge macro**: Replaced by the workbench (macro no longer needed)
### Installation
**Via FreeCAD Addon Manager (Recommended):**
1. Open FreeCAD
2. Go to Tools > Addon Manager
3. Search for "Robust MCP Bridge"
4. Click Install
5. Restart FreeCAD
**Manual Installation:**
Copy the `FreecadRobustMCPBridge` folder to your FreeCAD Mod directory:
- **macOS**: `~/Library/Application Support/FreeCAD/Mod/`
- **Linux**: `~/.local/share/FreeCAD/Mod/`
- **Windows**: `%APPDATA%/FreeCAD/Mod/`
### Upgrade Notes
- **Uninstall the old macro**: If you had `StartMCPBridge.FCMacro`, you can delete it
- **Auto-start disabled by default**: Enable in Preferences if you want the bridge to start automatically
- **Same ports**: Default ports remain 9875 (XML-RPC) and 9876 (JSON-RPC Socket)
-470
View File
@@ -1,470 +0,0 @@
"""MCP Bridge commands for the FreeCAD workbench.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module defines the GUI commands for starting, stopping, and
checking the status of the MCP bridge server.
"""
from __future__ import annotations
from typing import Any
import FreeCAD
from path_utils import get_addon_path, get_icon_path
# FreeCADGui is imported lazily in methods that need it, as this module
# may be imported during headless operation where FreeCADGui is not available
# Re-export for any modules that might import from commands
__all__ = ["get_addon_path", "get_icon_path"]
# Global reference to the plugin instance
_mcp_plugin: Any = None
# Track current running configuration for restart detection
_running_config: dict[str, int] | None = None
def is_bridge_running() -> bool:
"""Check if the MCP bridge is currently running.
This is a public helper to encapsulate access to the private _mcp_plugin state.
Returns:
True if the bridge is running, False otherwise.
"""
return _mcp_plugin is not None and _mcp_plugin.is_running
class StartMCPBridgeCommand:
"""Command to start the MCP bridge server."""
def GetResources(self) -> dict[str, str]:
"""Return the command resources (icon, menu text, tooltip)."""
# Get configured ports for tooltip (fall back to defaults if import fails)
try:
from preferences import get_socket_port, get_xmlrpc_port
xmlrpc_port = get_xmlrpc_port()
socket_port = get_socket_port()
except Exception:
xmlrpc_port = 9875
socket_port = 9876
return {
"Pixmap": get_icon_path("icons/mcp_start.svg"),
"MenuText": "Start MCP Bridge",
"ToolTip": (
"Start the MCP bridge server for AI assistant integration.\n"
f"Listens on XML-RPC (port {xmlrpc_port}) and Socket (port {socket_port})."
),
}
def IsActive(self) -> bool:
"""Return True if the command can be executed."""
return _mcp_plugin is None or not _mcp_plugin.is_running
def Activated(self) -> None:
"""Execute the command to start the MCP bridge."""
global _mcp_plugin, _running_config
if _mcp_plugin is not None and _mcp_plugin.is_running:
FreeCAD.Console.PrintWarning("MCP Bridge is already running.\n")
return
try:
from freecad_mcp_bridge.server import FreecadMCPPlugin
from preferences import (
get_socket_port,
get_status_bar_enabled,
get_xmlrpc_port,
)
from status_widget import (
update_status_error,
update_status_running,
update_status_starting,
)
# Update status bar widget if enabled
if get_status_bar_enabled():
update_status_starting()
xmlrpc_port = get_xmlrpc_port()
socket_port = get_socket_port()
# Create plugin in a local variable first to avoid leaving
# a partially initialized instance in _mcp_plugin if start() fails
plugin = FreecadMCPPlugin(
host="localhost",
port=socket_port,
xmlrpc_port=xmlrpc_port,
enable_xmlrpc=True,
)
plugin.start()
# Only assign to globals after start() succeeds
_mcp_plugin = plugin
_running_config = {
"xmlrpc_port": xmlrpc_port,
"socket_port": socket_port,
}
# Update status bar widget
if get_status_bar_enabled():
update_status_running(
xmlrpc_port, socket_port, _mcp_plugin.request_count
)
FreeCAD.Console.PrintMessage("\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage("MCP Bridge started!\n")
FreeCAD.Console.PrintMessage(f" - XML-RPC: localhost:{xmlrpc_port}\n")
FreeCAD.Console.PrintMessage(f" - Socket: localhost:{socket_port}\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage(
"\nYou can now connect your MCP client (Claude Code, etc.) to FreeCAD.\n"
)
except ImportError as e:
# Clear any stale state to ensure clean retry
_mcp_plugin = None
_running_config = None
FreeCAD.Console.PrintError(f"Failed to import MCP Bridge module: {e}\n")
FreeCAD.Console.PrintError(
"Ensure the FreecadRobustMCPBridge addon is properly installed.\n"
)
try:
from preferences import get_status_bar_enabled
from status_widget import update_status_error
if get_status_bar_enabled():
update_status_error(str(e))
except Exception:
pass
except Exception as e:
# Clear any stale state to ensure clean retry
_mcp_plugin = None
_running_config = None
FreeCAD.Console.PrintError(f"Failed to start MCP Bridge: {e}\n")
try:
from preferences import get_status_bar_enabled
from status_widget import update_status_error
if get_status_bar_enabled():
update_status_error(str(e))
except Exception:
pass
class StopMCPBridgeCommand:
"""Command to stop the MCP bridge server."""
def GetResources(self) -> dict[str, str]:
"""Return the command resources (icon, menu text, tooltip)."""
return {
"Pixmap": get_icon_path("icons/mcp_stop.svg"),
"MenuText": "Stop MCP Bridge",
"ToolTip": "Stop the running MCP bridge server.",
}
def IsActive(self) -> bool:
"""Return True if the command can be executed."""
return _mcp_plugin is not None and _mcp_plugin.is_running
def Activated(self) -> None:
"""Execute the command to stop the MCP bridge."""
global _mcp_plugin, _running_config
if _mcp_plugin is None or not _mcp_plugin.is_running:
FreeCAD.Console.PrintWarning("MCP Bridge is not running.\n")
return
try:
_mcp_plugin.stop()
_mcp_plugin = None
_running_config = None
# Update status bar widget
try:
from preferences import get_status_bar_enabled
from status_widget import update_status_stopped
if get_status_bar_enabled():
update_status_stopped()
except Exception:
pass
FreeCAD.Console.PrintMessage("\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage("MCP Bridge stopped.\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
except Exception as e:
FreeCAD.Console.PrintError(f"Failed to stop MCP Bridge: {e}\n")
class MCPBridgeStatusCommand:
"""Command to show MCP bridge status."""
def GetResources(self) -> dict[str, str]:
"""Return the command resources (icon, menu text, tooltip)."""
return {
"Pixmap": get_icon_path("icons/mcp_status.svg"),
"MenuText": "MCP Bridge Status",
"ToolTip": "Show the current status of the MCP bridge server.",
}
def IsActive(self) -> bool:
"""Return True if the command can be executed."""
return True
def Activated(self) -> None:
"""Execute the command to show MCP bridge status."""
FreeCAD.Console.PrintMessage("\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage("MCP Bridge Status\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
if _mcp_plugin is None:
FreeCAD.Console.PrintMessage("Status: Not initialized\n")
elif not _mcp_plugin.is_running:
FreeCAD.Console.PrintMessage("Status: Stopped\n")
else:
FreeCAD.Console.PrintMessage("Status: Running\n")
FreeCAD.Console.PrintMessage(f" Instance ID: {_mcp_plugin.instance_id}\n")
FreeCAD.Console.PrintMessage(f" XML-RPC Port: {_mcp_plugin.xmlrpc_port}\n")
FreeCAD.Console.PrintMessage(f" Socket Port: {_mcp_plugin.socket_port}\n")
FreeCAD.Console.PrintMessage(
f" Requests processed: {_mcp_plugin.request_count}\n"
)
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
def restart_bridge_if_running() -> bool:
"""Restart the bridge if it's currently running.
Returns:
True if bridge was restarted, False if it wasn't running.
"""
global _mcp_plugin, _running_config
if _mcp_plugin is None or not _mcp_plugin.is_running:
return False
FreeCAD.Console.PrintMessage("Restarting MCP Bridge with new configuration...\n")
# Update status bar widget
try:
from preferences import get_status_bar_enabled
from status_widget import update_status_starting
if get_status_bar_enabled():
update_status_starting()
except Exception:
pass
# Stop the current bridge
try:
_mcp_plugin.stop()
_mcp_plugin = None
_running_config = None
except Exception as e:
FreeCAD.Console.PrintError(f"Failed to stop MCP Bridge: {e}\n")
try:
from preferences import get_status_bar_enabled
from status_widget import update_status_error
if get_status_bar_enabled():
update_status_error(str(e))
except Exception:
pass
return False
# Start with new configuration
try:
from freecad_mcp_bridge.server import FreecadMCPPlugin
from preferences import get_socket_port, get_status_bar_enabled, get_xmlrpc_port
from status_widget import update_status_running
xmlrpc_port = get_xmlrpc_port()
socket_port = get_socket_port()
_mcp_plugin = FreecadMCPPlugin(
host="localhost",
port=socket_port,
xmlrpc_port=xmlrpc_port,
enable_xmlrpc=True,
)
_mcp_plugin.start()
_running_config = {
"xmlrpc_port": xmlrpc_port,
"socket_port": socket_port,
}
# Update status bar widget
if get_status_bar_enabled():
update_status_running(xmlrpc_port, socket_port, _mcp_plugin.request_count)
FreeCAD.Console.PrintMessage("MCP Bridge restarted successfully.\n")
FreeCAD.Console.PrintMessage(f" - XML-RPC: localhost:{xmlrpc_port}\n")
FreeCAD.Console.PrintMessage(f" - Socket: localhost:{socket_port}\n")
return True
except Exception as e:
FreeCAD.Console.PrintError(f"Failed to restart MCP Bridge: {e}\n")
try:
from preferences import get_status_bar_enabled
from status_widget import update_status_error
if get_status_bar_enabled():
update_status_error(str(e))
except Exception:
pass
return False
class MCPBridgePreferencesCommand:
"""Command to open MCP bridge preferences dialog."""
def GetResources(self) -> dict[str, str]:
"""Return the command resources (icon, menu text, tooltip)."""
return {
"Pixmap": get_icon_path("icons/preferences-robust_mcp_bridge.svg"),
"MenuText": "MCP Bridge Preferences...",
"ToolTip": "Configure MCP Bridge settings (ports, auto-start, etc.)",
}
def IsActive(self) -> bool:
"""Return True if the command can be executed."""
return True
def Activated(self) -> None:
"""Execute the command to show preferences dialog."""
if not FreeCAD.GuiUp:
FreeCAD.Console.PrintError(
"MCP Bridge Preferences requires FreeCAD GUI mode.\n"
)
return
# Import here to avoid issues during module loading
import FreeCADGui
from preferences import (
get_auto_start,
get_socket_port,
get_status_bar_enabled,
get_xmlrpc_port,
set_auto_start,
set_socket_port,
set_status_bar_enabled,
set_xmlrpc_port,
)
# Import QtWidgets with fallback for different PySide versions
try:
from PySide6 import QtWidgets
except ImportError:
try:
from PySide2 import QtWidgets
except ImportError:
from PySide import QtWidgets # type: ignore[import-not-found]
# Create the dialog
dialog = QtWidgets.QDialog(FreeCADGui.getMainWindow())
dialog.setWindowTitle("MCP Bridge Preferences")
dialog.setMinimumWidth(400)
layout = QtWidgets.QVBoxLayout(dialog)
# Startup group
startup_group = QtWidgets.QGroupBox("Startup")
startup_layout = QtWidgets.QVBoxLayout(startup_group)
auto_start_cb = QtWidgets.QCheckBox("Auto-start bridge when FreeCAD launches")
auto_start_cb.setChecked(get_auto_start())
startup_layout.addWidget(auto_start_cb)
layout.addWidget(startup_group)
# Display group
display_group = QtWidgets.QGroupBox("Display")
display_layout = QtWidgets.QVBoxLayout(display_group)
status_bar_cb = QtWidgets.QCheckBox("Show status indicator in status bar")
status_bar_cb.setChecked(get_status_bar_enabled())
display_layout.addWidget(status_bar_cb)
layout.addWidget(display_group)
# Ports group
ports_group = QtWidgets.QGroupBox("Network Ports")
ports_layout = QtWidgets.QFormLayout(ports_group)
xmlrpc_spin = QtWidgets.QSpinBox()
xmlrpc_spin.setRange(1024, 65535)
xmlrpc_spin.setValue(get_xmlrpc_port())
xmlrpc_spin.setToolTip("Port for XML-RPC connections (default: 9875)")
ports_layout.addRow("XML-RPC Port:", xmlrpc_spin)
socket_spin = QtWidgets.QSpinBox()
socket_spin.setRange(1024, 65535)
socket_spin.setValue(get_socket_port())
socket_spin.setToolTip("Port for JSON-RPC socket connections (default: 9876)")
ports_layout.addRow("Socket Port:", socket_spin)
# Warning label for ports
port_warning = QtWidgets.QLabel(
"<i>Note: If the bridge is running, changing ports will restart it.</i>"
)
port_warning.setWordWrap(True)
ports_layout.addRow(port_warning)
layout.addWidget(ports_group)
# Current status info
status_group = QtWidgets.QGroupBox("Current Status")
status_layout = QtWidgets.QVBoxLayout(status_group)
if _mcp_plugin is not None and _mcp_plugin.is_running:
status_label = QtWidgets.QLabel(
f"<b>Bridge is running</b><br>"
f"XML-RPC: localhost:{_mcp_plugin.xmlrpc_port}<br>"
f"Socket: localhost:{_mcp_plugin.socket_port}"
)
else:
status_label = QtWidgets.QLabel("<b>Bridge is not running</b>")
status_layout.addWidget(status_label)
layout.addWidget(status_group)
# Buttons
button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
)
button_box.accepted.connect(dialog.accept)
button_box.rejected.connect(dialog.reject)
layout.addWidget(button_box)
# Show dialog (use exec() not exec_() which is deprecated in PySide6)
if dialog.exec() == QtWidgets.QDialog.Accepted:
# Save preferences
old_xmlrpc = get_xmlrpc_port()
old_socket = get_socket_port()
set_auto_start(auto_start_cb.isChecked())
set_status_bar_enabled(status_bar_cb.isChecked())
set_xmlrpc_port(xmlrpc_spin.value())
set_socket_port(socket_spin.value())
FreeCAD.Console.PrintMessage("MCP Bridge preferences saved.\n")
# Check if ports changed and bridge is running
new_xmlrpc = xmlrpc_spin.value()
new_socket = socket_spin.value()
ports_changed = old_xmlrpc != new_xmlrpc or old_socket != new_socket
bridge_running = _mcp_plugin is not None and _mcp_plugin.is_running
if ports_changed and bridge_running:
restart_bridge_if_running()
@@ -1,13 +0,0 @@
"""FreeCAD Robust MCP Bridge - Bundled server module for the workbench addon.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module provides the MCP bridge server that runs inside FreeCAD.
It is bundled with the workbench addon for self-contained installation.
"""
from .server import FreecadMCPPlugin
__version__ = "0.6.2" # Updated by release workflow
__all__ = ["FreecadMCPPlugin", "__version__"]
@@ -1,118 +0,0 @@
#!/usr/bin/env python3
r"""Blocking FreeCAD Robust MCP Bridge Server.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This script starts the MCP bridge server and blocks with run_forever().
It works with both freecad (GUI) and freecadcmd (headless) modes.
Use this script when you need FreeCAD to keep running (CI, background servers).
For interactive GUI sessions, use startup_bridge.py instead (non-blocking).
Usage:
# Headless mode (no GUI features):
freecadcmd ~/.local/share/FreeCAD/Mod/FreecadRobustMCPBridge/\
freecad_mcp_bridge/blocking_bridge.py
# GUI mode (full features including screenshots):
freecad ~/.local/share/FreeCAD/Mod/FreecadRobustMCPBridge/\
freecad_mcp_bridge/blocking_bridge.py
# On macOS:
/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd \
~/Library/Application\ Support/FreeCAD/Mod/FreecadRobustMCPBridge/\
freecad_mcp_bridge/blocking_bridge.py
Note: In headless mode (freecadcmd), GUI features like screenshots are not available.
For full functionality, run with the freecad GUI executable.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
# Check if we're running inside FreeCAD
try:
import FreeCAD
print(f"FreeCAD version: {FreeCAD.Version()[0]}.{FreeCAD.Version()[1]}")
except ImportError:
print("ERROR: This script must be run with freecad or freecadcmd.")
print("")
print("Usage:")
print(
" freecadcmd /path/to/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py"
)
print("")
print("On macOS (if workbench installed):")
print(" /Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd \\")
print(
" ~/Library/Application\\ Support/FreeCAD/Mod/FreecadRobustMCPBridge/"
"freecad_mcp_bridge/blocking_bridge.py"
)
print("")
print("On Linux (if workbench installed):")
print(
" freecadcmd ~/.local/share/FreeCAD/Mod/FreecadRobustMCPBridge/"
"freecad_mcp_bridge/blocking_bridge.py"
)
sys.exit(1)
# Import the plugin server directly from the module file in the same directory
script_dir = str(Path(__file__).resolve().parent)
sys.path.insert(0, script_dir)
from bridge_utils import get_running_plugin # noqa: E402
from server import FreecadMCPPlugin # noqa: E402
# Check if bridge is already running (from auto-start in Init.py)
plugin = get_running_plugin()
if plugin is None:
# Get configuration from environment variables (with defaults)
try:
socket_port = int(os.environ.get("FREECAD_SOCKET_PORT", "9876"))
xmlrpc_port = int(os.environ.get("FREECAD_XMLRPC_PORT", "9875"))
except ValueError as e:
print(f"ERROR: Invalid port configuration: {e}")
print("FREECAD_SOCKET_PORT and FREECAD_XMLRPC_PORT must be integers.")
sys.exit(1)
# Create and run the plugin
plugin = FreecadMCPPlugin(
host="localhost",
port=socket_port, # JSON-RPC socket port
xmlrpc_port=xmlrpc_port, # XML-RPC port
enable_xmlrpc=True,
)
# Start the plugin
plugin.start()
# Print status messages with flush to ensure they appear immediately
# (FreeCAD's Python may have buffered stdout)
# Plugin is guaranteed non-None at this point (either from get_running_plugin or created above)
actual_xmlrpc_port = plugin.xmlrpc_port
actual_socket_port = plugin.socket_port
print("", flush=True)
print("=" * 60, flush=True)
gui_mode = "GUI" if FreeCAD.GuiUp else "headless"
print(f"MCP Bridge started in {gui_mode} mode!", flush=True)
print(f" - XML-RPC: localhost:{actual_xmlrpc_port}", flush=True)
print(f" - Socket: localhost:{actual_socket_port}", flush=True)
print("", flush=True)
if not FreeCAD.GuiUp:
print(
"Note: Screenshot and view features are not available in headless mode.",
flush=True,
)
print("Press Ctrl+C to stop.", flush=True)
print("=" * 60, flush=True)
print("", flush=True)
# Run forever (blocks until Ctrl+C)
# Plugin is guaranteed non-None at this point
plugin.run_forever()
@@ -1,272 +0,0 @@
"""Shared utilities for the FreeCAD Robust MCP Bridge.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module provides common functionality used by both blocking_bridge.py,
startup_bridge.py, and Init.py to avoid code duplication.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from collections.abc import Callable
from types import ModuleType
from server import FreecadMCPPlugin
# Default timing constants for GUI waiting
DEFAULT_GUI_CHECK_INTERVAL_MS: int = 100 # How often to check if GUI is ready
DEFAULT_GUI_DEFER_START_MS: int = 2000 # Delay before starting bridge after GUI ready
DEFAULT_GUI_WAIT_MAX_RETRIES: int = 600 # Max retries (600 * 100ms = 60s timeout)
class GuiWaiter:
"""Helper class to wait for FreeCAD GUI to be ready before starting the bridge.
This class encapsulates the logic for waiting for FreeCAD.GuiUp to become True
before invoking a callback. It uses Qt timers to poll the GUI state and defers
the callback after the GUI is ready to allow FreeCAD to fully stabilize.
CRITICAL: Starting the MCP bridge before FreeCAD.GuiUp is True causes the bridge
to use a background thread for queue processing, which leads to crashes when
executing Qt operations from that thread.
Usage:
waiter = GuiWaiter(
callback=my_start_function,
log_prefix="My Component",
)
waiter.start()
The waiter will:
1. Poll FreeCAD.GuiUp every check_interval_ms milliseconds
2. Log progress every 5 seconds
3. Once GuiUp is True, defer the callback by defer_ms milliseconds
4. If timeout is reached, log an error without starting (to prevent crashes)
"""
def __init__(
self,
callback: Callable[[], None],
log_prefix: str = "Bridge",
check_interval_ms: int = DEFAULT_GUI_CHECK_INTERVAL_MS,
defer_ms: int = DEFAULT_GUI_DEFER_START_MS,
max_retries: int = DEFAULT_GUI_WAIT_MAX_RETRIES,
timeout_error_extra: str = "",
) -> None:
"""Initialize the GUI waiter.
Args:
callback: Function to call when GUI is ready (after defer delay).
log_prefix: Prefix for log messages (e.g., "Startup Bridge").
check_interval_ms: How often to check FreeCAD.GuiUp (milliseconds).
defer_ms: Delay after GUI ready before calling callback (milliseconds).
max_retries: Maximum number of check attempts before timeout.
timeout_error_extra: Additional text to include in timeout error message.
"""
self.callback = callback
self.log_prefix = log_prefix
self.check_interval_ms = check_interval_ms
self.defer_ms = defer_ms
self.max_retries = max_retries
self.timeout_error_extra = timeout_error_extra
# Timer references use Any since they could be from PySide2 or PySide6
self._check_timer: Any | None = None
self._defer_timer: Any | None = None
self._retry_count: int = 0
self._qtcore: ModuleType | None = None
def start(self) -> None:
"""Start waiting for GUI to be ready.
This method sets up a repeating timer that checks FreeCAD.GuiUp.
The timer reference is stored to prevent garbage collection.
The QtCore module is resolved once and stored for later use.
"""
import FreeCAD
# Resolve QtCore once and store for later use
try:
from PySide2 import QtCore # type: ignore[import]
except ImportError:
try:
from PySide6 import QtCore # type: ignore[import]
except ImportError:
FreeCAD.Console.PrintError(
f"{self.log_prefix}: Neither PySide2 nor PySide6 is available. "
"Cannot wait for GUI - Qt is required for timer-based waiting.\n"
)
return
self._qtcore = QtCore
self._check_timer = QtCore.QTimer()
self._check_timer.setSingleShot(False) # Repeating timer
self._check_timer.timeout.connect(self._check_gui)
self._check_timer.start(self.check_interval_ms)
FreeCAD.Console.PrintMessage(
f"{self.log_prefix}: Waiting for GUI to be ready...\n"
)
def _check_gui(self) -> None:
"""Check if GUI is ready and handle the result.
Called repeatedly by the check timer. When GUI is ready, stops the timer
and schedules the callback with a defer delay.
"""
import FreeCAD
self._retry_count += 1
# Log progress every 50 checks (5 seconds at default interval)
if self._retry_count % 50 == 0:
elapsed = self._retry_count * (self.check_interval_ms / 1000.0)
FreeCAD.Console.PrintMessage(
f"{self.log_prefix}: Still waiting for GUI... ({elapsed:.1f}s elapsed)\n"
)
if FreeCAD.GuiUp:
self._on_gui_ready()
elif self._retry_count >= self.max_retries:
self._on_timeout()
def _on_gui_ready(self) -> None:
"""Handle GUI becoming ready."""
import FreeCAD
# Stop the check timer
if self._check_timer is not None:
self._check_timer.stop()
self._check_timer = None
elapsed = self._retry_count * (self.check_interval_ms / 1000.0)
FreeCAD.Console.PrintMessage(
f"{self.log_prefix}: GUI ready after {elapsed:.1f}s, "
"deferring bridge start...\n"
)
# IMPORTANT: Don't start the bridge immediately from this timer callback!
# Even though GuiUp is True, FreeCAD may still be initializing internally.
# Use a single-shot timer to defer the actual start to a later, more stable
# point in the event loop.
# Note: self._qtcore was resolved in start() so we don't need to re-import
if self._qtcore is None:
# This should never happen if start() was called, but handle gracefully
FreeCAD.Console.PrintError(
f"{self.log_prefix}: QtCore not initialized - start() was not called\n"
)
return
self._defer_timer = self._qtcore.QTimer()
self._defer_timer.setSingleShot(True)
self._defer_timer.timeout.connect(self.callback)
self._defer_timer.start(self.defer_ms)
def _on_timeout(self) -> None:
"""Handle timeout - GUI did not become ready in time."""
import FreeCAD
# Stop the check timer
if self._check_timer is not None:
self._check_timer.stop()
self._check_timer = None
timeout_seconds = self.max_retries * (self.check_interval_ms / 1000.0)
FreeCAD.Console.PrintError(
f"\n{'=' * 60}\n"
f"{self.log_prefix.upper()} ERROR: GUI did not become ready "
f"within {timeout_seconds:.0f}s!\n"
f"{'=' * 60}\n\n"
f"The bridge was NOT started because starting with a background\n"
f"thread would cause FreeCAD to crash when executing Qt operations.\n\n"
f"Possible causes:\n"
f" - FreeCAD is running in headless mode\n"
f" - FreeCAD GUI initialization is extremely slow\n"
f" - There's an issue with the FreeCAD installation\n"
f"{self.timeout_error_extra}"
f"{'=' * 60}\n"
)
# Do NOT call callback here - it would use background thread and crash
def register_mcp_plugin(
plugin: FreecadMCPPlugin,
xmlrpc_port: int,
socket_port: int,
) -> None:
"""Register an MCP plugin with the workbench commands module.
This centralizes plugin registration so both Init.py auto-start and
startup_bridge.py use the same logic. Registration allows the workbench
to detect if a bridge is already running.
Args:
plugin: The FreecadMCPPlugin instance to register.
xmlrpc_port: The XML-RPC port the plugin is using.
socket_port: The JSON-RPC socket port the plugin is using.
Note:
If the commands module isn't available (workbench not loaded yet),
registration silently fails. The bridge will still work but won't
be visible to the workbench UI.
"""
try:
import commands
commands._mcp_plugin = plugin
commands._running_config = {
"xmlrpc_port": xmlrpc_port,
"socket_port": socket_port,
}
except ImportError:
# Commands module not available (workbench not loaded yet)
pass
def get_running_plugin() -> FreecadMCPPlugin | None:
"""Check if an MCP bridge plugin is already running.
This function checks if the workbench commands module has an active
plugin instance (typically started via auto-start in Init.py).
Returns:
The running FreecadMCPPlugin instance if one exists and is running,
None otherwise.
Note:
This function requires FreeCAD to be available in the environment.
It will print status messages to FreeCAD.Console when a running
plugin is found.
"""
try:
import FreeCAD
except ImportError:
return None
try:
# Check if the workbench commands module has a running plugin
import commands
plugin = getattr(commands, "_mcp_plugin", None)
if plugin is not None and plugin.is_running:
# Get actual ports from running config, with sensible defaults
config = getattr(commands, "_running_config", {})
xmlrpc_port = config.get("xmlrpc_port", 9875)
socket_port = config.get("socket_port", 9876)
FreeCAD.Console.PrintMessage(
"\nMCP Bridge already running (from auto-start).\n"
)
FreeCAD.Console.PrintMessage(f" - XML-RPC: localhost:{xmlrpc_port}\n")
FreeCAD.Console.PrintMessage(f" - Socket: localhost:{socket_port}\n\n")
return plugin
except ImportError:
# Workbench commands module not available
pass
except AttributeError as e:
# _mcp_plugin exists but is malformed (missing is_running, etc.)
FreeCAD.Console.PrintWarning(f"MCP plugin state check failed: {e}\n")
return None
@@ -1,931 +0,0 @@
"""FreeCAD Robust MCP Bridge Plugin - Socket Server with Queue-based Thread Safety.
This module provides a socket server that runs inside FreeCAD to handle
MCP bridge requests. It must be executed within FreeCAD's Python environment.
Design inspired by neka-nat/freecad-mcp (MIT License):
- Queue-based GUI communication for thread safety
- XML-RPC compatibility mode (port 9875)
- Screenshot capture with view type detection
Attribution:
The queue-based thread safety pattern and XML-RPC protocol design were
inspired by neka-nat/freecad-mcp (https://github.com/neka-nat/freecad-mcp),
which is licensed under the MIT License. This implementation is a complete
rewrite with additional features (JSON-RPC 2.0, async socket server).
"""
from __future__ import annotations
import asyncio
import contextlib
import errno
import io
import json
import os
import queue
import sys
import threading
import time
import traceback
import uuid
import xmlrpc.server
from contextlib import redirect_stderr, redirect_stdout
from typing import Any
# These imports only work inside FreeCAD
try:
import FreeCAD
import FreeCADGui
FREECAD_AVAILABLE = True
except ImportError:
FREECAD_AVAILABLE = False
# Default configuration
DEFAULT_SOCKET_PORT = 9876
DEFAULT_XMLRPC_PORT = 9875
QUEUE_POLL_INTERVAL_MS = 50
STATUS_UPDATE_INTERVAL_MS = 5000 # Update status bar every 5 seconds
HEADLESS_POLL_INTERVAL_S = 0.1 # Headless mode poll interval in seconds
def _get_qt_core() -> Any:
"""Get the QtCore module if GUI mode is available.
This helper checks if FreeCAD is available with GUI enabled and
attempts to import QtCore from PySide2 or PySide6.
Returns:
The QtCore module if available in GUI mode, None otherwise.
"""
if not (FREECAD_AVAILABLE and FreeCAD.GuiUp):
return None
# Try PySide2 first, then PySide6
with contextlib.suppress(ImportError):
from PySide2 import QtCore
return QtCore
with contextlib.suppress(ImportError):
from PySide6 import QtCore
return QtCore
return None
class ExecutionRequest:
"""Represents a code execution request."""
def __init__(
self,
code: str,
timeout_ms: int = 30000,
request_id: str | None = None,
) -> None:
"""Initialize execution request.
Args:
code: Python code to execute.
timeout_ms: Execution timeout in milliseconds.
request_id: Optional request ID for tracking.
"""
self.code = code
self.timeout_ms = timeout_ms
self.request_id = request_id
self.result: dict[str, Any] | None = None
self.completed = threading.Event()
class FreecadMCPPlugin:
"""Plugin that runs inside FreeCAD to handle MCP bridge requests.
This class creates servers that accept connections from the MCP server
and executes commands in FreeCAD's context using a thread-safe queue
system for GUI operations.
Attributes:
socket_host: Hostname for socket server.
socket_port: Port for JSON-RPC socket server.
xmlrpc_port: Port for XML-RPC server.
"""
def __init__(
self,
host: str = "localhost",
port: int = DEFAULT_SOCKET_PORT,
xmlrpc_port: int = DEFAULT_XMLRPC_PORT,
enable_xmlrpc: bool = True,
) -> None:
"""Initialize the plugin.
Args:
host: Hostname to bind to.
port: Port for JSON-RPC socket server.
xmlrpc_port: Port for XML-RPC server.
enable_xmlrpc: Whether to enable XML-RPC server.
"""
# Generate unique instance ID for this server
self._instance_id = str(uuid.uuid4())
self._host = host
self._port = port
self._xmlrpc_port = xmlrpc_port
self._enable_xmlrpc = enable_xmlrpc
# Server instances
self._socket_server: asyncio.Server | None = None
self._xmlrpc_server: xmlrpc.server.SimpleXMLRPCServer | None = None
self._socket_loop: asyncio.AbstractEventLoop | None = None
# Threading
self._socket_thread: threading.Thread | None = None
self._xmlrpc_thread: threading.Thread | None = None
self._running = False
# Queue-based execution for thread safety (learned from neka-nat)
self._request_queue: queue.Queue[ExecutionRequest] = queue.Queue()
self._timer = None
self._queue_thread: threading.Thread | None = None
self._headless = False
# Status bar tracking
self._status_timer = None
self._request_count = 0
self._last_request_time: float | None = None
# =========================================================================
# Public API (for external access without using private attributes)
# =========================================================================
@property
def is_running(self) -> bool:
"""Check if the MCP bridge server is currently running.
Returns:
True if the server is running, False otherwise.
"""
return self._running
@property
def instance_id(self) -> str:
"""Get the unique instance ID for this server.
Returns:
UUID string identifying this server instance.
"""
return self._instance_id
@property
def socket_port(self) -> int:
"""Get the JSON-RPC socket server port.
Returns:
Port number for the socket server.
"""
return self._port
@property
def xmlrpc_port(self) -> int:
"""Get the XML-RPC server port.
Returns:
Port number for the XML-RPC server.
"""
return self._xmlrpc_port
@property
def request_count(self) -> int:
"""Get the total number of requests processed.
Returns:
Number of requests processed since server start.
"""
return self._request_count
def get_status(self) -> dict[str, Any]:
"""Get the current status of the MCP bridge server.
Returns:
Dictionary containing:
- running: Whether the server is running
- instance_id: Unique server instance ID
- socket_port: JSON-RPC socket port
- xmlrpc_port: XML-RPC port
- xmlrpc_enabled: Whether XML-RPC is enabled
- request_count: Total requests processed
- last_request_time: Timestamp of last request (or None)
- headless: Whether running in headless mode
"""
return {
"running": self._running,
"instance_id": self._instance_id,
"socket_port": self._port,
"xmlrpc_port": self._xmlrpc_port,
"xmlrpc_enabled": self._enable_xmlrpc,
"request_count": self._request_count,
"last_request_time": self._last_request_time,
"headless": self._headless,
}
def start(self) -> None:
"""Start all servers."""
if self._running:
return
self._running = True
# Print instance ID to stderr only for test automation (when env var is set).
# This avoids red error text in FreeCAD's console during normal use.
if os.environ.get("FREECAD_MCP_TESTING"):
print(
f"FREECAD_MCP_BRIDGE_INSTANCE_ID={self._instance_id}",
file=sys.stderr,
flush=True,
)
# Start the queue processing timer on the main thread
self._start_queue_processor()
# Start socket server
self._socket_thread = threading.Thread(
target=self._run_socket_server,
daemon=True,
name="MCP-Socket",
)
self._socket_thread.start()
# Start XML-RPC server if enabled
if self._enable_xmlrpc:
self._xmlrpc_thread = threading.Thread(
target=self._run_xmlrpc_server,
daemon=True,
name="MCP-XMLRPC",
)
self._xmlrpc_thread.start()
if FREECAD_AVAILABLE:
FreeCAD.Console.PrintMessage(
f"MCP Bridge started (Instance ID: {self._instance_id}):\n"
)
FreeCAD.Console.PrintMessage(f" - JSON-RPC: {self._host}:{self._port}\n")
if self._enable_xmlrpc:
FreeCAD.Console.PrintMessage(
f" - XML-RPC: {self._host}:{self._xmlrpc_port}\n"
)
# Start status bar updates in GUI mode
self._start_status_updates()
def stop(self) -> None:
"""Stop all servers."""
self._running = False
# Stop status bar updates
self._stop_status_updates()
# Stop queue processor timer (GUI mode)
if self._timer:
with contextlib.suppress(Exception):
self._timer.stop()
self._timer = None
# Stop XML-RPC server by closing its socket directly
# This will cause handle_request() to raise an exception and exit
# Keep server reference until thread exits to avoid race condition
if self._xmlrpc_server:
with contextlib.suppress(Exception):
self._xmlrpc_server.socket.close()
# Stop socket server - close the server and stop the event loop
if self._socket_loop and self._socket_server:
self._socket_loop.call_soon_threadsafe(self._socket_server.close)
self._socket_loop.call_soon_threadsafe(self._socket_loop.stop)
# Wait briefly for threads - they're daemon threads so they'll
# be killed when the main thread exits anyway
if self._queue_thread and self._queue_thread.is_alive():
self._queue_thread.join(timeout=0.5)
self._queue_thread = None
if self._socket_thread and self._socket_thread.is_alive():
self._socket_thread.join(timeout=0.5)
self._socket_thread = None
# Wait for XML-RPC thread to exit before clearing server reference
if self._xmlrpc_thread and self._xmlrpc_thread.is_alive():
self._xmlrpc_thread.join(timeout=0.5)
self._xmlrpc_thread = None
# Now safe to clear the server reference
self._xmlrpc_server = None
if FREECAD_AVAILABLE:
FreeCAD.Console.PrintMessage("MCP Bridge stopped\n")
def run_forever(self) -> None:
"""Run the server indefinitely.
This method blocks until interrupted (Ctrl+C) or stop() is called.
Works in both GUI and headless modes:
- GUI mode: Uses Qt event loop to allow timers to fire
- Headless mode: Uses short sleep intervals for responsive shutdown
"""
self.start()
if FREECAD_AVAILABLE:
FreeCAD.Console.PrintMessage("Server running. Press Ctrl+C to stop.\n")
# Check if we're in GUI mode and have Qt available
QtCore = _get_qt_core()
try:
if QtCore is not None:
# GUI mode: use Qt's processEvents to keep the event loop running
# This allows QTimers to fire for queue processing
app = QtCore.QCoreApplication.instance()
if app is not None:
while self._running:
# Process Qt events (including our QTimer callbacks)
app.processEvents()
# Small sleep to prevent busy-waiting
time.sleep(0.01)
else:
# No QApplication - fall back to headless behavior
self._run_forever_headless()
else:
# Headless mode
self._run_forever_headless()
except KeyboardInterrupt:
pass # Normal exit via Ctrl+C
finally:
if FREECAD_AVAILABLE:
FreeCAD.Console.PrintMessage("\nShutting down...\n")
self.stop()
def _run_forever_headless(self) -> None:
"""Run forever in headless mode using short sleep intervals.
Uses a short sleep interval to allow responsive shutdown when
stop() sets _running to False.
"""
while self._running:
# Use short sleep to allow responsive shutdown
# This is more portable than signal.pause() and responds
# quickly when stop() sets _running = False
time.sleep(HEADLESS_POLL_INTERVAL_S)
# =========================================================================
# Status Bar Updates (GUI mode only)
# =========================================================================
def _start_status_updates(self) -> None:
"""Start periodic status bar updates in GUI mode."""
QtCore = _get_qt_core()
if QtCore is None:
return
# Create timer for status updates
timer = QtCore.QTimer()
timer.timeout.connect(self._update_status_bar)
timer.start(STATUS_UPDATE_INTERVAL_MS)
self._status_timer = timer
# Show initial status
self._update_status_bar()
def _stop_status_updates(self) -> None:
"""Stop status bar updates and clear the status."""
if self._status_timer:
with contextlib.suppress(Exception):
self._status_timer.stop()
self._status_timer = None
# Clear status bar message
if FREECAD_AVAILABLE and FreeCAD.GuiUp:
self._set_status_bar("")
def _update_status_bar(self) -> None:
"""Update the FreeCAD status bar with MCP bridge status."""
if not (FREECAD_AVAILABLE and FreeCAD.GuiUp):
return
# Build status message
ports = f"XML-RPC:{self._xmlrpc_port}" if self._enable_xmlrpc else ""
if ports:
ports = f" ({ports})"
if self._request_count > 0:
# Show activity info
if self._last_request_time:
elapsed = time.time() - self._last_request_time
if elapsed < 60:
time_ago = f"{int(elapsed)}s ago"
else:
time_ago = f"{int(elapsed / 60)}m ago"
status = f"🔌 MCP Bridge active{ports} | {self._request_count} requests | last: {time_ago}"
else:
status = f"🔌 MCP Bridge active{ports} | {self._request_count} requests"
else:
status = f"🔌 MCP Bridge running{ports} | waiting for connections..."
self._set_status_bar(status)
def _set_status_bar(self, message: str) -> None:
"""Set the FreeCAD main window status bar message.
Args:
message: Message to display in status bar.
"""
if not (FREECAD_AVAILABLE and FreeCAD.GuiUp):
return
try:
main_window = FreeCADGui.getMainWindow()
if main_window:
status_bar = main_window.statusBar()
if status_bar:
if message:
# Show message persistently (0 = no timeout)
status_bar.showMessage(message, 0)
else:
status_bar.clearMessage()
except Exception:
# Silently ignore status bar errors
pass
def _record_request(self) -> None:
"""Record that a request was processed (for status tracking)."""
self._request_count += 1
self._last_request_time = time.time()
# =========================================================================
# Queue-based Thread Safety (from neka-nat)
# =========================================================================
def _start_queue_processor(self) -> None:
"""Start the queue processor on the main GUI thread or as background thread."""
# Check if we're in GUI mode using FreeCAD.GuiUp
# Note: Qt (PySide) may be available even in headless mode, but without
# a running event loop, Qt timers won't fire. Use GuiUp to detect this.
gui_available = FREECAD_AVAILABLE and FreeCAD.GuiUp
if gui_available:
# GUI mode: use Qt timer for thread-safe GUI operations
try:
from PySide2 import QtCore
except ImportError:
try:
from PySide6 import QtCore
except ImportError:
QtCore = None # type: ignore[assignment]
if QtCore is not None:
timer = QtCore.QTimer()
timer.timeout.connect(self._process_queue)
timer.start(QUEUE_POLL_INTERVAL_MS)
self._timer = timer
return
# Headless mode: use a background thread for queue processing
# In headless mode, there's no GUI thread concern, so direct
# processing in a background thread is safe
self._headless = True
self._queue_thread = threading.Thread(
target=self._run_queue_processor_loop,
daemon=True,
name="MCP-QueueProcessor",
)
self._queue_thread.start()
if FREECAD_AVAILABLE:
FreeCAD.Console.PrintMessage(
"Running in headless mode (queue processor thread started)\n"
)
def _run_queue_processor_loop(self) -> None:
"""Run queue processor in a loop for headless mode."""
while self._running:
self._process_queue()
time.sleep(QUEUE_POLL_INTERVAL_MS / 1000.0)
def _process_queue(self) -> None:
"""Process pending execution requests on the main thread.
This method is called periodically by a Qt timer to ensure
GUI operations happen on the main thread.
"""
while not self._request_queue.empty():
try:
request = self._request_queue.get_nowait()
result = self._execute_code_sync(request.code)
request.result = result
request.completed.set()
# Track request for status bar
self._record_request()
except queue.Empty:
break
except Exception as e:
if FREECAD_AVAILABLE:
FreeCAD.Console.PrintError(f"Queue processing error: {e}\n")
def _execute_via_queue(
self,
code: str,
timeout_ms: int = 30000,
) -> dict[str, Any]:
"""Execute code via the queue system for thread safety.
Args:
code: Python code to execute.
timeout_ms: Execution timeout in milliseconds.
Returns:
Execution result dictionary.
"""
request = ExecutionRequest(code, timeout_ms)
self._request_queue.put(request)
# Wait for completion
if request.completed.wait(timeout=timeout_ms / 1000):
return request.result or {
"success": False,
"error_type": "InternalError",
"error_message": "No result returned",
}
else:
return {
"success": False,
"error_type": "TimeoutError",
"error_message": f"Execution timed out after {timeout_ms}ms",
"execution_time_ms": timeout_ms,
}
def _execute_code_sync(self, code: str) -> dict[str, Any]:
"""Execute Python code synchronously (call on main thread only).
Args:
code: Python code to execute.
Returns:
Execution result dictionary.
"""
start = time.perf_counter()
stdout_capture = io.StringIO()
stderr_capture = io.StringIO()
exec_globals: dict[str, Any] = {
"__builtins__": __builtins__,
}
if FREECAD_AVAILABLE:
exec_globals["FreeCAD"] = FreeCAD
exec_globals["App"] = FreeCAD
exec_globals["FreeCADGui"] = FreeCADGui
exec_globals["Gui"] = FreeCADGui
try:
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
compiled = compile(code, "<mcp>", "exec")
exec(compiled, exec_globals) # noqa: S102
elapsed = (time.perf_counter() - start) * 1000
return {
"success": True,
"result": exec_globals.get("_result_"),
"stdout": stdout_capture.getvalue(),
"stderr": stderr_capture.getvalue(),
"execution_time_ms": elapsed,
}
except Exception as e:
elapsed = (time.perf_counter() - start) * 1000
return {
"success": False,
"result": None,
"stdout": stdout_capture.getvalue(),
"stderr": stderr_capture.getvalue(),
"execution_time_ms": elapsed,
"error_type": type(e).__name__,
"error_message": str(e),
"error_traceback": traceback.format_exc(),
}
# =========================================================================
# Socket Server (JSON-RPC 2.0)
# =========================================================================
def _run_socket_server(self) -> None:
"""Run the asyncio event loop in background thread."""
self._socket_loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._socket_loop)
try:
self._socket_loop.run_until_complete(self._start_socket_server())
self._socket_loop.run_forever()
except OSError as e:
# Server failed to start - mark as not running
self._running = False
if e.errno == errno.EADDRINUSE:
if FREECAD_AVAILABLE:
FreeCAD.Console.PrintWarning(
f"MCP Bridge: JSON-RPC port {self._port} already in use. "
f"Another instance may be running.\n"
)
elif FREECAD_AVAILABLE:
FreeCAD.Console.PrintError(
f"MCP Bridge: Failed to start JSON-RPC server: {e}\n"
)
finally:
self._socket_loop.close()
async def _start_socket_server(self) -> None:
"""Start the TCP server."""
self._socket_server = await asyncio.start_server(
self._handle_socket_client,
self._host,
self._port,
)
async def _handle_socket_client(
self,
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
) -> None:
"""Handle a connected socket client.
Args:
reader: Stream reader for incoming data.
writer: Stream writer for outgoing data.
"""
try:
while self._running:
data = await reader.readline()
if not data:
break
try:
request = json.loads(data.decode("utf-8"))
response = await self._process_jsonrpc_request(request)
except json.JSONDecodeError as e:
response = {
"jsonrpc": "2.0",
"id": None,
"error": {
"code": -32700,
"message": "Parse error",
"data": str(e),
},
}
response_data = json.dumps(response).encode("utf-8") + b"\n"
writer.write(response_data)
await writer.drain()
except Exception as e:
if FREECAD_AVAILABLE:
FreeCAD.Console.PrintError(f"MCP socket error: {e}\n")
finally:
writer.close()
with contextlib.suppress(Exception):
await writer.wait_closed()
async def _process_jsonrpc_request(
self,
request: dict[str, Any],
) -> dict[str, Any]:
"""Process a JSON-RPC 2.0 request.
Args:
request: JSON-RPC request dictionary.
Returns:
JSON-RPC response dictionary.
"""
request_id = request.get("id")
method = request.get("method")
params = request.get("params", {})
# Handle ping specially (no queue needed)
if method == "ping":
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"pong": True,
"timestamp": time.time(),
"instance_id": self._instance_id,
},
}
# Handle get_instance_id specially (no queue needed)
if method == "get_instance_id":
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {"instance_id": self._instance_id},
}
# Handle execute via queue
if method == "execute":
code = params.get("code", "")
timeout_ms = params.get("timeout_ms", 30000)
# Execute via queue for thread safety
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None,
lambda: self._execute_via_queue(code, timeout_ms),
)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": result,
}
# Unknown method
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": -32601,
"message": "Method not found",
"data": f"Unknown method: {method}",
},
}
# =========================================================================
# XML-RPC Server (neka-nat compatible)
# =========================================================================
def _run_xmlrpc_server(self) -> None:
"""Run the XML-RPC server."""
try:
self._xmlrpc_server = xmlrpc.server.SimpleXMLRPCServer(
(self._host, self._xmlrpc_port),
allow_none=True,
logRequests=False,
)
except OSError as e:
if e.errno == errno.EADDRINUSE:
if FREECAD_AVAILABLE:
FreeCAD.Console.PrintWarning(
f"MCP Bridge: XML-RPC port {self._xmlrpc_port} already in use. "
f"Another instance may be running.\n"
)
elif FREECAD_AVAILABLE:
FreeCAD.Console.PrintError(
f"MCP Bridge: Failed to start XML-RPC server: {e}\n"
)
return
# Set a timeout so handle_request() doesn't block forever
# This allows the server to check self._running periodically
self._xmlrpc_server.timeout = 0.5
# Register methods (type: ignore needed - xmlrpc types are overly restrictive)
self._xmlrpc_server.register_function(self._xmlrpc_execute, "execute") # type: ignore[arg-type]
self._xmlrpc_server.register_function(self._xmlrpc_ping, "ping") # type: ignore[arg-type]
self._xmlrpc_server.register_function(
self._xmlrpc_get_instance_id, "get_instance_id"
) # type: ignore[arg-type]
self._xmlrpc_server.register_function(self._xmlrpc_get_view, "get_view") # type: ignore[arg-type]
self._xmlrpc_server.register_introspection_functions()
while self._running:
try:
self._xmlrpc_server.handle_request()
except OSError:
# Socket was closed during shutdown - this is expected
break
def _xmlrpc_ping(self) -> dict[str, Any]:
"""XML-RPC ping handler."""
return {
"pong": True,
"timestamp": time.time(),
"instance_id": self._instance_id,
}
def _xmlrpc_get_instance_id(self) -> dict[str, Any]:
"""XML-RPC get_instance_id handler.
Returns:
Dictionary containing the unique instance ID for this bridge.
"""
return {"instance_id": self._instance_id}
def _xmlrpc_execute(self, code: str) -> dict[str, Any]:
"""XML-RPC execute handler (neka-nat compatible).
Args:
code: Python code to execute.
Returns:
Execution result dictionary.
"""
return self._execute_via_queue(code, 30000)
# Valid view types for screenshot capture
_VALID_VIEW_TYPES = frozenset(
{"FitAll", "Isometric", "Front", "Back", "Top", "Bottom", "Left", "Right"}
)
def _xmlrpc_get_view(
self,
width: int = 800,
height: int = 600,
view_type: str = "Isometric",
) -> dict[str, Any]:
"""XML-RPC get_view handler for screenshots (neka-nat compatible).
Args:
width: Image width.
height: Image height.
view_type: View angle type.
Returns:
Dictionary with base64 image data or error.
"""
# Validate inputs to prevent code injection
# Type hints don't enforce at runtime, so explicit conversion is needed
try:
width = int(width)
height = int(height)
except (ValueError, TypeError) as e:
return {"success": False, "error": f"Invalid dimensions: {e}"}
if view_type not in self._VALID_VIEW_TYPES:
return {
"success": False,
"error": f"Invalid view_type: {view_type}. "
f"Must be one of: {', '.join(sorted(self._VALID_VIEW_TYPES))}",
}
code = f"""
import base64
import tempfile
import os
if not FreeCAD.GuiUp:
_result_ = {{"success": False, "error": "GUI not available"}}
else:
doc = FreeCAD.ActiveDocument
if doc is None:
_result_ = {{"success": False, "error": "No active document"}}
else:
view = FreeCADGui.ActiveDocument.ActiveView
if view is None:
_result_ = {{"success": False, "error": "No active view"}}
else:
# Check view type
view_class = view.__class__.__name__
if view_class not in ["View3DInventor", "View3DInventorPy"]:
_result_ = {{"success": False, "error": f"Cannot capture from {{view_class}}"}}
else:
# Set view angle
view_type = {view_type!r}
if view_type == "FitAll":
view.fitAll()
elif view_type == "Isometric":
view.viewIsometric()
elif view_type == "Front":
view.viewFront()
elif view_type == "Back":
view.viewRear()
elif view_type == "Top":
view.viewTop()
elif view_type == "Bottom":
view.viewBottom()
elif view_type == "Left":
view.viewLeft()
elif view_type == "Right":
view.viewRight()
# Capture screenshot
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
temp_path = f.name
view.saveImage(temp_path, {width}, {height}, "Current")
with open(temp_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode("utf-8")
os.unlink(temp_path)
_result_ = {{
"success": True,
"data": image_data,
"format": "png",
"width": {width},
"height": {height},
}}
"""
result = self._execute_via_queue(code, 30000)
if result.get("success") and result.get("result"):
return result["result"]
return {"success": False, "error": result.get("error_message", "Unknown error")}
# Backwards compatibility
start = FreecadMCPPlugin
@@ -1,232 +0,0 @@
#!/usr/bin/env python3
"""FreeCAD Robust MCP Bridge Startup Script.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This script starts the MCP bridge in FreeCAD GUI mode. It checks if the bridge
is already running (e.g., from workbench auto-start) before starting a new
instance to avoid port conflicts.
CRITICAL: This script waits for FreeCAD.GuiUp to be True before starting the
bridge. If we start when GuiUp is False, the bridge uses a background thread
for queue processing, which causes crashes when executing Qt operations.
Usage:
# Passed as argument to FreeCAD GUI on startup
freecad /path/to/startup_bridge.py
# Or on macOS:
open -a FreeCAD.app --args /path/to/startup_bridge.py
"""
from __future__ import annotations
import contextlib
import os
import sys
import traceback
from pathlib import Path
from typing import Any
# Add the script's directory to sys.path so we can import the server module
script_dir = str(Path(__file__).resolve().parent)
if script_dir not in sys.path:
sys.path.insert(0, script_dir)
# Check if we're running inside FreeCAD
try:
import FreeCAD
except ImportError:
print("ERROR: This script must be run inside FreeCAD.")
print("")
print("Usage:")
print(" freecad /path/to/startup_bridge.py")
print("")
print("Or on macOS:")
print(" open -a FreeCAD.app --args /path/to/startup_bridge.py")
sys.exit(1)
# Global reference to GuiWaiter to prevent garbage collection
_gui_waiter: Any | None = None
def _start_bridge() -> None:
"""Start the MCP bridge if not already running.
This function checks if a bridge is already running (via get_running_plugin)
and only starts a new bridge if none exists. It reads port configuration from
environment variables and registers the plugin with the workbench commands
module for visibility to other components.
Args:
None.
Returns:
None. Early returns if bridge is already running.
Environment Variables:
FREECAD_XMLRPC_PORT: XML-RPC port (default: 9875)
FREECAD_SOCKET_PORT: JSON-RPC socket port (default: 9876)
Raises:
ValueError: If FREECAD_XMLRPC_PORT or FREECAD_SOCKET_PORT contain
non-integer values. The exception is re-raised after logging.
Exception: Any exception from FreecadMCPPlugin initialization or start()
is caught, logged to FreeCAD.Console, and suppressed.
Side Effects:
- Creates and starts a FreecadMCPPlugin instance
- Registers the plugin with the workbench commands module
- Prints status messages to FreeCAD.Console
Example:
This function is typically called via GuiWaiter callback or directly::
_gui_waiter = GuiWaiter(callback=_start_bridge)
_gui_waiter.start()
"""
# Check if bridge is already running (from auto-start in Init.py)
from bridge_utils import get_running_plugin
if get_running_plugin() is not None:
FreeCAD.Console.PrintMessage(
"MCP Bridge already running (started by workbench auto-start)\n"
)
return
try:
from server import FreecadMCPPlugin
# Get configuration from environment variables (with defaults)
try:
socket_port = int(os.environ.get("FREECAD_SOCKET_PORT", "9876"))
xmlrpc_port = int(os.environ.get("FREECAD_XMLRPC_PORT", "9875"))
except ValueError as e:
FreeCAD.Console.PrintError(f"Invalid port configuration: {e}\n")
FreeCAD.Console.PrintError(
"FREECAD_SOCKET_PORT and FREECAD_XMLRPC_PORT must be integers.\n"
)
raise
plugin = FreecadMCPPlugin(
host="localhost",
port=socket_port, # JSON-RPC socket port
xmlrpc_port=xmlrpc_port, # XML-RPC port
enable_xmlrpc=True,
)
plugin.start()
# Register plugin with commands module so Init.py auto-start can see it
# This prevents both scripts from trying to start separate bridges
from bridge_utils import register_mcp_plugin
register_mcp_plugin(plugin, xmlrpc_port, socket_port)
FreeCAD.Console.PrintMessage("\n")
FreeCAD.Console.PrintMessage("=" * 50 + "\n")
FreeCAD.Console.PrintMessage("MCP Bridge started (via startup script)!\n")
FreeCAD.Console.PrintMessage(f" - XML-RPC: localhost:{xmlrpc_port}\n")
FreeCAD.Console.PrintMessage(f" - Socket: localhost:{socket_port}\n")
FreeCAD.Console.PrintMessage(
f" - Mode: {'GUI' if FreeCAD.GuiUp else 'Headless'}\n"
)
FreeCAD.Console.PrintMessage("=" * 50 + "\n\n")
except Exception as e:
FreeCAD.Console.PrintError(f"Failed to start MCP Bridge: {e}\n")
FreeCAD.Console.PrintError(traceback.format_exc())
# Schedule bridge start after FreeCAD finishes loading
# Strategy:
# - If FreeCAD.GuiUp is True: Qt event loop is running, start bridge directly
# - If FreeCAD.GuiUp is False but QApplication exists: FreeCAD GUI is initializing.
# Use GuiWaiter to wait for GuiUp to become True before starting.
# This ensures the bridge uses Qt timer (not background thread) for queue processing.
# - If no QApplication: True headless mode, start bridge directly
#
# IMPORTANT: We check for QApplication.instance() rather than just QtCore availability
# because FreeCAD bundles PySide even in headless mode (freecadcmd), but there's no
# Qt event loop running. Without a QApplication, Qt timers will never fire.
#
# CRITICAL: We must wait for FreeCAD.GuiUp to be True before starting the bridge
# in GUI mode. If we start when GuiUp is False, the bridge's _start_queue_processor()
# will see GuiUp=False and use a background thread. Later, code executed on that
# thread will try to do Qt operations, causing crashes (SIGABRT in QCocoaWindow).
try:
# Try to import Qt and check for running QApplication
QtCore = None
QtWidgets = None
_has_qapp = False
_is_true_headless = False
try:
from PySide2 import QtCore, QtWidgets # type: ignore[assignment, no-redef]
except ImportError:
with contextlib.suppress(ImportError):
from PySide6 import QtCore, QtWidgets # type: ignore[assignment, no-redef]
# Detect GUI mode vs true headless mode
# - True headless (freecadcmd): QCoreApplication exists but NOT QApplication
# - GUI mode early startup: No app yet, or QApplication being initialized
# - GUI mode ready: FreeCAD.GuiUp is True
if QtWidgets is not None and QtCore is not None:
qapp = QtWidgets.QApplication.instance()
if qapp is not None:
_has_qapp = True
else:
# No QApplication - check if QCoreApplication exists
# If QCoreApplication exists but is NOT a QApplication, it's true headless
qcore_app = QtCore.QCoreApplication.instance()
if qcore_app is not None and not isinstance(
qcore_app, QtWidgets.QApplication
):
_is_true_headless = True
# If no app at all, assume early GUI startup (will use GuiWaiter)
FreeCAD.Console.PrintMessage(
f"Startup Bridge: GuiUp={FreeCAD.GuiUp}, "
f"QtCore={'available' if QtCore else 'unavailable'}, "
f"QApp={'running' if _has_qapp else 'none'}, "
f"headless={_is_true_headless}\n"
)
if FreeCAD.GuiUp:
# GUI is already up - start bridge directly
FreeCAD.Console.PrintMessage("Startup Bridge: GUI already up, starting...\n")
_start_bridge()
elif _is_true_headless:
# True headless mode - QCoreApplication exists but not QApplication
# No Qt event loop for GUI, so start bridge directly with background thread
FreeCAD.Console.PrintMessage(
"Startup Bridge: True headless mode (QCoreApplication only), "
"starting directly...\n"
)
_start_bridge()
elif QtCore is not None:
# GUI not ready yet (either QApplication exists or no app yet)
# Use GuiWaiter to wait for GuiUp to become True before starting
# This ensures the bridge uses Qt timer (not background thread) for queue
FreeCAD.Console.PrintMessage(
"Startup Bridge: GUI not ready, using GuiWaiter...\n"
)
from bridge_utils import GuiWaiter
_gui_waiter = GuiWaiter(
callback=_start_bridge,
log_prefix="Startup Bridge",
timeout_error_extra=(
"\nTo start the bridge in headless mode, use:\n"
" just freecad::run-headless\n\n"
),
)
_gui_waiter.start()
else:
# No Qt available at all - unusual state, start directly
FreeCAD.Console.PrintMessage(
"Startup Bridge: No Qt available, starting directly...\n"
)
_start_bridge()
except Exception as e:
FreeCAD.Console.PrintError(f"Startup Bridge: Failed to initialize: {e}\n")
FreeCAD.Console.PrintError(traceback.format_exc())
@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<!-- Transparent background (allows Qt pressed state to show through) -->
<!-- Bridge Structure with light outline for dark background contrast -->
<!-- Bridge deck -->
<rect x="4" y="40" width="56" height="5" rx="1" fill="#8b7355" stroke="#d4c4b0" stroke-width="1.5"/>
<!-- Bridge railings -->
<rect x="4" y="37" width="56" height="2" rx="0.5" fill="#b89c7a" stroke="#d4c4b0" stroke-width="0.5"/>
<!-- Bridge supports/pillars -->
<rect x="8" y="45" width="6" height="8" fill="#7a6450" stroke="#d4c4b0" stroke-width="1"/>
<rect x="50" y="45" width="6" height="8" fill="#7a6450" stroke="#d4c4b0" stroke-width="1"/>
<!-- Bridge arch underneath -->
<path d="M 14 53 Q 32 47 50 53" fill="none" stroke="#7a6450" stroke-width="2.5"/>
<path d="M 14 53 Q 32 47 50 53" fill="none" stroke="#d4c4b0" stroke-width="1" opacity="0.6"/>
<!-- Robot on the bridge (facing RIGHT - same direction as play buttons) -->
<!-- Robot body with light outline -->
<rect x="24" y="18" width="16" height="14" rx="2" fill="#5a9ee8" stroke="#e0f0ff" stroke-width="1.5"/>
<!-- Robot head (facing right) -->
<rect x="26" y="8" width="12" height="10" rx="2" fill="#5a9ee8" stroke="#e0f0ff" stroke-width="1.5"/>
<!-- Robot face plate (on right side) -->
<rect x="32" y="10" width="6" height="6" rx="1" fill="#f0f8ff" stroke="#c0e0ff" stroke-width="0.5"/>
<!-- Robot eyes (looking right) -->
<circle cx="36" cy="12" r="1.5" fill="#2c5aa0"/>
<circle cx="36" cy="15" r="1.5" fill="#2c5aa0"/>
<!-- Robot antenna -->
<line x1="32" y1="8" x2="32" y2="3" stroke="#3c6ab0" stroke-width="2" stroke-linecap="round"/>
<circle cx="32" cy="2" r="3" fill="#2ecc71" stroke="#e0ffe0" stroke-width="1"/>
<!-- Robot arms (reaching forward to right) -->
<rect x="18" y="22" width="6" height="4" rx="1.5" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5"/>
<rect x="40" y="20" width="8" height="4" rx="1.5" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5"/>
<!-- Robot legs (walking pose) -->
<rect x="24" y="32" width="4" height="8" rx="1" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5" transform="rotate(10, 26, 36)"/>
<rect x="36" y="32" width="4" height="8" rx="1" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5" transform="rotate(-10, 38, 36)"/>
<!-- Bold Play Triangles in TOP corners with light outline -->
<polygon points="4,4 4,16 12,10" fill="#2ecc71" stroke="#c0ffc0" stroke-width="1.5"/>
<polygon points="52,4 52,16 60,10" fill="#2ecc71" stroke="#c0ffc0" stroke-width="1.5"/>
<!-- Green Data Flow (river under bridge - active) -->
<!-- Data stream background -->
<path d="M 0 60 Q 32 55 64 60" fill="none" stroke="#2ecc71" stroke-width="4" opacity="0.4"/>
<!-- Left arrow -->
<path d="M 6 59 L 16 59" stroke="#2ecc71" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="6,59 11,56 11,62" fill="#2ecc71" stroke="#c0ffc0" stroke-width="0.5"/>
<!-- Right arrow -->
<path d="M 48 59 L 58 59" stroke="#2ecc71" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="58,59 53,56 53,62" fill="#2ecc71" stroke="#c0ffc0" stroke-width="0.5"/>
<!-- Center data dots (flowing) -->
<circle cx="26" cy="57" r="2" fill="#2ecc71" stroke="#c0ffc0" stroke-width="0.5"/>
<circle cx="32" cy="56" r="2" fill="#2ecc71" stroke="#c0ffc0" stroke-width="0.5"/>
<circle cx="38" cy="57" r="2" fill="#2ecc71" stroke="#c0ffc0" stroke-width="0.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<!-- Transparent background (allows Qt pressed state to show through) -->
<!-- Bridge Structure with light outline -->
<!-- Bridge deck -->
<rect x="4" y="40" width="56" height="5" rx="1" fill="#8b7355" stroke="#d4c4b0" stroke-width="1.5"/>
<!-- Bridge railings -->
<rect x="4" y="37" width="56" height="2" rx="0.5" fill="#b89c7a" stroke="#d4c4b0" stroke-width="0.5"/>
<!-- Bridge supports/pillars -->
<rect x="8" y="45" width="6" height="8" fill="#7a6450" stroke="#d4c4b0" stroke-width="1"/>
<rect x="50" y="45" width="6" height="8" fill="#7a6450" stroke="#d4c4b0" stroke-width="1"/>
<!-- Bridge arch underneath -->
<path d="M 14 53 Q 32 47 50 53" fill="none" stroke="#7a6450" stroke-width="2.5"/>
<path d="M 14 53 Q 32 47 50 53" fill="none" stroke="#d4c4b0" stroke-width="1" opacity="0.6"/>
<!-- Robot SITTING on bridge edge, feet dangling -->
<!-- Robot body (sitting position - shorter/compressed) with light outline -->
<rect x="24" y="24" width="16" height="10" rx="2" fill="#5a9ee8" stroke="#e0f0ff" stroke-width="1.5"/>
<!-- Robot head with light outline -->
<rect x="26" y="14" width="12" height="10" rx="2" fill="#5a9ee8" stroke="#e0f0ff" stroke-width="1.5"/>
<!-- Robot face plate -->
<rect x="28" y="16" width="8" height="6" rx="1" fill="#f0f8ff" stroke="#c0e0ff" stroke-width="0.5"/>
<!-- Robot eyes (looking curious) -->
<circle cx="30" cy="19" r="1.5" fill="#2c5aa0"/>
<circle cx="34" cy="19" r="1.5" fill="#2c5aa0"/>
<!-- Robot antenna with YELLOW ball (matches status color) with light outline -->
<line x1="32" y1="14" x2="32" y2="9" stroke="#3c6ab0" stroke-width="2" stroke-linecap="round"/>
<circle cx="32" cy="8" r="3" fill="#f1c40f" stroke="#fff8c0" stroke-width="1"/>
<!-- Robot arms (resting on bridge) -->
<rect x="18" y="28" width="6" height="4" rx="1.5" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5"/>
<rect x="40" y="28" width="6" height="4" rx="1.5" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5"/>
<!-- Robot legs (dangling over edge) -->
<rect x="26" y="34" width="4" height="12" rx="1" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5"/>
<rect x="34" y="34" width="4" height="12" rx="1" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5"/>
<!-- Robot feet -->
<rect x="25" y="45" width="6" height="3" rx="1" fill="#3c6ab0" stroke="#c0e0ff" stroke-width="0.5"/>
<rect x="33" y="45" width="6" height="3" rx="1" fill="#3c6ab0" stroke="#c0e0ff" stroke-width="0.5"/>
<!-- Bold Question marks in TOP corners with light stroke -->
<text x="9" y="16" font-family="Arial, Helvetica, sans-serif" font-size="18" font-weight="900" fill="#f1c40f" stroke="#fff8c0" stroke-width="1.5" text-anchor="middle">?</text>
<text x="55" y="16" font-family="Arial, Helvetica, sans-serif" font-size="18" font-weight="900" fill="#f1c40f" stroke="#fff8c0" stroke-width="1.5" text-anchor="middle">?</text>
<!-- Yellow Data Flow (river under bridge - status check) -->
<!-- Data stream background -->
<path d="M 0 60 Q 32 55 64 60" fill="none" stroke="#f1c40f" stroke-width="4" opacity="0.4"/>
<!-- Left arrow -->
<path d="M 6 59 L 16 59" stroke="#f1c40f" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="6,59 11,56 11,62" fill="#f1c40f" stroke="#fff8c0" stroke-width="0.5"/>
<!-- Right arrow -->
<path d="M 48 59 L 58 59" stroke="#f1c40f" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="58,59 53,56 53,62" fill="#f1c40f" stroke="#fff8c0" stroke-width="0.5"/>
<!-- Center data dots (checking) -->
<circle cx="26" cy="57" r="2" fill="#f1c40f" stroke="#fff8c0" stroke-width="0.5"/>
<circle cx="32" cy="56" r="2" fill="#f1c40f" stroke="#fff8c0" stroke-width="0.5"/>
<circle cx="38" cy="57" r="2" fill="#f1c40f" stroke="#fff8c0" stroke-width="0.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<!-- Transparent background (allows Qt pressed state to show through) -->
<!-- Bold Stop Squares in TOP corners with light outline -->
<rect x="2" y="2" width="14" height="14" rx="2" fill="#e74c3c" stroke="#ffc0c0" stroke-width="1.5"/>
<rect x="48" y="2" width="14" height="14" rx="2" fill="#e74c3c" stroke="#ffc0c0" stroke-width="1.5"/>
<!-- DRAWBRIDGE OPEN - No robot -->
<!-- Left bridge section (raised) with light outline -->
<g transform="rotate(-35, 4, 40)">
<rect x="4" y="40" width="26" height="5" rx="1" fill="#8b7355" stroke="#d4c4b0" stroke-width="1.5"/>
<rect x="4" y="37" width="26" height="2" rx="0.5" fill="#b89c7a" stroke="#d4c4b0" stroke-width="0.5"/>
</g>
<!-- Right bridge section (raised) with light outline -->
<g transform="rotate(35, 60, 40)">
<rect x="34" y="40" width="26" height="5" rx="1" fill="#8b7355" stroke="#d4c4b0" stroke-width="1.5"/>
<rect x="34" y="37" width="26" height="2" rx="0.5" fill="#b89c7a" stroke="#d4c4b0" stroke-width="0.5"/>
</g>
<!-- Bridge supports/pillars with light outline -->
<rect x="8" y="45" width="6" height="8" fill="#7a6450" stroke="#d4c4b0" stroke-width="1"/>
<rect x="50" y="45" width="6" height="8" fill="#7a6450" stroke="#d4c4b0" stroke-width="1"/>
<!-- Bridge arch underneath (broken/gap) -->
<path d="M 14 53 Q 20 49 26 50" fill="none" stroke="#7a6450" stroke-width="2.5"/>
<path d="M 14 53 Q 20 49 26 50" fill="none" stroke="#d4c4b0" stroke-width="1" opacity="0.6"/>
<path d="M 38 50 Q 44 49 50 53" fill="none" stroke="#7a6450" stroke-width="2.5"/>
<path d="M 38 50 Q 44 49 50 53" fill="none" stroke="#d4c4b0" stroke-width="1" opacity="0.6"/>
<!-- Red Data Flow (river under bridge - stopped) -->
<!-- Data stream background -->
<path d="M 0 60 Q 32 55 64 60" fill="none" stroke="#e74c3c" stroke-width="4" opacity="0.4"/>
<!-- Left arrow (blocked) -->
<path d="M 6 59 L 16 59" stroke="#e74c3c" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="6,59 11,56 11,62" fill="#e74c3c" stroke="#ffc0c0" stroke-width="0.5"/>
<!-- Right arrow (blocked) -->
<path d="M 48 59 L 58 59" stroke="#e74c3c" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="58,59 53,56 53,62" fill="#e74c3c" stroke="#ffc0c0" stroke-width="0.5"/>
<!-- Center X (flow blocked) -->
<line x1="28" y1="55" x2="36" y2="61" stroke="#e74c3c" stroke-width="3" stroke-linecap="round"/>
<line x1="36" y1="55" x2="28" y2="61" stroke="#e74c3c" stroke-width="3" stroke-linecap="round"/>
<line x1="28" y1="55" x2="36" y2="61" stroke="#ffc0c0" stroke-width="1" stroke-linecap="round" opacity="0.6"/>
<line x1="36" y1="55" x2="28" y2="61" stroke="#ffc0c0" stroke-width="1" stroke-linecap="round" opacity="0.6"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

@@ -1,84 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<!-- Transparent background (allows Qt pressed state to show through) -->
<!-- Bridge Structure with light outline -->
<!-- Bridge deck -->
<rect x="4" y="40" width="56" height="5" rx="1" fill="#8b7355" stroke="#d4c4b0" stroke-width="1.5"/>
<!-- Bridge railings -->
<rect x="4" y="37" width="56" height="2" rx="0.5" fill="#b89c7a" stroke="#d4c4b0" stroke-width="0.5"/>
<!-- Bridge supports/pillars -->
<rect x="8" y="45" width="6" height="8" fill="#7a6450" stroke="#d4c4b0" stroke-width="1"/>
<rect x="50" y="45" width="6" height="8" fill="#7a6450" stroke="#d4c4b0" stroke-width="1"/>
<!-- Bridge arch underneath -->
<path d="M 14 53 Q 32 47 50 53" fill="none" stroke="#7a6450" stroke-width="2.5"/>
<path d="M 14 53 Q 32 47 50 53" fill="none" stroke="#d4c4b0" stroke-width="1" opacity="0.6"/>
<!-- Robot SITTING on bridge edge, feet dangling with light outline -->
<!-- Robot body (sitting position - shorter/compressed) -->
<rect x="24" y="24" width="16" height="10" rx="2" fill="#5a9ee8" stroke="#e0f0ff" stroke-width="1.5"/>
<!-- Robot head -->
<rect x="26" y="14" width="12" height="10" rx="2" fill="#5a9ee8" stroke="#e0f0ff" stroke-width="1.5"/>
<!-- Robot face plate -->
<rect x="28" y="16" width="8" height="6" rx="1" fill="#f0f8ff" stroke="#c0e0ff" stroke-width="0.5"/>
<!-- Robot eyes -->
<circle cx="30" cy="19" r="1.5" fill="#2c5aa0"/>
<circle cx="34" cy="19" r="1.5" fill="#2c5aa0"/>
<!-- Robot antenna with GRAY ball (matches gear color) with light outline -->
<line x1="32" y1="14" x2="32" y2="9" stroke="#3c6ab0" stroke-width="2" stroke-linecap="round"/>
<circle cx="32" cy="8" r="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="1"/>
<!-- Robot arms (resting on bridge) -->
<rect x="18" y="28" width="6" height="4" rx="1.5" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5"/>
<rect x="40" y="28" width="6" height="4" rx="1.5" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5"/>
<!-- Robot legs (dangling over edge) -->
<rect x="26" y="34" width="4" height="12" rx="1" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5"/>
<rect x="34" y="34" width="4" height="12" rx="1" fill="#4a8bd8" stroke="#c0e0ff" stroke-width="0.5"/>
<!-- Robot feet -->
<rect x="25" y="45" width="6" height="3" rx="1" fill="#3c6ab0" stroke="#c0e0ff" stroke-width="0.5"/>
<rect x="33" y="45" width="6" height="3" rx="1" fill="#3c6ab0" stroke="#c0e0ff" stroke-width="0.5"/>
<!-- Better Gear symbols in TOP corners with light outlines -->
<!-- Left gear -->
<circle cx="10" cy="10" r="6" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="1.5"/>
<!-- Gear teeth (8 teeth around the gear) -->
<rect x="8" y="2" width="4" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<rect x="8" y="15" width="4" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<rect x="2" y="8" width="3" height="4" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<rect x="15" y="8" width="3" height="4" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<!-- Diagonal teeth -->
<rect x="3" y="3" width="3" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5" transform="rotate(45, 4.5, 4.5)"/>
<rect x="14" y="3" width="3" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5" transform="rotate(-45, 15.5, 4.5)"/>
<rect x="3" y="14" width="3" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5" transform="rotate(-45, 4.5, 15.5)"/>
<rect x="14" y="14" width="3" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5" transform="rotate(45, 15.5, 15.5)"/>
<!-- Gear center hole -->
<circle cx="10" cy="10" r="2.5" fill="#7a6450" stroke="#d4c4b0" stroke-width="0.5"/>
<!-- Right gear -->
<circle cx="54" cy="10" r="6" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="1.5"/>
<!-- Gear teeth (8 teeth around the gear) -->
<rect x="52" y="2" width="4" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<rect x="52" y="15" width="4" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<rect x="46" y="8" width="3" height="4" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<rect x="59" y="8" width="3" height="4" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<!-- Diagonal teeth -->
<rect x="47" y="3" width="3" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5" transform="rotate(45, 48.5, 4.5)"/>
<rect x="58" y="3" width="3" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5" transform="rotate(-45, 59.5, 4.5)"/>
<rect x="47" y="14" width="3" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5" transform="rotate(-45, 48.5, 15.5)"/>
<rect x="58" y="14" width="3" height="3" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5" transform="rotate(45, 59.5, 15.5)"/>
<!-- Gear center hole -->
<circle cx="54" cy="10" r="2.5" fill="#7a6450" stroke="#d4c4b0" stroke-width="0.5"/>
<!-- Gray Data Flow (river under bridge - settings) -->
<!-- Data stream background -->
<path d="M 0 60 Q 32 55 64 60" fill="none" stroke="#a8b8c8" stroke-width="4" opacity="0.4"/>
<!-- Left arrow -->
<path d="M 6 59 L 16 59" stroke="#a8b8c8" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="6,59 11,56 11,62" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<!-- Right arrow -->
<path d="M 48 59 L 58 59" stroke="#a8b8c8" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="58,59 53,56 53,62" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<!-- Center data dots -->
<circle cx="26" cy="57" r="2" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<circle cx="32" cy="56" r="2" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
<circle cx="38" cy="57" r="2" fill="#a8b8c8" stroke="#d0e0f0" stroke-width="0.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

-119
View File
@@ -1,119 +0,0 @@
"""Shared path utilities for the Robust MCP Bridge addon.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module provides centralized path-finding functions for locating
the addon directory, icons, and other resources. All path-related
logic is consolidated here to avoid duplication across modules.
NOTE: Using os.path instead of pathlib throughout this module due to
FreeCAD's module loading behavior which can have issues with some
Python features at load time.
"""
from __future__ import annotations
import os # noqa: PTH
import FreeCAD
# Addon directory name - single source of truth for renames
_ADDON_DIRNAME = "FreecadRobustMCPBridge"
# Cache for addon path to avoid repeated filesystem lookups
_addon_path_cache: str | None = None
def get_addon_path() -> str:
"""Get the path to this addon's directory.
Uses multiple fallback methods to locate the addon directory:
1. __file__ if available
2. FreeCAD's Mod path + addon name
3. Versioned FreeCAD directory (FreeCAD 1.x: v1-*)
Returns:
The absolute path to the addon directory, or empty string if not found.
Once found, the path is cached for subsequent calls.
"""
global _addon_path_cache
if _addon_path_cache is not None:
return _addon_path_cache
# Method 1: Try __file__
try:
_addon_path_cache = os.path.dirname(os.path.abspath(__file__)) # noqa: PTH100, PTH120
return _addon_path_cache
except NameError:
pass
# Method 2: Use FreeCAD's Mod path + our addon name
try:
mod_path = os.path.join( # noqa: PTH118
FreeCAD.getUserAppDataDir(), "Mod", _ADDON_DIRNAME
)
if os.path.exists(mod_path): # noqa: PTH110
_addon_path_cache = mod_path
return _addon_path_cache
except (OSError, PermissionError) as e:
FreeCAD.Console.PrintWarning(f"Could not access Mod directory: {e}\n")
# Method 3: Try versioned FreeCAD directory (FreeCAD 1.x)
try:
base_path = FreeCAD.getUserAppDataDir()
for item in os.listdir(base_path): # noqa: PTH208
if item.startswith("v1-"):
versioned_mod = os.path.join( # noqa: PTH118
base_path, item, "Mod", _ADDON_DIRNAME
)
if os.path.exists(versioned_mod): # noqa: PTH110
_addon_path_cache = versioned_mod
return _addon_path_cache
except (OSError, PermissionError) as e:
FreeCAD.Console.PrintWarning(f"Could not scan versioned directories: {e}\n")
return ""
def get_icon_path(icon_name: str) -> str:
"""Get the full path to an icon file.
Args:
icon_name: The icon filename or relative path (e.g., "icons/mcp_start.svg")
Returns:
The absolute path to the icon file, or empty string if addon path not found.
"""
addon_path = get_addon_path()
if not addon_path:
return ""
return os.path.join(addon_path, icon_name) # noqa: PTH118
def get_icons_dir() -> str:
"""Get the path to the addon's icons directory.
Returns:
The absolute path to the icons directory, or empty string if not found.
"""
addon_path = get_addon_path()
if addon_path:
icons_dir = os.path.join(addon_path, "icons") # noqa: PTH118
if os.path.isdir(icons_dir): # noqa: PTH112
return icons_dir
return ""
def get_workbench_icon() -> str:
"""Get the path to the workbench's main icon (FreecadRobustMCPBridge.svg).
Returns:
The absolute path to the workbench icon, or empty string if not found.
"""
addon_path = get_addon_path()
if addon_path:
icon_path = os.path.join(addon_path, f"{_ADDON_DIRNAME}.svg") # noqa: PTH118
if os.path.exists(icon_path): # noqa: PTH110
return icon_path
return ""
-150
View File
@@ -1,150 +0,0 @@
"""Preferences management for the Robust MCP Bridge workbench.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module handles reading and writing workbench preferences using
FreeCAD's parameter system.
NOTE: Using os.path instead of pathlib throughout this module due to
FreeCAD's module loading behavior which can have issues with some
Python features at load time.
"""
from __future__ import annotations
from typing import TypedDict
import FreeCAD
class PreferencesDict(TypedDict):
"""Type definition for the preferences dictionary."""
auto_start: bool
status_bar_enabled: bool
xmlrpc_port: int
socket_port: int
# Parameter path for our workbench preferences
PARAM_PATH = "User parameter:BaseApp/Preferences/Mod/RobustMCPBridge"
# Default values
DEFAULT_AUTO_START = False
DEFAULT_STATUS_BAR_ENABLED = True
DEFAULT_XMLRPC_PORT = 9875
DEFAULT_SOCKET_PORT = 9876
def get_param() -> FreeCAD.ParameterGrp:
"""Get the parameter group for our preferences."""
return FreeCAD.ParamGet(PARAM_PATH)
def get_auto_start() -> bool:
"""Get whether the bridge should auto-start when FreeCAD launches.
Returns:
True if bridge should auto-start, False otherwise.
Default: False
"""
return get_param().GetBool("AutoStart", DEFAULT_AUTO_START)
def set_auto_start(enabled: bool) -> None:
"""Set whether the bridge should auto-start when FreeCAD launches.
Args:
enabled: True to enable auto-start, False to disable.
"""
get_param().SetBool("AutoStart", enabled)
def get_status_bar_enabled() -> bool:
"""Get whether the status bar indicator is enabled.
Returns:
True if status bar indicator should be shown, False otherwise.
Default: True
"""
return get_param().GetBool("StatusBarEnabled", DEFAULT_STATUS_BAR_ENABLED)
def set_status_bar_enabled(enabled: bool) -> None:
"""Set whether the status bar indicator is enabled.
Args:
enabled: True to show status bar indicator, False to hide.
"""
get_param().SetBool("StatusBarEnabled", enabled)
def get_xmlrpc_port() -> int:
"""Get the XML-RPC port number.
Returns:
Port number for XML-RPC server.
Default: 9875
"""
return get_param().GetInt("XMLRPCPort", DEFAULT_XMLRPC_PORT)
def set_xmlrpc_port(port: int) -> None:
"""Set the XML-RPC port number.
Args:
port: Port number for XML-RPC server (1024-65535).
Raises:
ValueError: If port is out of valid range.
"""
if not 1024 <= port <= 65535:
raise ValueError(f"Port must be between 1024 and 65535, got {port}")
get_param().SetInt("XMLRPCPort", port)
def get_socket_port() -> int:
"""Get the JSON-RPC socket port number.
Returns:
Port number for JSON-RPC socket server.
Default: 9876
"""
return get_param().GetInt("SocketPort", DEFAULT_SOCKET_PORT)
def set_socket_port(port: int) -> None:
"""Set the JSON-RPC socket port number.
Args:
port: Port number for JSON-RPC socket server (1024-65535).
Raises:
ValueError: If port is out of valid range.
"""
if not 1024 <= port <= 65535:
raise ValueError(f"Port must be between 1024 and 65535, got {port}")
get_param().SetInt("SocketPort", port)
def get_all_preferences() -> PreferencesDict:
"""Get all preferences as a dictionary.
Returns:
Dictionary with all preference values.
"""
return {
"auto_start": get_auto_start(),
"status_bar_enabled": get_status_bar_enabled(),
"xmlrpc_port": get_xmlrpc_port(),
"socket_port": get_socket_port(),
}
def reset_to_defaults() -> None:
"""Reset all preferences to their default values."""
set_auto_start(DEFAULT_AUTO_START)
set_status_bar_enabled(DEFAULT_STATUS_BAR_ENABLED)
set_xmlrpc_port(DEFAULT_XMLRPC_PORT)
set_socket_port(DEFAULT_SOCKET_PORT)
@@ -1,209 +0,0 @@
"""Preferences page for FreeCAD Preferences dialog integration.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module provides a QWidget-based preferences page that integrates
with FreeCAD's main Preferences dialog (Edit → Preferences).
NOTE: This is separate from the preferences.py module which handles
the actual preference storage. This module only handles the UI.
"""
from __future__ import annotations
from PySide import QtCore, QtWidgets # type: ignore[import-not-found]
class MCPBridgePreferencesPage(QtWidgets.QWidget):
"""Preferences page for FreeCAD's Preferences dialog.
This widget appears in the FreeCAD Preferences dialog sidebar
when registered via FreeCADGui.addPreferencePage().
Required methods:
- loadSettings(): Load preferences into widgets
- saveSettings(): Save widget values to preferences
"""
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
"""Initialize the preferences page widget."""
super().__init__(parent)
# Set window title - this appears in the preferences tree under the category
self.setWindowTitle("General")
self._setup_ui()
def _setup_ui(self) -> None:
"""Set up the user interface."""
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(10, 10, 10, 10)
# Title
title = QtWidgets.QLabel("<h2>Robust MCP Bridge</h2>")
layout.addWidget(title)
description = QtWidgets.QLabel(
"Configure the MCP Bridge for AI assistant integration with FreeCAD."
)
description.setWordWrap(True)
layout.addWidget(description)
layout.addSpacing(10)
# Startup group
startup_group = QtWidgets.QGroupBox("Startup")
startup_layout = QtWidgets.QVBoxLayout(startup_group)
self.auto_start_cb = QtWidgets.QCheckBox(
"Auto-start bridge when FreeCAD launches"
)
self.auto_start_cb.setToolTip(
"Automatically start the MCP bridge server when FreeCAD starts.\n"
"The bridge allows AI assistants like Claude to control FreeCAD."
)
startup_layout.addWidget(self.auto_start_cb)
layout.addWidget(startup_group)
# Display group
display_group = QtWidgets.QGroupBox("Display")
display_layout = QtWidgets.QVBoxLayout(display_group)
self.status_bar_cb = QtWidgets.QCheckBox("Show status indicator in status bar")
self.status_bar_cb.setToolTip(
"Display MCP bridge connection status in FreeCAD's status bar."
)
display_layout.addWidget(self.status_bar_cb)
layout.addWidget(display_group)
# Network Ports group
ports_group = QtWidgets.QGroupBox("Network Ports")
ports_layout = QtWidgets.QFormLayout(ports_group)
self.xmlrpc_spin = QtWidgets.QSpinBox()
self.xmlrpc_spin.setRange(1024, 65535)
self.xmlrpc_spin.setToolTip(
"Port for XML-RPC connections.\n"
"Default: 9875\n\n"
"The MCP server connects to this port to communicate with FreeCAD."
)
ports_layout.addRow("XML-RPC Port:", self.xmlrpc_spin)
self.socket_spin = QtWidgets.QSpinBox()
self.socket_spin.setRange(1024, 65535)
self.socket_spin.setToolTip(
"Port for JSON-RPC socket connections.\n"
"Default: 9876\n\n"
"Alternative connection method using raw sockets."
)
ports_layout.addRow("Socket Port:", self.socket_spin)
# Warning about restart
port_warning = QtWidgets.QLabel(
"<i>Note: Changing ports requires restarting the bridge.</i>"
)
port_warning.setWordWrap(True)
ports_layout.addRow(port_warning)
layout.addWidget(ports_group)
# Server Configuration info
server_group = QtWidgets.QGroupBox("MCP Server Configuration")
server_layout = QtWidgets.QVBoxLayout(server_group)
server_intro = QtWidgets.QLabel(
"The external MCP server (used by Claude Code, etc.) is configured "
"separately using environment variables:"
)
server_intro.setWordWrap(True)
server_layout.addWidget(server_intro)
server_layout.addSpacing(5)
# Environment variables as a form layout for better alignment
env_layout = QtWidgets.QFormLayout()
env_layout.setLabelAlignment(QtCore.Qt.AlignRight)
env_vars = [
("FREECAD_XMLRPC_PORT", "XML-RPC port (default: 9875)"),
("FREECAD_SOCKET_PORT", "JSON-RPC socket port (default: 9876)"),
("FREECAD_MODE", "Connection mode: xmlrpc, socket, or embedded"),
("FREECAD_SOCKET_HOST", "Server hostname (default: localhost)"),
]
for var_name, description in env_vars:
var_label = QtWidgets.QLabel(f"<code>{var_name}</code>")
var_label.setTextFormat(QtCore.Qt.RichText)
desc_label = QtWidgets.QLabel(description)
env_layout.addRow(var_label, desc_label)
server_layout.addLayout(env_layout)
server_layout.addSpacing(5)
server_note = QtWidgets.QLabel(
"<i>Ensure these match the ports configured above.</i>"
)
server_note.setTextFormat(QtCore.Qt.RichText)
server_layout.addWidget(server_note)
layout.addWidget(server_group)
# Add stretch to push everything to the top
layout.addStretch()
def loadSettings(self) -> None:
"""Load settings from FreeCAD preferences into widgets.
This method is called by FreeCAD when the Preferences dialog opens.
"""
# Import here to avoid circular imports and ensure module is available
from preferences import (
get_auto_start,
get_socket_port,
get_status_bar_enabled,
get_xmlrpc_port,
)
self.auto_start_cb.setChecked(get_auto_start())
self.status_bar_cb.setChecked(get_status_bar_enabled())
self.xmlrpc_spin.setValue(get_xmlrpc_port())
self.socket_spin.setValue(get_socket_port())
def saveSettings(self) -> None:
"""Save settings from widgets to FreeCAD preferences.
This method is called by FreeCAD when OK or Apply is clicked.
"""
from preferences import (
get_socket_port,
get_xmlrpc_port,
set_auto_start,
set_socket_port,
set_status_bar_enabled,
set_xmlrpc_port,
)
# Track if ports changed for potential restart
old_xmlrpc = get_xmlrpc_port()
old_socket = get_socket_port()
# Save all preferences
set_auto_start(self.auto_start_cb.isChecked())
set_status_bar_enabled(self.status_bar_cb.isChecked())
set_xmlrpc_port(self.xmlrpc_spin.value())
set_socket_port(self.socket_spin.value())
# Check if ports changed and notify about restart if needed
new_xmlrpc = self.xmlrpc_spin.value()
new_socket = self.socket_spin.value()
if old_xmlrpc != new_xmlrpc or old_socket != new_socket:
# Import FreeCAD here to avoid issues at module load time
import FreeCAD
FreeCAD.Console.PrintMessage(
"MCP Bridge ports changed. "
"If the bridge is running, restart it for changes to take effect.\n"
)
@@ -1,358 +0,0 @@
"""Status bar widget for MCP Bridge status display.
SPDX-License-Identifier: MIT
Copyright (c) 2025 Sean P. Kane (GitHub: spkane)
This module provides a permanent status widget for FreeCAD's status bar
that shows the current MCP bridge connection status without being
overwritten by other FreeCAD messages.
NOTE: All GUI operations in this module MUST be performed on the main Qt thread.
The functions in this module check for thread safety and will silently return
if called from a non-main thread to prevent crashes.
"""
from __future__ import annotations
import threading
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from PySide import QtWidgets
# Global reference to the status widget (protected by _status_widget_lock)
_status_widget: MCPStatusWidget | None = None
_status_widget_lock = threading.Lock()
def _is_main_thread() -> bool:
"""Check if the current thread is the main Qt/GUI thread.
Uses Qt's QApplication.instance().thread() to reliably detect the main thread,
rather than relying on which thread first imports this module.
Returns:
True if on main thread, False otherwise.
"""
try:
# Try to import Qt (PySide6 first, then PySide2 as fallback)
try:
from PySide6 import QtCore, QtWidgets
except ImportError:
from PySide2 import QtCore, QtWidgets
# Get the QApplication instance
app = QtWidgets.QApplication.instance()
if app is None:
# No QApplication - can't determine main thread, assume safe
return True
# Check if current thread is the application's main thread
return QtCore.QThread.currentThread() == app.thread()
except Exception:
# If Qt check fails, fall back to threading module check
# This is less reliable but better than nothing
current_thread = threading.current_thread()
return current_thread is threading.main_thread()
def _check_main_thread(operation: str) -> bool:
"""Check if we're on the main thread and log warning if not.
Args:
operation: Name of the operation being attempted.
Returns:
True if on main thread (safe to proceed), False otherwise.
"""
if not _is_main_thread():
try:
import FreeCAD
FreeCAD.Console.PrintWarning(
f"MCP status widget: {operation} called from non-main thread, "
"skipping to prevent crash\n"
)
except Exception:
pass
return False
return True
class MCPStatusWidget:
"""Manages the MCP Bridge status display in FreeCAD's status bar."""
def __init__(self) -> None:
"""Initialize the status widget."""
self._widget: QtWidgets.QLabel | None = None
self._installed = False
def install(self) -> bool:
"""Install the status widget into FreeCAD's status bar.
Returns:
True if successfully installed, False otherwise.
Note:
This method must be called from the main Qt thread.
If called from another thread, it will return False to prevent crashes.
"""
if self._installed:
return True
# Thread safety check - GUI operations must be on main thread
if not _check_main_thread("install"):
return False
try:
import FreeCADGui
from PySide import QtWidgets # type: ignore[import-not-found]
# Get the main window and status bar
main_window = FreeCADGui.getMainWindow()
if main_window is None:
return False
status_bar = main_window.statusBar()
if status_bar is None:
return False
# Create the status label widget
self._widget = QtWidgets.QLabel()
self._widget.setObjectName("mcp_bridge_status_widget")
self._widget.setToolTip("MCP Bridge Status")
# Style it to stand out slightly
self._widget.setStyleSheet(
"QLabel { padding: 2px 6px; border-radius: 3px; font-size: 11px; }"
)
# Add as a permanent widget (won't be hidden by temporary messages)
status_bar.addPermanentWidget(self._widget)
self._installed = True
self.set_stopped()
return True
except Exception as e:
import FreeCAD
FreeCAD.Console.PrintWarning(f"Could not install MCP status widget: {e}\n")
return False
def remove(self) -> None:
"""Remove the status widget from the status bar.
Note:
This method must be called from the main Qt thread.
If called from another thread, it will silently return.
"""
if self._widget is None:
return
# Thread safety check - GUI operations must be on main thread
if not _check_main_thread("remove"):
return
try:
self._widget.setParent(None)
self._widget.deleteLater()
except Exception:
pass
self._widget = None
self._installed = False
def set_running(
self, xmlrpc_port: int, socket_port: int, request_count: int = 0
) -> None:
"""Update the widget to show running status.
Args:
xmlrpc_port: The XML-RPC port number.
socket_port: The socket port number.
request_count: Number of requests processed this session.
"""
if self._widget is None:
return
# Thread safety check
if not _check_main_thread("set_running"):
return
self._widget.setText(f"MCP: Running ({xmlrpc_port}/{socket_port})")
self._widget.setStyleSheet(
"QLabel { "
"background-color: #2e7d32; "
"color: white; "
"padding: 2px 6px; "
"border-radius: 3px; "
"font-size: 11px; "
"}"
)
self._widget.setToolTip(
f"MCP Bridge is running\n"
f"XML-RPC: localhost:{xmlrpc_port}\n"
f"Socket: localhost:{socket_port}\n"
f"Requests processed: {request_count}"
)
def set_stopped(self) -> None:
"""Update the widget to show stopped status."""
if self._widget is None:
return
# Thread safety check
if not _check_main_thread("set_stopped"):
return
self._widget.setText("MCP: Stopped")
self._widget.setStyleSheet(
"QLabel { "
"background-color: #757575; "
"color: white; "
"padding: 2px 6px; "
"border-radius: 3px; "
"font-size: 11px; "
"}"
)
self._widget.setToolTip("MCP Bridge is not running")
def set_starting(self) -> None:
"""Update the widget to show starting status."""
if self._widget is None:
return
# Thread safety check
if not _check_main_thread("set_starting"):
return
self._widget.setText("MCP: Starting...")
self._widget.setStyleSheet(
"QLabel { "
"background-color: #f57c00; "
"color: white; "
"padding: 2px 6px; "
"border-radius: 3px; "
"font-size: 11px; "
"}"
)
self._widget.setToolTip("MCP Bridge is starting...")
def set_error(self, message: str) -> None:
"""Update the widget to show error status.
Args:
message: Error message to display in tooltip.
"""
if self._widget is None:
return
# Thread safety check
if not _check_main_thread("set_error"):
return
self._widget.setText("MCP: Error")
self._widget.setStyleSheet(
"QLabel { "
"background-color: #c62828; "
"color: white; "
"padding: 2px 6px; "
"border-radius: 3px; "
"font-size: 11px; "
"}"
)
self._widget.setToolTip(f"MCP Bridge Error: {message}")
def get_status_widget() -> MCPStatusWidget:
"""Get the global status widget instance, creating if needed.
This function is thread-safe and uses double-checked locking.
Returns:
The MCPStatusWidget instance.
"""
global _status_widget
# Fast path: if already created, return it without acquiring lock
if _status_widget is not None:
return _status_widget
# Slow path: acquire lock and check again before creating
with _status_widget_lock:
if _status_widget is None:
_status_widget = MCPStatusWidget()
return _status_widget
def install_status_widget() -> bool:
"""Install the status widget into the status bar.
Returns:
True if successfully installed.
"""
return get_status_widget().install()
def update_status_running(
xmlrpc_port: int, socket_port: int, request_count: int = 0
) -> None:
"""Update status widget to show running state."""
widget = get_status_widget()
widget.install() # Ensure installed
widget.set_running(xmlrpc_port, socket_port, request_count)
def update_status_stopped() -> None:
"""Update status widget to show stopped state."""
widget = get_status_widget()
widget.install() # Ensure installed
widget.set_stopped()
def update_status_starting() -> None:
"""Update status widget to show starting state."""
widget = get_status_widget()
widget.install() # Ensure installed
widget.set_starting()
def update_status_error(message: str) -> None:
"""Update status widget to show error state."""
widget = get_status_widget()
widget.install() # Ensure installed
widget.set_error(message)
def sync_status_with_bridge() -> None:
"""Sync status widget with current bridge state.
This function checks the bridge status and updates the widget accordingly.
Must be called from the main Qt thread.
"""
try:
# Thread safety check
if not _check_main_thread("sync_status_with_bridge"):
return
from commands import _mcp_plugin
from preferences import get_status_bar_enabled
if not get_status_bar_enabled():
return
widget = get_status_widget()
if not widget.install():
return
if _mcp_plugin is not None and _mcp_plugin.is_running:
widget.set_running(
_mcp_plugin.xmlrpc_port,
_mcp_plugin.socket_port,
_mcp_plugin.request_count,
)
else:
widget.set_stopped()
except Exception:
pass
@@ -1,196 +0,0 @@
<languages/>
<translate>
<!--T:172-->
[[Image:FreecadRobustMCPBridge.svg|thumb|128px|Robust MCP Bridge workbench icon]]
<!--T:1-->
{{Workbench
|Name=Robust MCP Bridge Workbench
|Icon=FreecadRobustMCPBridge.svg
|Description=Bridge workbench designed to provide an interface between FreeCAD and the Robust MCP Server to enable AI assistants (like Claude) to control FreeCAD via the Model Context Protocol (MCP). It provides XML-RPC and JSON-RPC interfaces for external automation.
|Author=Sean P. Kane
|Version=0.6.2
|Date=2026-01-18
|FCVersion=0.21+
|Download=[https://github.com/spkane/freecad-robust-mcp-and-more/releases Latest Release]
|SeeAlso=[[Macros|Macros]], [[External_workbenches|External Workbenches]]
}}
==Description== <!--T:2-->
<!--T:3-->
The '''Robust MCP Bridge Workbench''' is the server-side connection point that bridges to the [https://pypi.org/project/freecad-robust-mcp/ Robust MCP Server], which enables external applications to control FreeCAD through the [https://modelcontextprotocol.io/ Model Context Protocol (MCP)]. The workbench runs inside FreeCAD and exposes XML-RPC and JSON-RPC interfaces that external MCP clients can connect to.
<!--T:4-->
This workbench is designed to work with the [https://pypi.org/project/freecad-robust-mcp/ Robust MCP Server] (available on PyPI), which allows AI assistants like [https://claude.ai Claude] to interact with FreeCAD through natural language. The full documentation and source code can be found at [https://github.com/spkane/freecad-robust-mcp-and-more github/spkane/freecad-robust-mcp-and-more], where you will find the MCP Server, Bridge and some FreeCAD Macros used for various things, not necessarily related to AI or the MCP work.
<!--T:5-->
'''Key Features:'''
* Toolbar controls for starting/stopping the MCP bridge
* Status indicator showing connection state (green=running, red=stopped)
* XML-RPC server on configurable port (default: 9875)
* JSON-RPC socket server on configurable port (default: 9876)
* Headless mode support for automation and CI/CD pipelines
* Thread-safe queue system ensuring safe FreeCAD operations
* Configurable auto-start on FreeCAD launch
==Installation== <!--T:6-->
===Via Addon Manager (Recommended)=== <!--T:7-->
<!--T:8-->
# Open FreeCAD
# Go to {{MenuCommand|Tools → Addon Manager}}
# Search for "Robust MCP Bridge"
# Click {{Button|Install}}
# Restart FreeCAD
===Manual Installation=== <!--T:9-->
<!--T:10-->
Download the latest release from [https://github.com/spkane/freecad-robust-mcp-and-more/releases GitHub Releases] and extract to your FreeCAD Mod directory:
<!--T:11-->
* '''Linux''': {{FileName|~/.local/share/FreeCAD/Mod/FreecadRobustMCPBridge/}}
* '''macOS''': {{FileName|~/Library/Application Support/FreeCAD/Mod/FreecadRobustMCPBridge/}}
* '''Windows''': {{FileName|%APPDATA%/FreeCAD/Mod/FreecadRobustMCPBridge/}}
==Usage== <!--T:12-->
===GUI Mode=== <!--T:13-->
<!--T:173-->
[[Image:Addon_RobustMCPBridge_toolbar.png|Robust MCP Bridge workbench toolbar]]
<!--T:14-->
# Switch to the '''Robust MCP Bridge''' workbench using the workbench selector
# Click {{Button|Start Bridge}} in the toolbar
# The status indicator turns green when running
# External MCP clients can now connect to localhost:9875 (XML-RPC) or localhost:9876 (Socket)
<!--T:15-->
To stop the bridge, click {{Button|Stop Bridge}} in the toolbar.
<!--T:174-->
[[Image:Addon_RobustMCPBridge_statusbar.png|Robust MCP Bridge workbench statusbar]]
===Headless Mode=== <!--T:16-->
<!--T:17-->
For automation and CI/CD pipelines, the bridge can run without the GUI:
<!--T:18-->
'''Linux:'''
{{Code|code=
freecadcmd ~/.local/share/FreeCAD/Mod/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py
}}
<!--T:19-->
'''macOS:'''
{{Code|code=
/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd \
~/Library/Application\ Support/FreeCAD/Mod/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py
}}
<!--T:20-->
The bridge will start and keep FreeCAD running until you press Ctrl+C.
==Configuration== <!--T:21-->
<!--T:22-->
Access preferences via {{MenuCommand|Edit → Preferences → Robust MCP Bridge}} or {{MenuCommand|Robust MCP Bridge → MCP Bridge Preferences...}}
<!--T:23-->
{| class="wikitable"
! Setting !! Description !! Default
|-
| Auto-start bridge || Start bridge automatically when FreeCAD launches || Disabled
|-
| Show status indicator || Display connection status in FreeCAD's status bar || Enabled
|-
| XML-RPC Port || Port for XML-RPC connections || 9875
|-
| Socket Port || Port for JSON-RPC socket connections || 9876
|}
[[Image:Addon RobustMCPBridge preferences.png|800px|Robust MCP Bridge workbench preference pane]]
==Features by Mode== <!--T:24-->
<!--T:25-->
{| class="wikitable"
! Feature !! GUI Mode !! Headless Mode
|-
| Object creation || Yes || Yes
|-
| Boolean operations || Yes || Yes
|-
| Export (STEP, STL, 3MF) || Yes || Yes
|-
| Macro execution || Yes || Yes
|-
| Document management || Yes || Yes
|-
| Screenshots || Yes || No
|-
| Object colors/visibility || Yes || No
|-
| Camera/view control || Yes || No
|}
==Connecting MCP Clients== <!--T:26-->
<!--T:27-->
This workbench provides the server that MCP clients connect to. To use with AI assistants like Claude, you need an MCP client such as the [https://pypi.org/project/freecad-robust-mcp/ Robust MCP Server]:
<!--T:28-->
{{Code|code=
pip install freecad-robust-mcp
}}
<!--T:29-->
Or using uv:
{{Code|code=
uv tool install freecad-robust-mcp
}}
<!--T:30-->
See the [https://github.com/spkane/freecad-robust-mcp-and-more GitHub repository] for full documentation on configuring MCP clients.
==Troubleshooting== <!--T:31-->
===Bridge Won't Start=== <!--T:32-->
<!--T:33-->
# Check the FreeCAD Python console ({{MenuCommand|View → Panels → Python console}}) for error messages
# Ensure no other process is using ports 9875/9876
# Try restarting FreeCAD
===Connection Refused=== <!--T:34-->
<!--T:35-->
# Verify the bridge is running (green status indicator in toolbar)
# Check that ports match between the workbench preferences and your MCP client configuration
# If connecting from Docker, use {{incode|host.docker.internal}} instead of {{incode|localhost}}
===Headless Mode Won't Start=== <!--T:36-->
<!--T:37-->
# Ensure you're using {{incode|freecadcmd}} (not {{incode|freecad}})
# Verify the script path is correct for your installation
# Test FreeCAD first: {{incode|freecadcmd -c "print('test')"}}
==Links== <!--T:38-->
<!--T:39-->
* [https://spkane.github.io/freecad-robust-mcp-and-more/ Full Documentation] - Complete guides, API reference, and tutorials
* [https://github.com/spkane/freecad-robust-mcp-and-more GitHub Repository] - Source code and issue tracker
* [https://pypi.org/project/freecad-robust-mcp/ Robust MCP Server on PyPI] - The MCP client that connects to this bridge
* [https://modelcontextprotocol.io/ Model Context Protocol] - The protocol specification
</translate>
[[Category:User Documentation{{#translation:}}]]
[[Category:Addons{{#translation:}}]]
[[Category:External Workbenches{{#translation:}}]]
-299
View File
@@ -1,299 +0,0 @@
# FreeCAD GUI Binary Hangs in Headless CI Environments Without Window Manager
Bug Report: [github.com/FreeCAD/FreeCAD/issues/26817](https://github.com/FreeCAD/FreeCAD/issues/26817)
---
## TL;DR (GitHub Issue Version)
**Bug**: `freecad --version` hangs indefinitely when run with Xvfb but without a window manager. `freecadcmd --version` works fine.
**Environment**: FreeCAD 1.0.2 AppImage, Ubuntu 22.04, Xvfb
**Reproduce**:
```bash
Xvfb :99 -screen 0 1920x1080x24 &
export DISPLAY=:99
./FreeCAD.AppImage --appimage-extract
./squashfs-root/AppRun freecad --version # HANGS
./squashfs-root/AppRun freecadcmd --version # Works
```
**Cause**: FreeCAD GUI requires window manager events to display any window (including the `--version` dialog box). Without a WM, Qt waits indefinitely for `ConfigureNotify`/`Expose` events that never arrive.
**Note**: Unlike most CLI tools, `freecad --version` displays a **GUI dialog**, not console output.
**Workaround**: Run `openbox &` before FreeCAD, or use `freecadcmd` for headless operations.
---
## Detailed Report
## Summary
The FreeCAD GUI binary (`freecad`) hangs indefinitely during Qt initialization when run in a headless environment with Xvfb but **without a window manager**. This affects all command-line operations including `freecad --version` and `freecad --help`. The headless binary (`freecadcmd`) works correctly in the same environment.
## Environment
- **FreeCAD versions tested**: 1.0.2 (stable), 1.1.0 (weekly-2025.09.03)
- **Platform**: Linux (Ubuntu 22.04, both x86_64 and aarch64)
- **AppImage**: `FreeCAD_1.0.2-conda-Linux-x86_64-py311.AppImage`
- **Display**: Xvfb virtual framebuffer (`:99`, 1920x1080x24)
- **Qt platform**: xcb
- **Context**: GitHub Actions CI, Docker containers
## Steps to Reproduce
### Easiest reproduction using Docker (hangs)
This one-liner reproduces the bug in an isolated container:
```bash
# Run this on any system with Docker installed (Linux, macOS, Windows)
# Automatically selects the correct AppImage for your architecture (x86_64 or aarch64)
docker run --rm -it ubuntu:22.04 bash -c '
apt-get update && apt-get install -y xvfb curl libfuse2 libgl1 libegl1 openbox >/dev/null 2>&1
cd /tmp
ARCH=$(uname -m)
if [ "$ARCH" = "aarch64" ]; then
APPIMAGE="FreeCAD_1.0.2-conda-Linux-aarch64-py311.AppImage"
else
APPIMAGE="FreeCAD_1.0.2-conda-Linux-x86_64-py311.AppImage"
fi
echo "Downloading FreeCAD AppImage for $ARCH..."
curl -sLO "https://github.com/FreeCAD/FreeCAD/releases/download/1.0.2/$APPIMAGE"
chmod +x "$APPIMAGE"
echo "Extracting AppImage..."
./"$APPIMAGE" --appimage-extract > /dev/null
ls -la /tmp/squashfs-root/AppRun
export XDG_RUNTIME_DIR=/tmp/runtime-root
echo "Starting freecadcmd with xvfb-run (should work fine)..."
xvfb-run -a /tmp/squashfs-root/AppRun freecadcmd --version || echo "This should have worked"
echo "Starting FreeCAD with xvfb-run (will hang without window manager)..."
timeout 10 xvfb-run -a /tmp/squashfs-root/AppRun freecad --version || echo "HUNG as expected (timeout after 10s)"
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
sleep 2
export DISPLAY=:99
openbox &
xvfb-run -a /tmp/squashfs-root/AppRun freecad --version
'
```
### Simplest reproduction using xvfb-run (hangs)
```bash
# Download FreeCAD AppImage
curl -LO "https://github.com/FreeCAD/FreeCAD/releases/download/1.0.2/FreeCAD_1.0.2-conda-Linux-x86_64-py311.AppImage"
chmod +x FreeCAD_1.0.2-conda-Linux-x86_64-py311.AppImage
# Extract AppImage (required because xvfb-run doesn't work well with FUSE)
./FreeCAD_1.0.2-conda-Linux-x86_64-py311.AppImage --appimage-extract
# This hangs indefinitely - xvfb-run provides Xvfb but no window manager
xvfb-run --auto-servernum ./squashfs-root/AppRun freecad --version
```
The `xvfb-run` wrapper is commonly used in CI environments to run GUI applications headlessly. It starts Xvfb automatically, but does **not** start a window manager, causing FreeCAD to hang.
### Manual Xvfb reproduction (hangs)
```bash
# Start Xvfb without a window manager
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
sleep 2
export DISPLAY=:99
export QT_QPA_PLATFORM=xcb
# Extract and run FreeCAD AppImage
./FreeCAD_1.0.2-conda-Linux-x86_64-py311.AppImage --appimage-extract
./squashfs-root/AppRun freecad --version # HANGS INDEFINITELY
```
### Variants that also hang
```bash
# All of these hang:
./squashfs-root/AppRun freecad --help
./squashfs-root/AppRun freecad --version
./squashfs-root/AppRun freecad -c "print('hello')"
QT_QPA_PLATFORM=offscreen ./squashfs-root/AppRun freecad --version
QT_QPA_PLATFORM=minimal ./squashfs-root/AppRun freecad --version
```
### What works correctly
```bash
# Headless binary works fine:
./squashfs-root/AppRun freecadcmd --version # Works immediately
./squashfs-root/AppRun freecadcmd -c "import FreeCAD; print(FreeCAD.Version())" # Works
```
## Expected Behavior
FreeCAD GUI should work in headless CI environments with just Xvfb, allowing:
1. Display of version/help dialogs (FreeCAD uses GUI dialogs, not console output)
2. Execution of Python scripts
3. Basic GUI operations for automated testing
**Note**: Unlike most CLI tools, `freecad --version` and `freecad --help` display **GUI dialog boxes** rather than printing to the console. This is by design, but it means they require a functioning GUI environment.
## Actual Behavior
The `freecad` binary:
1. Connects to X11 display successfully
2. Initializes Qt's QApplication
3. Attempts to create/display a window (version dialog, main window, etc.)
4. Waits indefinitely for X11 window manager events that never arrive
5. Hangs before the dialog/window can be displayed
## Technical Analysis
### Process State During Hang
Using `strace` and `/proc` inspection, we found:
```text
Process state: S (sleeping)
Threads: 2
Thread 1 (main): waiting in do_sys_poll on eventfd
Thread 2 (X11 reader): waiting in do_sys_poll on X11 socket
```
### Strace Output
The final syscalls before the hang show both threads blocked on `ppoll()`:
```text
# Thread 21 (main Qt event loop) - waiting on eventfd with 30s timeout
[pid 21] ppoll([{fd=6, events=POLLIN}], 1, {tv_sec=29, tv_nsec=541000000}, NULL, 8
# Thread 22 (X11 reader) - waiting on X11 socket indefinitely
[pid 22] ppoll([{fd=4, events=POLLIN}], 1, NULL, NULL, 0 <unfinished ...>
```
The file descriptors are:
- fd 4: X11 socket connection (successfully established)
- fd 6: eventfd for Qt thread synchronization
### Root Cause
FreeCAD's GUI requires window manager events to display any window or dialog (including the `--version` dialog). In a bare Xvfb environment without a window manager:
1. FreeCAD creates a window and waits for it to be mapped/configured
2. No window manager means no `ConfigureNotify`, `Expose`, or `MapNotify` events
3. Qt's event loop waits for these events before the window can be displayed
4. The main thread blocks waiting for signals from the X11 reader thread
5. The X11 reader thread blocks waiting for X11 events that never arrive
6. Deadlock: both threads wait indefinitely for events that will never come
### Why `freecadcmd` Works
The `freecadcmd` binary does not initialize Qt's GUI components, so it never enters this blocking state.
## Workaround
Running a lightweight window manager (like `openbox`) alongside Xvfb resolves the issue:
```bash
# Start Xvfb
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
sleep 2
export DISPLAY=:99
# Start a window manager (this is the key fix)
openbox &
sleep 1
# Now FreeCAD GUI works
./squashfs-root/AppRun freecad --version # Works!
```
Additionally, sending synthetic X11 events with `xdotool` can help:
```bash
# In a loop while FreeCAD starts:
xdotool mousemove 500 500 click 1 key Escape
```
## Suggested Fixes
Several approaches could improve FreeCAD's headless CI compatibility:
### Option 1: Add timeout/fallback for window manager events
FreeCAD could detect when no window manager responds within a reasonable timeout (e.g., 5 seconds) and either:
- Fall back to a minimal mode
- Exit with a clear error message instead of hanging indefinitely
- Use `QT_QPA_PLATFORM=offscreen` automatically when no WM is detected
### Option 2: Console output for `--version`/`--help` (CI-friendly)
For CI environments, having `--version` and `--help` output to console (like most CLI tools) would be helpful. This could be:
- A separate flag like `--version-console`
- Automatic when `DISPLAY` is not set or in a detected CI environment
- Controlled by an environment variable
### Option 3: Document the window manager requirement
At minimum, clearly document that the FreeCAD GUI binary requires a window manager (not just Xvfb) for any operation, including `--version`.
## Impact
This bug affects:
- **CI/CD pipelines** using FreeCAD in headless environments
- **Docker containers** running FreeCAD without a display manager
- **Automated testing** of FreeCAD-based applications
- **Server-side rendering** or batch processing with GUI features
The workaround (adding `openbox`) increases container size and complexity for CI environments.
## Additional Context
### Test Script
Here's a complete test script that demonstrates both the bug and the workaround:
```bash
#!/bin/bash
set -e
# Setup
apt-get update && apt-get install -y xvfb openbox xdotool curl
curl -LO "https://github.com/FreeCAD/FreeCAD/releases/download/1.0.2/FreeCAD_1.0.2-conda-Linux-x86_64-py311.AppImage"
chmod +x FreeCAD_1.0.2-conda-Linux-x86_64-py311.AppImage
./FreeCAD_1.0.2-conda-Linux-x86_64-py311.AppImage --appimage-extract
export DISPLAY=:99
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
sleep 2
echo "=== Test 1: Without window manager (will hang) ==="
timeout 10 ./squashfs-root/AppRun freecad --version || echo "HUNG as expected"
echo "=== Test 2: With window manager (works) ==="
openbox &
sleep 1
timeout 10 ./squashfs-root/AppRun freecad --version && echo "SUCCESS"
```
### Related
- This may be related to how FreeCAD integrates with PySide6/Qt6
- The `freecadcmd` binary correctly avoids this issue by not initializing GUI
- Other Qt applications (like `qmlscene --help`) typically handle this correctly
## System Information
```text
FreeCAD 1.0.2, Libs: 1.0.2R39319 (Git)
OS: Ubuntu 22.04 (Docker/GitHub Actions)
Python: 3.11.13 (conda-forge)
Qt: 6.x (bundled in AppImage)
```
-307
View File
@@ -1,307 +0,0 @@
# FreeCAD Robust MCP Server Comparison Analysis
This document analyzes existing FreeCAD Robust MCP server implementations to identify best practices and improvements for our architecture.
## Existing Implementations
### 1. [neka-nat/freecad-mcp](https://github.com/neka-nat/freecad-mcp) (380+ stars)
**Architecture:**
- XML-RPC server running inside FreeCAD (port 9875)
- Queue-based GUI communication for thread safety
- FastMCP-based MCP server connecting via XML-RPC
**Key Features:**
- `create_object()` supports Part::, Draft::, PartDesign::, Fem:: types
- `get_view()` captures screenshots with multiple perspectives
- `insert_part_from_library()` for part library access
- Smart screenshot handling (detects unsupported views like TechDraw)
**Strengths:**
- Thread-safe queue system for GUI operations
- Comprehensive object type support
- Screenshot capabilities with view selection
- Parts library integration
**Weaknesses:**
- XML-RPC only (no embedded/headless mode)
- No macro development tools
- Limited debugging capabilities
---
### 2. [jango-blockchained/mcp-freecad](https://github.com/jango-blockchained/mcp-freecad)
**Architecture:**
- 6 connection modes: Launcher, Server, Bridge, RPC, Wrapper, Mock
- FastMCP 2.13.0+ with FastAPI integration
- Multi-provider AI support (Claude, OpenAI, Google, OpenRouter)
**Key Features:**
- AppImage/AppRun launcher integration
- Connection recovery mechanisms
- Resource caching and performance diagnostics
- Modern GUI addon with real-time diagnostics
**Strengths:**
- Most flexible connection options
- Multi-AI provider support
- Performance monitoring
- Connection recovery/resilience
**Weaknesses:**
- Complex setup with many moving parts
- Heavier dependency footprint
---
### 3. [contextform/freecad-mcp](https://github.com/contextform/freecad-mcp)
**Architecture:**
- Node.js-based MCP bridge (`working_bridge.py`)
- AICopilot workbench integrated into FreeCAD
- Cross-platform installer (`freecad-mcp-setup`)
**Key Features:**
- 13 PartDesign operations (Pad, Revolution, Fillet, Chamfer, etc.)
- 18 Part operations (Primitives, Booleans, Transforms)
- 14 View control tools
- Automated installer with OS detection
**Strengths:**
- Most comprehensive CAD operation coverage
- Excellent PartDesign/Part workbench integration
- Easy installation process
- Demo showcasing full workflow (house modeling)
**Weaknesses:**
- Node.js dependency adds complexity
- Less focus on debugging/development workflows
---
### 4. [ATOI-Ming/FreeCAD-MCP](https://github.com/ATOI-Ming/FreeCAD-MCP)
**Architecture:**
- Server-client with stdio/TCP (port 9876)
- GUI control panel inside FreeCAD
- Macro-centric workflow
**Key Features:**
- Macro templates (default, basic, part, sketch)
- Automatic import injection in macros
- Macro validation before execution
- View control (front, top, right, axonometric)
- Log management and report browser
**Strengths:**
- Focus on macro development workflow
- GUI panel for easy control
- Automatic boilerplate injection
- Comprehensive logging
**Weaknesses:**
- Macro-only focus (no direct object creation)
- Windows-centric paths in documentation
---
### 5. [bonninr/freecad_mcp](https://github.com/bonninr/freecad_mcp)
**Architecture:**
- Simple socket-based server
- Two primary tools: `get_scene_info` and `run_script`
**Key Features:**
- Comprehensive scene information retrieval
- Arbitrary Python code execution
- Minimal, focused API
**Strengths:**
- Simple, easy to understand
- Full Python access via `run_script`
- Lightweight
**Weaknesses:**
- Very minimal tool set
- No specialized CAD operations
---
## Feature Comparison Matrix
| Feature | neka-nat | jango | contextform | ATOI-Ming | bonninr | **Ours** |
| ------------------- | -------- | ----- | ----------- | --------- | ------- | -------- |
| Headless mode | No | Yes | No | No | No | **Yes** |
| GUI mode | Yes | Yes | Yes | Yes | Yes | **Yes** |
| XML-RPC | Yes | Yes | No | No | No | **Yes** |
| Socket/TCP | No | Yes | No | Yes | Yes | **Yes** |
| Embedded Python | No | No | No | No | No | **Yes** |
| Screenshot capture | Yes | Yes | Yes | No | No | **Yes** |
| Parts library | Yes | No | No | No | No | **Yes** |
| Macro support | No | No | No | Yes | No | **Yes** |
| PartDesign tools | Basic | Yes | Yes | Via macro | No | **Yes** |
| Boolean operations | Yes | Yes | Yes | Via macro | No | **Yes** |
| FEM support | Yes | No | No | No | No | **Yes** |
| Multi-AI provider | No | Yes | No | No | No | No |
| Connection recovery | No | Yes | No | No | No | **Yes** |
| Thread-safe GUI ops | Yes | Yes | Unknown | Unknown | Unknown | **Yes** |
| MCP Resources | No | No | No | No | No | **Yes** |
| MCP Prompts | No | No | No | No | No | **Yes** |
---
## Key Learnings & Improvements for Our Architecture
### 1. Communication Protocol
**Learning:** neka-nat uses XML-RPC which is proven and reliable.
**Improvement:** Support both XML-RPC (for compatibility) and JSON-RPC over sockets (simpler, more modern). Add embedded mode for true headless operation.
### 2. Thread-Safe GUI Operations
**Learning:** neka-nat's queue-based system is essential for GUI stability.
**Improvement:** Implement similar queue system with Qt timer for main thread execution:
```python
# Queue-based GUI communication
rpc_request_queue = queue.Queue()
rpc_response_queue = queue.Queue()
def process_gui_tasks():
"""Execute queued operations on main GUI thread."""
while not rpc_request_queue.empty():
task = rpc_request_queue.get()
result = task()
rpc_response_queue.put(result)
```
### 3. Screenshot Capabilities
**Learning:** neka-nat handles unsupported view types gracefully.
**Improvement:** Add comprehensive view support with intelligent fallbacks:
- Multiple view angles (Isometric, Front, Top, Right, etc.)
- View type detection (skip TechDraw, Spreadsheet)
- Configurable resolution and format
### 4. Macro Development Focus
**Learning:** ATOI-Ming's macro-centric approach is unique and valuable.
**Improvement:** Add dedicated macro tools:
- `create_macro` with templates
- `validate_macro` for syntax checking
- `run_macro` with parameter passing
- Automatic import injection
### 5. PartDesign Integration
**Learning:** contextform has the most comprehensive PartDesign coverage.
**Improvement:** Add specialized PartDesign tools:
- Pad, Pocket, Revolution, Groove
- Fillet, Chamfer
- Hole (with standards support)
- LinearPattern, PolarPattern
- Mirrored
### 6. Connection Resilience
**Learning:** jango-blockchained has connection recovery mechanisms.
**Improvement:** Add connection health monitoring and auto-reconnect:
```python
async def maintain_connection(self):
"""Background task to maintain connection health."""
while self._running:
if not await self.is_connected():
await self.reconnect()
await asyncio.sleep(5)
```
### 7. Parts Library Integration
**Learning:** neka-nat provides access to FreeCAD parts library.
**Improvement:** Expose parts library with search and filtering.
### 8. Comprehensive Logging
**Learning:** ATOI-Ming has excellent logging with GUI browser.
**Improvement:** Add structured logging with levels, rotation, and optional GUI viewer in plugin.
---
## Updated Architecture Decisions
Based on this analysis, our architecture will:
1. **Support 3 connection modes:**
- Embedded (headless, in-process FreeCAD)
- XML-RPC (proven, compatible with neka-nat addon)
- JSON-RPC over socket (modern, simpler)
1. **Implement queue-based GUI thread safety** following neka-nat's pattern
1. **Provide comprehensive tool coverage:**
- Execution: Python, macros
- Documents: create, open, save, list
- Objects: create, edit, delete, query
- PartDesign: pad, pocket, fillet, chamfer, patterns
- Part: primitives, booleans, transforms
- Export: STEP, STL, OBJ, FreeCAD native
- View: screenshot, camera control
1. **Add unique features:**
- MCP Resources for document introspection
- MCP Prompts for guided workflows
- Macro development toolkit
- Workbench-specific tools
1. **Focus on developer experience:**
- Debugging tools
- Error introspection
- Console history access
- Constraint validation
---
## Sources
- [neka-nat/freecad-mcp](https://github.com/neka-nat/freecad-mcp)
- [jango-blockchained/mcp-freecad](https://github.com/jango-blockchained/mcp-freecad)
- [contextform/freecad-mcp](https://github.com/contextform/freecad-mcp)
- [ATOI-Ming/FreeCAD-MCP](https://github.com/ATOI-Ming/FreeCAD-MCP)
- [bonninr/freecad_mcp](https://github.com/bonninr/freecad_mcp)
File diff suppressed because it is too large Load Diff
-485
View File
@@ -1,485 +0,0 @@
# FreeCAD Robust MCP User Guide
This guide explains how to use AI assistants with FreeCAD via the MCP (Model Context Protocol) server to create and manipulate 3D CAD models.
---
## Table of Contents
1. [Getting Started](#getting-started)
1. [Running Modes](#running-modes)
1. [Basic Workflows](#basic-workflows)
1. [Object Creation Examples](#object-creation-examples)
1. [PartDesign Workflow](#partdesign-workflow)
1. [Complete Example: Mounting Bracket](#complete-example-mounting-bracket)
1. [Tips and Best Practices](#tips-and-best-practices)
---
## Getting Started
### Prerequisites
1. **FreeCAD installed** - Version 1.0.x or later
1. **An MCP client** (Claude Code, or other MCP-compatible AI assistant) configured
1. **Python 3.11** - Must match FreeCAD's bundled Python version
### Starting FreeCAD with MCP Bridge
You have two options depending on your workflow:
#### Headless Mode (Command-line only)
Best for automated workflows, batch processing, or when you don't need visual feedback.
```bash
just freecad::run-headless
```
**Capabilities:** All modeling operations, export, scripting.
**Limitations:** No screenshots, no visual feedback, no color/visibility control.
#### GUI Mode (Full graphical interface)
Best for interactive design work where you want to see results visually.
```bash
just freecad::run-gui
```
**Capabilities:** Everything headless mode can do, plus screenshots, colors, view control.
### Verifying Connection
Once FreeCAD is running with the MCP bridge, you can verify the connection:
```text
"Check the FreeCAD connection status"
```
Claude will use the `get_connection_status` tool to confirm the bridge is working.
---
## Running Modes
### Headless vs GUI Mode
| Feature | Headless Mode | GUI Mode |
| ------------------------ | ------------- | -------- |
| Object creation | Yes | Yes |
| Boolean operations | Yes | Yes |
| Export (STEP, STL, etc.) | Yes | Yes |
| Save documents | Yes | Yes |
| Screenshots | No | Yes |
| Object colors | No | Yes |
| Object visibility | No | Yes |
| Camera control | No | Yes |
| Interactive selection | No | Yes |
### Detecting the Current Mode
When working with Claude, it will automatically detect whether FreeCAD is in GUI or headless mode and adapt accordingly. GUI-only operations will return informative errors in headless mode rather than crashing.
---
## Basic Workflows
### Creating a Document
Every FreeCAD project needs a document. You can ask Claude:
```text
"Create a new FreeCAD document called 'MyProject'"
```
Claude will use the `create_document` tool.
### Creating Simple Shapes
Ask Claude to create primitive shapes:
```text
"Create a box that's 50mm long, 30mm wide, and 10mm tall"
"Add a cylinder with radius 5mm and height 20mm"
"Create a sphere with 15mm radius"
```
### Positioning Objects
Move and rotate objects:
```text
"Move the box to position (100, 50, 0)"
"Rotate the cylinder 45 degrees around the Z axis"
```
### Boolean Operations
Combine shapes:
```text
"Fuse the box and cylinder together"
"Cut a hole through the box using the cylinder"
"Find the intersection of the two shapes"
```
### Saving and Exporting
```text
"Save the document as MyProject.FCStd"
"Export the model to STEP format"
"Export for 3D printing as STL"
```
---
## Object Creation Examples
### Example 1: Simple Box with Hole
**Request:**
```text
Create a 50x50x20mm box with a 10mm diameter hole through the center.
```
**What Claude does:**
1. Creates a document
1. Creates a box (50x50x20)
1. Creates a cylinder (radius 5, height 30) positioned at center
1. Performs boolean cut operation
1. Returns the result
### Example 2: Pipe/Tube Shape
**Request:**
```text
Create a pipe with outer diameter 40mm, inner diameter 30mm, and length 100mm.
```
**What Claude does:**
1. Creates outer cylinder (radius 20, height 100)
1. Creates inner cylinder (radius 15, height 100)
1. Cuts inner from outer to create hollow tube
### Example 3: L-Bracket
**Request:**
```text
Create an L-shaped bracket:
- Horizontal part: 100mm x 50mm x 5mm
- Vertical part: 5mm x 50mm x 80mm standing on the horizontal part
```
**What Claude does:**
1. Creates horizontal box
1. Creates vertical box positioned at correct location
1. Fuses them together
---
## PartDesign Workflow
For parametric modeling that maintains design history, use the PartDesign workflow.
### Understanding PartDesign Concepts
**Body**: A container for a single solid model built from features.
**Sketch**: A 2D drawing that defines profiles for 3D operations.
**Features**: Operations like Pad (extrude), Pocket (cut), Fillet, etc.
### Basic PartDesign Example
**Request:**
```text
Create a parametric mounting plate:
1. Start with a PartDesign body
2. Create a sketch on the XY plane with a 100x60mm rectangle
3. Pad it 8mm thick
4. Add four 5mm holes near each corner for mounting screws
5. Fillet all edges with 2mm radius
```
**What Claude does:**
1. `create_partdesign_body()` - Creates the Body container
1. `create_sketch(body_name="Body", plane="XY_Plane")` - Creates attached sketch
1. `add_sketch_rectangle(...)` - Adds the profile geometry
1. `pad_sketch(sketch_name="Sketch", length=8)` - Extrudes to solid
1. Creates new sketches with points for hole locations
1. `create_hole(...)` for each mounting hole
1. `fillet_edges(...)` - Rounds the edges
### Revolving Profiles
**Request:**
```text
Create a turned part:
- Draw a profile on the XZ plane
- Revolve it 360 degrees around the X axis
```
**What Claude does:**
1. Creates body and sketch on XZ plane
1. Draws the profile using lines and arcs
1. Uses `revolution_sketch()` to create the solid
### Pattern Operations
**Request:**
```text
Create a plate with a row of 6 holes spaced 15mm apart.
```
**What Claude does:**
1. Creates the base plate
1. Creates one hole feature
1. Uses `linear_pattern()` to repeat the hole
---
## Complete Example: Mounting Bracket
This detailed example shows a complete workflow for creating a practical part.
### Design Requirements
Create a mounting bracket with:
- Base plate: 80mm x 60mm x 5mm
- Vertical support: 5mm thick, 50mm tall, 60mm wide
- Two mounting holes (6mm diameter) on the base
- One slot (10mm x 20mm) on the vertical support
- 3mm fillets on external corners
### Step-by-Step Workflow
#### Step 1: Ask Claude to create the bracket
```text
Create a mounting bracket with the following specifications:
Base plate:
- Size: 80mm x 60mm x 5mm
- Two 6mm diameter mounting holes, centered 15mm from each short edge
Vertical support:
- Attached to one end of the base
- 5mm thick, 60mm wide, 50mm tall
- One slot: 10mm wide x 20mm tall, centered
Finish:
- 3mm fillet on all outer edges
- Export as STEP file when done
```
#### Step 2: Claude's approach
Claude will break this down into manageable operations:
```python
# 1. Create document and PartDesign body
create_document(name="MountingBracket")
create_partdesign_body(name="Body")
# 2. Create base plate sketch and pad
create_sketch(body_name="Body", plane="XY_Plane", name="BaseSketch")
add_sketch_rectangle(sketch_name="BaseSketch", x=0, y=0, width=80, height=60)
pad_sketch(sketch_name="BaseSketch", length=5)
# 3. Add vertical support
create_sketch(body_name="Body", plane="XZ_Plane", name="SupportSketch")
# ... add geometry
pad_sketch(sketch_name="SupportSketch", length=60)
# 4. Add mounting holes
create_sketch(body_name="Body", plane="XY_Plane", name="HoleSketch")
add_sketch_point(sketch_name="HoleSketch", x=15, y=30)
add_sketch_point(sketch_name="HoleSketch", x=65, y=30)
create_hole(sketch_name="HoleSketch", diameter=6, hole_type="ThroughAll")
# 5. Add slot (as pocket)
create_sketch(body_name="Body", plane="Face...", name="SlotSketch")
add_sketch_rectangle(...)
pocket_sketch(sketch_name="SlotSketch", length=5, type="ThroughAll")
# 6. Add fillets
fillet_edges(object_name="...", radius=3)
# 7. Export
export_step(file_path="/path/to/bracket.step")
```
#### Step 3: View the result (GUI mode)
```text
Take a screenshot of the bracket from an isometric view
```
Claude will use `get_screenshot(view_angle="Isometric")` to capture and display the result.
---
## Tips and Best Practices
### 1. Be Specific with Dimensions
**Good:** "Create a box 50mm x 30mm x 10mm"
**Vague:** "Create a small box"
### 2. Specify Units
FreeCAD uses millimeters by default. Always include units to avoid confusion:
```text
"Create a cylinder with 25.4mm (1 inch) diameter"
```
### 3. Use Meaningful Names
```text
"Create a box named 'BasePlate' and a cylinder named 'MountingHole'"
```
This makes it easier to reference objects later.
### 4. Work Incrementally
For complex parts, build step by step:
1. Create the basic shape
1. Verify it looks correct
1. Add features one at a time
1. Check after each major operation
### 5. Save Frequently
```text
"Save the document"
```
FreeCAD can crash, and you don't want to lose work.
### 6. Use PartDesign for Parametric Parts
If you might need to modify dimensions later, use the PartDesign workflow with sketches rather than direct Part operations.
### 7. Export to Multiple Formats
For manufacturing or 3D printing:
```text
"Export as STEP for CNC machining and STL for 3D printing"
```
### 8. Use the execute_python Tool for Advanced Operations
For operations not covered by the standard tools, Claude can execute custom Python:
```text
"Calculate the volume and center of mass of the part"
```
Claude will use `execute_python()` to run the necessary FreeCAD Python commands.
### 9. Check GUI Mode for Visual Features
Before asking for screenshots or colors:
```text
"Is FreeCAD running in GUI mode?"
```
### 10. Use Patterns for Repetitive Features
Instead of creating many individual features:
```text
"Create one hole and pattern it in a 4x3 grid with 20mm spacing"
```
---
## Common Operations Quick Reference
| Task | How to Ask |
| ---------------- | ------------------------------------------------------- |
| Create box | "Create a box 50x30x10mm" |
| Create cylinder | "Create a cylinder with 10mm radius and 20mm height" |
| Move object | "Move MyBox to position (100, 50, 0)" |
| Rotate object | "Rotate MyCylinder 45 degrees around Z axis" |
| Boolean union | "Fuse Box and Cylinder together" |
| Boolean subtract | "Cut Cylinder from Box" |
| Create hole | "Add a 6mm through hole at (25, 15)" |
| Fillet edges | "Add 3mm fillet to all edges" |
| Chamfer edges | "Add 2mm chamfer to selected edges" |
| Export STEP | "Export to STEP format" |
| Export STL | "Export for 3D printing" |
| Save document | "Save the document as MyPart.FCStd" |
| Screenshot | "Take a screenshot from the front view" (GUI mode only) |
| Change color | "Make the box red" (GUI mode only) |
---
## Troubleshooting
### "GUI not available" Error
You're running in headless mode and trying to use a GUI-only feature. Either:
- Switch to GUI mode: `just freecad::run-gui`
- Use an alternative approach (e.g., skip visual operations)
### Objects Not Appearing
Make sure to recompute the document:
```text
"Recompute the document"
```
### Boolean Operations Failing
Ensure:
1. Both objects have valid shapes
1. The objects actually intersect
1. Objects are in the same document
### Sketch Errors
Sketches need to be fully constrained for PartDesign operations. Ask Claude to:
```text
"Check if the sketch is fully constrained"
```
### Connection Issues
If the MCP bridge isn't responding:
1. Check FreeCAD is running with the bridge started
1. Verify ports 9875 (XML-RPC) and 9876 (socket) are available
1. Restart FreeCAD with `just freecad::run-gui` or `just freecad::run-headless`
---
## Next Steps
- See [MCP_TOOLS_REFERENCE.md](MCP_TOOLS_REFERENCE.md) for detailed API documentation
- Explore the FreeCAD wiki for advanced techniques
- Practice with simple parts before attempting complex assemblies
-34
View File
@@ -1,34 +0,0 @@
# Bridge API Reference
The bridge module provides the communication layer between the Robust MCP Server and FreeCAD.
## Base Classes
::: freecad_mcp.bridge.base
options:
show_root_heading: true
show_source: true
## XML-RPC Bridge
::: freecad_mcp.bridge.xmlrpc
options:
show_root_heading: true
show_source: true
## Socket Bridge
::: freecad_mcp.bridge.socket
options:
show_root_heading: true
show_source: true
## Embedded Bridge
!!! warning "Linux Only"
The embedded bridge only works on Linux. See [Connection Modes](../guide/connection-modes.md) for details.
::: freecad_mcp.bridge.embedded
options:
show_root_heading: true
show_source: true
-6
View File
@@ -1,6 +0,0 @@
# Configuration API Reference
::: freecad_mcp.config
options:
show_root_heading: true
show_source: true
-11
View File
@@ -1,11 +0,0 @@
# Server API Reference
::: freecad_mcp.server
options:
show_root_heading: true
show_source: true
members:
\- mcp
\- get_bridge
\- startup
\- shutdown
File diff suppressed because it is too large Load Diff
-348
View File
@@ -1,348 +0,0 @@
# Architecture
This document provides a technical overview of the FreeCAD Robust MCP Server architecture.
For the full architecture document with design decisions and rationale, see [Detailed Architecture](architecture-detailed.md).
---
## Overview
The FreeCAD Robust MCP Server follows a **Bridge with Adapter** pattern:
```text
┌─────────────────────────────────────────────────────────────────────────┐
│ MCP Server Layer │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ FastMCP Application │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Tools │ │ Resources│ │ Prompts │ │ Lifecycle│ │ │
│ │ │ (82+) │ │ │ │ │ │ Manager │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ FreeCAD Bridge Interface │ │
│ │ (Abstract Base Class) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ ╱ │ ╲ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ EmbeddedBridge │ │ SocketBridge │ │ XMLRPCBridge │ │
│ │ (Linux only) │ │ (JSON-RPC) │ │ (Recommended) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Module Structure
```text
src/freecad_mcp/
├── __init__.py # Package entry point
├── _version.py # Version info (auto-generated)
├── server.py # Main MCP server entry point
├── config.py # Configuration management
├── py.typed # PEP 561 marker for type hints
├── bridge/ # FreeCAD communication layer
│ ├── __init__.py # Bridge factory
│ ├── base.py # Abstract bridge interface
│ ├── embedded.py # In-process FreeCAD (Linux only)
│ ├── socket.py # JSON-RPC socket bridge
│ └── xmlrpc.py # XML-RPC bridge (recommended)
├── tools/ # MCP tool implementations
│ ├── __init__.py
│ ├── execution.py # Python execution & debugging
│ ├── documents.py # Document management
│ ├── objects.py # Object creation/manipulation
│ ├── partdesign.py # PartDesign parametric modeling
│ ├── view.py # View, camera, display
│ ├── export.py # Export/import operations
│ └── macros.py # Macro management
├── resources/ # MCP resource implementations
│ ├── __init__.py
│ └── freecad.py # Document, console, capabilities
├── prompts/ # MCP prompt templates
│ ├── __init__.py
│ └── freecad.py # Modeling and debugging prompts
└── utils/ # Utility modules
└── __init__.py
```
---
## Bridge Architecture
### Base Interface
All bridges implement `FreecadBridge`:
```python
class FreecadBridge(ABC):
@abstractmethod
async def connect(self) -> None: ...
@abstractmethod
async def disconnect(self) -> None: ...
@abstractmethod
async def is_connected(self) -> bool: ...
@abstractmethod
async def execute_python(
self, code: str, timeout_ms: int = 30000
) -> ExecutionResult: ...
```
### XML-RPC Bridge (Recommended)
- Connects to FreeCAD via XML-RPC on port 9875
- Proven, reliable protocol
- Works on all platforms
### Socket Bridge
- Uses JSON-RPC over TCP sockets on port 9876
- Lower overhead than XML-RPC
- Easier to debug (JSON format)
### Embedded Bridge
- Imports FreeCAD directly into the MCP server process
- **Linux only** (crashes on macOS/Windows)
- Fastest execution (no IPC overhead)
- Headless mode only
---
## Workbench Addon Architecture
The workbench addon runs inside FreeCAD:
```text
addon/FreecadRobustMCPBridge/
├── Init.py # Module initialization
├── InitGui.py # GUI initialization (workbench)
├── FreecadRobustMCPBridge.svg # Workbench icon
└── freecad_mcp_bridge/ # Bridge plugin
├── __init__.py
├── server.py # XML-RPC/JSON-RPC server
├── blocking_bridge.py # Blocking server (keeps FreeCAD running)
└── startup_bridge.py # Non-blocking startup (for interactive GUI)
package.xml # FreeCAD addon metadata (in project root)
```
Note: The `package.xml` file is in the project root, not inside the addon directory. This is because it defines metadata for multiple components (workbench and macros) in a single manifest.
### Thread Safety
The workbench uses a queue-based system for thread-safe GUI operations:
```python
# Operations queued from network thread
request_queue.put(operation)
# Executed on main GUI thread via QTimer
def process_queue():
while not request_queue.empty():
op = request_queue.get()
result = op()
response_queue.put(result)
```
---
## Data Flow
### Tool Execution
```text
1. AI Assistant sends tool request
2. MCP Server receives request
3. Tool handler prepares Python code
4. Bridge.execute_python() sends code
5. FreeCAD executes code (main thread)
6. Result returned via bridge
7. MCP Server formats response
8. AI Assistant receives result
```
### Code Execution Pattern
Tools generate Python code that runs in FreeCAD:
```python
@mcp.tool()
async def create_box(length: float = 10.0, ...) -> dict:
bridge = await get_bridge()
code = f'''
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Unnamed")
obj = doc.addObject("Part::Box", "Box")
obj.Length = {length}
doc.recompute()
_result_ = {{"name": obj.Name, "volume": obj.Shape.Volume}}
'''
result = await bridge.execute_python(code)
return result.result
```
---
## GUI Detection
Tools check `FreeCAD.GuiUp` to handle headless mode:
```python
code = f'''
if not FreeCAD.GuiUp:
_result_ = {{"success": False, "error": "GUI not available"}}
else:
# GUI-only operations
obj.ViewObject.Visibility = True
_result_ = {{"success": True}}
'''
```
---
## Configuration
Configuration via environment variables:
| Variable | Default | Description |
| --------------------- | ----------- | --------------------------- |
| `FREECAD_MODE` | `xmlrpc` | Connection mode |
| `FREECAD_PATH` | auto | FreeCAD lib path (embedded) |
| `FREECAD_SOCKET_HOST` | `localhost` | Socket/XML-RPC host |
| `FREECAD_SOCKET_PORT` | `9876` | JSON-RPC socket port |
| `FREECAD_XMLRPC_PORT` | `9875` | XML-RPC port |
| `FREECAD_TIMEOUT_MS` | `30000` | Execution timeout |
---
## Testing Strategy
### Unit Tests
- Mock FreeCAD module
- Test bridge logic in isolation
- Run on all platforms
### Integration Tests
- Use FreeCAD AppImage in CI
- Test actual FreeCAD operations
- Run in headless mode
### Embedded Mode Testing
Embedded mode receives **minimal testing**:
- Unit tests with mocked FreeCAD
- No CI integration tests (would require Linux + FreeCAD in-process)
- Recommended to use xmlrpc/socket modes for production
---
## Future Considerations
### Bundled vs. Separate Server Architecture
The current architecture keeps the MCP server separate from the FreeCAD addon/workbench. This section documents the trade-offs and potential future directions.
#### Current Approach: Separate Components
```text
┌─────────────────┐ XML-RPC/Socket ┌─────────────────┐
│ MCP Server │◄──────────────────────►│ FreeCAD │
│ (separate venv) │ │ (+ Workbench) │
└─────────────────┘ └─────────────────┘
```
**Advantages:**
- Server runs in its own Python environment with full control over dependencies
- Allows remote server scenarios (AI/server on powerful machine, FreeCAD on workstation)
- Server can be updated independently of the addon
- No dependency conflicts with FreeCAD's embedded Python
- Easier testing and development
**Disadvantages:**
- Users must install two components separately
- More complex setup process
- Need to manage version compatibility between server and workbench
#### Potential Future: Bundled Server in Addon
```text
┌─────────────────────────────────────────┐
│ FreeCAD Addon │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ Workbench │◄──►│ Bundled Server │ │
│ │ (GUI) │ │ (subprocess) │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────┘
```
A bundled approach could:
- Provide single-install experience from FreeCAD Addon Manager
- Auto-start server when workbench loads
- Still support "Remote Server" mode for advanced users via preferences
**Implementation considerations:**
1. **Dependency management**: Server requires `fastmcp`, `httpx`, `uvicorn`, etc. These may conflict with FreeCAD's Python. Options:
- Bundle dependencies in addon (vendor them)
- Use subprocess with bundled `requirements.txt` and pip install on first run
- Create a minimal server that uses only stdlib
2. **Startup modes**:
```python
if preferences.use_remote_server:
connect_to(preferences.server_url)
else:
# Start bundled server in subprocess
subprocess.Popen([sys.executable, "-m", "robust_mcp_server"])
```
3. **Hybrid approach**: Default to bundled local server, but expose preferences for remote server URL (host:port) for advanced deployments.
#### Decision
Currently maintaining separate components because:
- Cleaner separation of concerns
- Proven reliability across platforms
- Easier to develop and test independently
- Remote server use case, while less common, is valuable for some workflows
May revisit bundling in a future major version when:
- Dependency requirements stabilize
- User feedback indicates strong preference for single-install
- A clean subprocess-based bundling approach is validated
---
## Next Steps
- [Contributing](contributing.md) - How to contribute
- [Detailed Architecture](architecture-detailed.md) - Complete design details
-222
View File
@@ -1,222 +0,0 @@
# Contributing
Thank you for your interest in contributing to FreeCAD Robust MCP Server!
---
## Development Setup
### Prerequisites
- Python 3.11 (must match FreeCAD's bundled version)
- [mise](https://mise.jdx.dev/) for tool management
- FreeCAD 0.21+ or 1.0+ installed
### Initial Setup
```bash
git clone https://github.com/spkane/freecad-robust-mcp-and-more.git
cd freecad-robust-mcp-and-more
# Install mise if not already installed
curl https://mise.run | sh
# Install project tools and dependencies
mise trust
mise install
just setup
```
### Safety CLI Account (Required for Security Scanning)
This project uses [Safety CLI](https://safetycli.com/) for dependency vulnerability scanning. Safety requires a **free account** for the `safety scan` command used in pre-commit hooks.
```bash
# Register for a free account (interactive)
uv run safety auth
# Or login if you already have an account
uv run safety auth --login
```
**Note:** Authentication is stored locally and only needs to be done once per machine. If you skip this step, the `safety` pre-commit hook will fail with an authentication prompt.
**CI/CD:** Safety runs in CI using the `SAFETY_API_KEY` repository secret.
### Running Tests
```bash
# Run unit tests
just testing::unit
# Run with coverage
just testing::cov
# Run all tests including integration
just testing::all
# Run type checking
uv run mypy src/
```
### Code Quality
```bash
# Run all pre-commit checks
just quality::check
# Run linting
just quality::lint
# Format code
just quality::format
# Run security checks
just quality::security
```
---
## Project Structure
```text
freecad-robust-mcp-and-more/
├── src/freecad_mcp/ # Main package
│ ├── bridge/ # FreeCAD connection bridges
│ ├── tools/ # MCP tool implementations
│ ├── resources/ # MCP resource implementations
│ ├── prompts/ # MCP prompt templates
│ └── server.py # Main server entry point
├── addon/ # FreeCAD workbench addon
│ └── FreecadRobustMCPBridge/ # Workbench files
├── tests/ # Test suite
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests
├── docs/ # Documentation
└── just/ # Justfile modules
```
---
## Contribution Guidelines
### Code Style
- Follow PEP 8 with 88-character line length (ruff/black)
- Use type hints for all function signatures
- Write Google-style docstrings
- Run `just quality::format` before committing
### Testing
- Write tests for all new functionality
- Maintain test coverage
- Run `just all` before submitting PRs (runs quality checks + unit tests)
- Integration tests require FreeCAD (run via CI)
### Documentation
- Update docstrings for API changes
- Update user docs for feature changes
- Run `just documentation::build` to build and verify
### Commits
- Use conventional commit format
- Keep commits focused and atomic
- Reference issues when applicable
---
## Adding New MCP Tools
1. **Choose the right module** in `src/freecad_mcp/tools/`
1. **Add the tool function** with proper docstring:
```python
@mcp.tool()
async def my_new_tool(
param1: str,
param2: int = 10,
doc_name: str | None = None,
) -> dict[str, Any]:
"""Short description of what the tool does.
Args:
param1: Description of param1.
param2: Description of param2.
doc_name: Document name. Uses active if None.
Returns:
Dictionary with result information.
"""
bridge = await get_bridge()
code = f'''
# FreeCAD Python code here
_result_ = {{"success": True}}
'''
result = await bridge.execute_python(code)
return result.result or {"success": False}
```
1. **Add tests** in the appropriate test file
1. **Update documentation** in `docs/guide/tools.md`
1. **Update capabilities resource** in `src/freecad_mcp/resources/freecad.py`
---
## Adding New Connection Modes
1. Create a new bridge class in `src/freecad_mcp/bridge/`
1. Inherit from `FreecadBridge` base class
1. Implement all abstract methods
1. Add to bridge factory in `src/freecad_mcp/bridge/__init__.py`
1. Add configuration option in `src/freecad_mcp/config.py`
1. Update documentation
---
## Release Process
Releases are automated via GitHub Actions:
1. Update the component's `RELEASE_NOTES.md` file (see [Releasing](releasing.md) for details)
1. Bump versions for workbench/macros (MCP Server auto-bumps from tag)
1. Commit and push changes
1. Create release tag with `just release::tag-<component> X.Y.Z`
1. CI builds and publishes:
- PyPI package and Docker images (MCP Server)
- GitHub Release archives (workbench and macros)
---
## Future Work
The following items are on the roadmap and welcome contributions:
### Embedded Mode Integration Tests
<!-- TODO: Add live FreeCAD integration tests for embedded mode -->
Currently, embedded mode has only mocked unit tests. Adding live integration tests would require:
1. CI workflow that runs on Linux (embedded mode is Linux-only)
1. Uses FreeCAD AppImage's bundled Python interpreter
1. Sets up `PYTHONPATH` and `LD_LIBRARY_PATH` to point to AppImage libs
1. Runs tests with `FREECAD_MODE=embedded`
**Challenge:** The AppImage bundles Python 3.11, so tests must run using that interpreter (not the system Python) to avoid ABI incompatibility.
**Reference:** See `macro-test.yaml` for how integration tests currently work with xmlrpc mode.
---
## Getting Help
- **Issues:** [GitHub Issues](https://github.com/spkane/freecad-robust-mcp-and-more/issues)
---
## License
This project is licensed under the MIT License. See [LICENSE](https://github.com/spkane/freecad-robust-mcp-and-more/blob/main/LICENSE) for details.
-570
View File
@@ -1,570 +0,0 @@
# MkDocs Documentation Guide
This project uses [MkDocs](https://www.mkdocs.org/) with the [Material theme](https://squidfunk.github.io/mkdocs-material/) for documentation. This guide covers our specific configuration, extensions, and how to use them.
## Quick Start
```bash
# Serve docs locally with live reload
just documentation::serve
# Build static site
just documentation::build
# Open docs in browser
just documentation::open
```
## Theme Configuration
We use **Material for MkDocs** with dark mode as the default. Users can toggle between dark and light modes using the sun/moon icon in the header.
**Color scheme:** Deep purple primary with purple accent.
## Markdown Extensions
### Code Blocks
#### Syntax Highlighting
Standard fenced code blocks with language hints:
````markdown
```python
def hello():
print("Hello, world!")
```
````
#### Line Numbers and Highlighting
````markdown
```python linenums="1" hl_lines="2 3"
def hello():
# These lines are highlighted
print("Hello!")
```
````
#### Code Annotations
Add numbered annotations that expand on hover:
````markdown
```python
def process(data): # (1)!
return data.strip() # (2)!
```
1. This function processes input data
2. Removes leading/trailing whitespace
````
#### Inline Code Highlighting
Use `#!python print("inline")` for inline syntax highlighting:
```markdown
Use `#!python print("inline")` for inline code with highlighting.
```
---
### Tabbed Content
Create tabs for platform-specific or alternative content:
```markdown
=== "macOS"
```bash
~/Library/Application Support/FreeCAD/Macro/
```
=== "Linux"
```bash
~/.local/share/FreeCAD/Macro/
```
=== "Windows"
```bash
%APPDATA%/FreeCAD/Macro/
```
```
**Renders as:**
<!-- markdownlint-disable MD046 -->
=== "macOS"
```bash
~/Library/Application Support/FreeCAD/Macro/
```
=== "Linux"
```bash
~/.local/share/FreeCAD/Macro/
```
=== "Windows"
```bash
%APPDATA%/FreeCAD/Macro/
```
<!-- markdownlint-enable MD046 -->
---
### Admonitions (Call-outs)
Create styled call-out boxes:
```markdown
!!! note "Optional Title"
This is a note admonition.
!!! warning
This is a warning without a custom title.
!!! danger "Critical"
This is a danger/error admonition.
!!! tip
This is a tip admonition.
!!! info
This is an info admonition.
!!! example
This is an example admonition.
```
**Collapsible admonitions:**
```markdown
??? note "Click to expand"
This content is hidden by default.
???+ note "Expanded by default"
This content is visible but can be collapsed.
```
**Available types:** `note`, `abstract`, `info`, `tip`, `success`, `question`, `warning`, `failure`, `danger`, `bug`, `example`, `quote`
---
### Task Lists
Create checkbox lists:
```markdown
- [x] Completed task
- [ ] Incomplete task
- [ ] Another task
```
**Renders as:**
- [x] Completed task
- [ ] Incomplete task
- [ ] Another task
---
### Keyboard Keys
Style keyboard shortcuts:
```markdown
Press ++ctrl+c++ to copy.
Press ++cmd+shift+p++ on macOS.
Press ++enter++ to confirm.
```
**Renders as:** Press ++ctrl+c++ to copy.
**Common keys:** `ctrl`, `alt`, `shift`, `cmd`, `enter`, `tab`, `esc`, `backspace`, `delete`, `up`, `down`, `left`, `right`, `f1`-`f12`
---
### Text Formatting
#### Highlighting
```markdown
==This text is highlighted==
```
**Renders as:** ==This text is highlighted==
#### Subscript and Superscript
```markdown
H~2~O (subscript)
x^2^ (superscript)
```
**Renders as:** H~2~O and x^2^
#### Strikethrough
```markdown
~~deleted text~~
```
**Renders as:** ~~deleted text~~
#### Critic Markup (for diffs/reviews)
```markdown
{--deleted--}
{++added++}
{~~old~>new~~}
{==highlighted==}
{>>comment<<}
```
---
### Definition Lists
```markdown
Term 1
: Definition for term 1
Term 2
: Definition for term 2
: Can have multiple definitions
```
**Renders as:**
Term 1
: Definition for term 1
Term 2
: Definition for term 2
: Can have multiple definitions
---
### Footnotes
```markdown
This needs a citation[^1].
[^1]: This is the footnote content.
```
---
### Mermaid Diagrams
Create diagrams using Mermaid syntax:
````markdown
```mermaid
graph LR
A[MCP Client] --> B[Robust MCP Server]
B --> C[FreeCAD Bridge]
C --> D[FreeCAD]
```
````
**Renders as:**
```mermaid
graph LR
A[MCP Client] --> B[Robust MCP Server]
B --> C[FreeCAD Bridge]
C --> D[FreeCAD]
```
**Supported diagram types:** flowchart, sequence, class, state, ER, gantt, pie, journey
---
## Plugins
### Git Revision Date
Pages automatically show "Last updated X ago" at the bottom. No action needed.
### Image Lightbox (GLightbox)
All images are automatically zoomable. Click any image to open in a lightbox overlay.
To exclude an image from lightbox:
```markdown
![Image](path/to/image.png){ .off-glb }
```
### Minification
HTML is automatically minified in production builds. No action needed.
### Search
Full-text search is enabled. Features:
- Search suggestions as you type
- Search result highlighting
- Shareable search URLs
---
## Macros Plugin
### Custom Delimiters
<!-- markdownlint-disable MD046 -->
!!! warning "Important"
We use **custom delimiters** to avoid conflicts with Python dict literals in code blocks.
- Variables: `{` `{@` and `@}` `}`
- Blocks: `{%` `@` and `@` `%}`
Standard Jinja2 `{` `{` `}` `}` syntax will NOT work.
<!-- markdownlint-enable MD046 -->
### Using Variables
Variables are defined in `docs/variables.yaml`:
```yaml
# docs/variables.yaml
project_name: FreeCAD Robust MCP Suite
package_name: freecad-robust-mcp
xmlrpc_port: 9875
socket_port: 9876
```
Use in Markdown:
```markdown
The default XML-RPC port is {{@ xmlrpc_port @}}.
Install with: `pip install {{@ package_name @}}`
```
### Available Variables
| Variable | Value | Description |
| ---------------------- | ------------------------- | ------------------- |
| `project_name` | FreeCAD Robust MCP Suite | Display name |
| `package_name` | freecad-robust-mcp | PyPI package name |
| `docker_image` | spkane/freecad-robust-mcp | Docker image |
| `xmlrpc_port` | 9875 | XML-RPC server port |
| `socket_port` | 9876 | Socket server port |
| `paths.macos.macro` | ~/Library/... | macOS macro path |
| `paths.linux.macro` | ~/.local/share/... | Linux macro path |
| `paths.windows.macro` | %APPDATA%/... | Windows macro path |
### Built-in Macros
```markdown
Current time: {{@ now() @}}
Page URL: {{@ page.url @}}
```
---
## API Documentation (mkdocstrings)
Auto-generate API docs from Python docstrings:
```markdown
::: freecad_mcp.server
options:
show_source: true
heading_level: 3
```
This renders the module's docstrings, classes, and functions automatically.
**Options:**
- `show_source: true` - Show source code
- `heading_level: 3` - Start headings at h3
- `members: [func1, func2]` - Only show specific members
- `filters: ["!^_"]` - Exclude private members
---
## Navigation
### Features Enabled
- **Instant navigation** - Pages load without full refresh
- **Navigation tabs** - Top-level sections as tabs
- **Section index pages** - Click section to see overview
- **Back to top** - Button appears when scrolling
- **Table of contents** - Follows scroll position
---
## Images
### Basic Image
```markdown
![Alt text](path/to/image.png)
```
### Image with Caption
```markdown
<figure markdown>
![Alt text](path/to/image.png)
<figcaption>This is the caption</figcaption>
</figure>
```
### Image Alignment
```markdown
![Image](image.png){ align=left }
![Image](image.png){ align=right }
```
### Image Size
```markdown
![Image](image.png){ width="300" }
```
---
## Links
### Internal Links
```markdown
[Installation Guide](../getting-started/installation.md)
[Config section](configuration.md#environment-variables)
```
### External Links
External links automatically open in new tab (Material theme default).
```markdown
[FreeCAD](https://www.freecad.org/)
```
---
## Tables
Standard Markdown tables with sorting enabled:
```markdown
| Column 1 | Column 2 | Column 3 |
|----------|----------|----------|
| Data 1 | Data 2 | Data 3 |
| Data 4 | Data 5 | Data 6 |
```
---
## File Structure
```text
docs/
├── index.md # Home page
├── variables.yaml # Macros variables
├── overrides/ # Theme customizations
│ └── .gitkeep
├── assets/ # Images, favicon, etc.
├── getting-started/
│ ├── installation.md
│ ├── configuration.md
│ └── quickstart.md
├── guide/
│ └── ...
├── api/
│ └── ...
└── development/
├── contributing.md
├── architecture.md
├── releasing.md
└── mkdocs-guide.md # This file
```
---
## Common Tasks
### Adding a New Page
1. Create the markdown file in the appropriate directory
2. Add to `nav:` section in `mkdocs.yaml`
### Adding Redirects
If you move/rename a page, add a redirect in `mkdocs.yaml`:
```yaml
plugins:
- redirects:
redirect_maps:
'old-page.md': 'new-location/new-page.md'
```
### Custom CSS
Add custom styles in `docs/overrides/stylesheets/extra.css` and reference in `mkdocs.yaml`:
```yaml
extra_css:
- overrides/stylesheets/extra.css
```
### Custom JavaScript
Add scripts in `docs/overrides/javascripts/extra.js` and reference in `mkdocs.yaml`:
```yaml
extra_javascript:
- overrides/javascripts/extra.js
```
---
## Troubleshooting
### Macros Not Rendering
Remember to use custom delimiters: `{{@ variable @}}` not `{{ variable }}`.
### Git Revision Date Warnings
New files not yet committed will show warnings. Commit the file to fix.
### Build Errors
```bash
# Check for syntax errors
uv run mkdocs build --strict
# Verbose output
uv run mkdocs build -v
```
### Serve Not Auto-Reloading
Some changes (like `mkdocs.yaml`) require restarting the server.
---
## Reference Links
- [MkDocs Documentation](https://www.mkdocs.org/)
- [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)
- [PyMdown Extensions](https://facelessuser.github.io/pymdown-extensions/)
- [mkdocstrings](https://mkdocstrings.github.io/)
- [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/)
-388
View File
@@ -1,388 +0,0 @@
# Release Process
This project uses **component-specific versioning**. Each component (MCP Server, Workbench) has its own version and release cycle, allowing independent updates without affecting other components.
## Quick Start
The complete release workflow in order:
```bash
# 1. Pre-release checks
just release::status # Check which components have unreleased changes
just release::changes-since mcp-server # View specific changes (or workbench)
just all # Run all quality checks (must pass)
# 2. Update release notes
just release::draft-notes mcp-server # Generate draft notes from commits
# Then edit the component's RELEASE_NOTES.md file (see "Release Notes Files" below)
# 3. Version bump (workbench only - MCP Server uses setuptools-scm)
just release::bump-workbench 1.0.0
# 4. Commit changes
git add -A
git commit -m "chore: bump workbench to 1.0.0" # or appropriate component/message
# 5. Create & push tag (triggers CI/CD automatically)
just release::tag-workbench 1.0.0 # or tag-mcp-server
# 6. Monitor release at GitHub Actions, then verify
just release::list-tags
just release::latest-versions
```
| Component | Bump Command | Tag Command |
| ---------- | ------------------------------------ | ------------------------------------ |
| MCP Server | *(none - uses setuptools-scm)* | `just release::tag-mcp-server X.Y.Z` |
| Workbench | `just release::bump-workbench X.Y.Z` | `just release::tag-workbench X.Y.Z` |
## Components and Their Release Targets
| Component | Tag Format | Releases To |
| --------------------------- | ----------------------------- | ------------------------------------------ |
| MCP Server | `robust-mcp-server-vX.Y.Z` | PyPI/TestPyPI*, Docker Hub, GitHub Release |
| Robust MCP Bridge Workbench | `robust-mcp-workbench-vX.Y.Z` | GitHub Release (archive) |
*Stable releases (`X.Y.Z`) publish to PyPI; non-stable releases (alpha, beta, rc) publish to TestPyPI only.
## Version Format
All versions follow [Semantic Versioning 2.0](https://semver.org/):
- `X.Y.Z` - Stable release (published to PyPI only)
- `X.Y.Z-alpha` or `X.Y.Z-alpha.N` - Alpha pre-release (published to TestPyPI only)
- `X.Y.Z-beta` or `X.Y.Z-beta.N` - Beta pre-release (published to TestPyPI only)
- `X.Y.Z-rc.N` - Release candidate (published to TestPyPI only)
## Release Workflow Overview
Releases follow a **two-step process**:
1. **Bump**: Update version in all source files locally, then commit
2. **Tag**: Create and push the git tag (triggers CI/CD)
This ensures the version in source files matches the git tag on the same commit.
```text
┌─────────────────────────────────────────────────────────────────┐
│ 1. just release::bump-<component> X.Y.Z │
│ └── Updates version in all relevant files │
│ │
│ 2. git add -A && git commit -m "chore: bump ... to X.Y.Z" │
│ └── Commit the version changes │
│ │
│ 3. just release::tag-<component> X.Y.Z │
│ └── Verifies versions match, then creates & pushes tag │
│ │
│ 4. GitHub Actions workflow runs automatically │
│ └── Verifies versions, builds, and publishes release │
└─────────────────────────────────────────────────────────────────┘
```
## Pre-Release Checklist
Before creating any release, complete these steps:
### 1. Check Release Status
See which components have unreleased changes:
```bash
just release::status
```
This shows the number of commits since the last release for each component.
### 2. Review Changes
View the specific changes for the component you're releasing:
```bash
# For MCP Server
just release::changes-since mcp-server
# For Workbench
just release::changes-since workbench
```
### 3. Run All Quality Checks
Ensure all tests and checks pass:
```bash
just all
```
For a more thorough check including integration tests:
```bash
just all-with-integration
```
### 4. Update Release Notes
Each component has its own `RELEASE_NOTES.md` file. Release workflows automatically extract the relevant section for GitHub Releases.
#### Release Notes Files
| Component | Release Notes File |
| --------------------------- | ----------------------------------------------- |
| MCP Server | `src/freecad_mcp/RELEASE_NOTES.md` |
| Robust MCP Bridge Workbench | `addon/FreecadRobustMCPBridge/RELEASE_NOTES.md` |
#### Draft Release Notes
Use the `draft-notes` command to generate a starting point from conventional commits:
```bash
# Generate draft notes for a component
just release::draft-notes mcp-server
just release::draft-notes workbench
```
This categorizes commits by type (feat, fix, refactor, etc.) to help you write the release notes.
#### Release Notes Format
Add a new version section **at the top** of the component's `RELEASE_NOTES.md`:
```markdown
## Version X.Y.Z (YYYY-MM-DD)
Release notes for changes between vA.B.C and vX.Y.Z.
### Added
- New feature description
### Changed
- Changed behavior description
### Fixed
- Bug fix description
```
**Important:** The version header format must match exactly for the GitHub Release workflow to extract it:
- Format: `## Version X.Y.Z (YYYY-MM-DD)`
- Example: `## Version 1.0.0 (2026-01-15)`
The release workflows automatically extract the version section and include it in the GitHub Release body.
## Releasing Each Component
### MCP Server Release
The MCP Server is the main Python package. It uses `setuptools-scm` to derive version from git tags at build time, so there's no version bump step needed.
```bash
# 1. Ensure all changes are committed
git status # Should show clean working tree
# 2. Update src/freecad_mcp/RELEASE_NOTES.md and commit
git add src/freecad_mcp/RELEASE_NOTES.md
git commit -m "docs: update release notes for MCP Server v1.0.0"
# 3. Create and push the release tag
just release::tag-mcp-server 1.0.0
```
**What happens automatically:**
1. GitHub Actions validates the tag format
2. Builds Python wheel and source distribution (version from tag)
3. Tests installation on Ubuntu and macOS
4. Publishes to PyPI (or TestPyPI for alpha, beta, and rc versions)
5. Builds multi-architecture Docker image (amd64 + arm64)
6. Pushes to Docker Hub as `spkane/freecad-robust-mcp:1.0.0`
7. Creates GitHub Release with wheel and tar.gz artifacts
**Pre-release versions:**
```bash
# Alpha (goes to TestPyPI only)
just release::tag-mcp-server 1.0.0-alpha.1
# Beta (goes to TestPyPI only)
just release::tag-mcp-server 1.0.0-beta.1
# Release candidate (goes to TestPyPI only)
just release::tag-mcp-server 1.0.0-rc.1
```
### Robust MCP Bridge Workbench Release
The workbench is a FreeCAD addon that provides the Robust MCP Bridge GUI.
```bash
# 1. Bump version in source files
just release::bump-workbench 1.0.0
# 2. Review and commit the changes
git diff # Review changes
git add -A
git commit -m "chore: bump workbench to 1.0.0"
# 3. Create and push the release tag
just release::tag-workbench 1.0.0
```
**Files updated by `bump-workbench`:**
- `addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py` (`__version__`)
- `package.xml` (workbench section: `<version>` and `<date>`)
**What happens automatically:**
1. GitHub Actions validates the tag format
2. Verifies version in source files matches tag
3. Creates archive (tar.gz and zip)
4. Creates GitHub Release with the archives
## Verifying a Release
### Check GitHub Actions
After pushing a tag, monitor the release workflow:
1. Go to [GitHub Actions](https://github.com/spkane/freecad-addon-robust-mcp-server/actions)
2. Find the workflow run triggered by your tag
3. Verify all steps complete successfully
### Verify Published Artifacts
**For MCP Server:**
```bash
# Check PyPI
pip index versions freecad-robust-mcp
# Check Docker Hub
docker pull spkane/freecad-robust-mcp:1.0.0
```
**For Workbench:**
- Check the GitHub Releases page for the archive downloads
- Verify the `package.xml` version was updated
## Managing Release Tags
### List All Tags
```bash
# All tags grouped by component
just release::list-tags
# Latest version of each component
just release::latest-versions
```
### Delete a Tag (If Needed)
If a release has issues and needs to be removed:
```bash
just release::delete-tag robust-mcp-server-v1.0.0
```
!!! warning "Deleting Published Releases"
Deleting a tag does not unpublish from PyPI or Docker Hub. Contact the respective registries to remove published artifacts if necessary.
### Preview a Release (Dry Run)
To see what a release tag would look like without creating it:
```bash
just release::dry-run-tag mcp-server 1.0.0
just release::dry-run-tag workbench 1.0.0
```
## Troubleshooting
### Release Workflow Failed
1. Check the GitHub Actions logs for the specific error
2. Common issues:
- Invalid version format (must be semver)
- Version mismatch between source files and tag
- Missing secrets (PyPI token, Docker credentials)
- Tests failing during the release build
### Version Mismatch Error
If the workflow fails with "Version mismatch":
```bash
# You forgot to bump the version before tagging
# Delete the tag
just release::delete-tag <full-tag-name>
# Bump the version
just release::bump-<component> X.Y.Z
# Commit the changes
git add -A && git commit -m "chore: bump <component> to X.Y.Z"
# Create the tag again
just release::tag-<component> X.Y.Z
```
### Tag Already Exists
If you try to create a tag that already exists:
```bash
# Delete the existing tag first
just release::delete-tag robust-mcp-server-v1.0.0
# Then create the new tag
just release::tag-mcp-server 1.0.0
```
### Wrong Version Released
1. Delete the tag: `just release::delete-tag <tag>`
2. Create a new release with the correct version
3. For PyPI: you cannot re-upload the same version; increment the patch version instead
## Release Cadence Recommendations
- **MCP Server**: Release when there are significant new features or important bug fixes
- **Workbench**: Release in sync with server changes that affect the bridge protocol
## Quick Reference
```bash
# Check what needs releasing
just release::status
# View changes for a component
just release::changes-since mcp-server
# Draft release notes from commits
just release::draft-notes mcp-server
just release::draft-notes workbench
# Bump versions (for workbench only)
just release::bump-workbench 1.0.0
# Commit version changes
git add -A && git commit -m "chore: bump <component> to X.Y.Z"
# Create releases (verifies versions, creates and pushes tag)
just release::tag-mcp-server 1.0.0
just release::tag-workbench 1.0.0
# List existing releases
just release::list-tags
just release::latest-versions
# Extract changelog for a version (used by CI)
just release::extract-changelog mcp-server 1.0.0
# Delete a tag if needed
just release::delete-tag <full-tag-name>
```
-162
View File
@@ -1,162 +0,0 @@
# Configuration
Configure the FreeCAD Robust MCP Server using environment variables and MCP client settings.
---
## Environment Variables
| Variable | Description | Default |
| --------------------- | ---------------------------------------------------- | ----------- |
| `FREECAD_MODE` | Connection mode: `xmlrpc`, `socket`, or `embedded` | `xmlrpc` |
| `FREECAD_PATH` | Path to FreeCAD's lib directory (embedded mode only) | Auto-detect |
| `FREECAD_SOCKET_HOST` | Socket/XML-RPC server hostname | `localhost` |
| `FREECAD_SOCKET_PORT` | JSON-RPC socket server port | `9876` |
| `FREECAD_XMLRPC_PORT` | XML-RPC server port | `9875` |
| `FREECAD_TIMEOUT_MS` | Execution timeout in ms | `30000` |
---
## Connection Modes
The Robust MCP Server supports three connection modes:
| Mode | Description | Platform Support |
| ---------- | ------------------------------------------- | ----------------------------------------- |
| `xmlrpc` | Connects to FreeCAD via XML-RPC (port 9875) | **All platforms** (recommended) |
| `socket` | Connects via JSON-RPC socket (port 9876) | **All platforms** |
| `embedded` | Imports FreeCAD directly into process | **Linux only** (crashes on macOS/Windows) |
### XML-RPC Mode (Recommended)
The default and recommended mode. Works on all platforms.
```bash
export FREECAD_MODE=xmlrpc
freecad-mcp
```
### Socket Mode
Alternative to XML-RPC using JSON-RPC over TCP sockets.
```bash
export FREECAD_MODE=socket
freecad-mcp
```
### Embedded Mode (Linux Only)
!!! warning "Linux Only"
Embedded mode only works on Linux. On macOS and Windows, it will crash because FreeCAD's `FreeCAD.so` library links to its bundled Python, which conflicts with external Python interpreters.
Embedded mode imports FreeCAD directly into the Robust MCP Server process for fastest execution.
```bash
export FREECAD_MODE=embedded
export FREECAD_PATH=/usr/lib/freecad/lib
freecad-mcp
```
**Note:** Embedded mode testing is minimal. For production use, prefer `xmlrpc` or `socket` modes.
---
## MCP Client Configuration
### Claude Code / Claude Desktop
Add to `~/.claude/claude_desktop_config.json` or a project `.mcp.json` file:
```json
{
"mcpServers": {
"freecad": {
"command": "freecad-mcp",
"env": {
"FREECAD_MODE": "xmlrpc"
}
}
}
}
```
If installed from source with mise/uv:
```json
{
"mcpServers": {
"freecad": {
"command": "/path/to/mise/shims/uv",
"args": ["run", "--project", "/path/to/freecad-robust-mcp-and-more", "freecad-mcp"],
"env": {
"FREECAD_MODE": "xmlrpc"
}
}
}
}
```
### Docker Configuration
```json
{
"mcpServers": {
"freecad": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "FREECAD_MODE=xmlrpc",
"-e", "FREECAD_SOCKET_HOST=host.docker.internal",
"spkane/freecad-robust-mcp"
]
}
}
}
```
---
## GUI vs Headless Mode
FreeCAD can run in two modes, and the Robust MCP Server works with both:
| Feature | Headless Mode | GUI Mode |
| ------------------------ | ------------- | -------- |
| Object creation | Yes | Yes |
| Boolean operations | Yes | Yes |
| Export (STEP, STL, etc.) | Yes | Yes |
| Save documents | Yes | Yes |
| Screenshots | No | Yes |
| Object colors | No | Yes |
| Object visibility | No | Yes |
| Camera control | No | Yes |
| Interactive selection | No | Yes |
### Starting FreeCAD
**GUI Mode** (for interactive work with visual feedback):
```bash
# Using just commands (from source)
just freecad::run-gui
# Or start FreeCAD normally and click "Start Bridge" in the workbench
```
**Headless Mode** (for automation, CI/CD, or when you don't need visual feedback):
```bash
# Using just commands (from source)
just freecad::run-headless
# Or run directly with FreeCADCmd
FreeCADCmd ~/.local/share/FreeCAD/Mod/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py
```
---
## Next Steps
- [Quick Start](quickstart.md) - Create your first model with AI assistance
- [Connection Modes](../guide/connection-modes.md) - Detailed guide on different connection modes
-126
View File
@@ -1,126 +0,0 @@
# Installation
This guide covers installing the FreeCAD Robust MCP Server and connecting it to your AI assistant.
---
## Requirements
- **FreeCAD** 0.21+ or 1.0+ (with Python 3.11)
- **Python 3.11** (must match FreeCAD's bundled Python version)
- An **MCP-compatible AI assistant** (Claude Code, Cursor, etc.)
---
## Installation Methods
### Method 1: pip (Recommended)
The simplest way to install the Robust MCP Server:
```bash
pip install freecad-robust-mcp
```
### Method 2: From Source (for Development)
```bash
git clone https://github.com/spkane/freecad-robust-mcp-and-more.git
cd freecad-robust-mcp-and-more
# Install mise (if not already installed)
curl https://mise.run | sh
mise trust
mise install
just setup
```
### Method 3: Docker
Run the Robust MCP Server in a container:
```bash
# Pull from Docker Hub
docker pull spkane/freecad-robust-mcp
# Or build locally
docker build -t freecad-robust-mcp .
```
**Note:** The Docker container runs the Robust MCP Server only—it does not include FreeCAD itself. You must run FreeCAD with the Robust MCP Bridge workbench on your host machine (or in a separate container) and configure the Robust MCP Server to connect via `xmlrpc` or `socket` mode.
**Why embedded mode doesn't work with Docker:** Embedded mode requires FreeCAD and the Robust MCP Server to run in the same process, which is impossible when FreeCAD runs on the host and the Robust MCP Server runs inside a Docker container. Additionally, embedded mode fails on macOS due to ABI incompatibility with FreeCAD's bundled Python libraries (`libpython3.11.dylib`). Always use `xmlrpc` or `socket` mode for Docker deployments.
---
## Installing the Robust MCP Bridge Workbench
The Robust MCP Bridge Workbench runs inside FreeCAD and provides the connection point for the Robust MCP Server.
### Via FreeCAD Addon Manager (Recommended)
1. Open FreeCAD
1. Go to **Tools > Addon Manager**
1. Search for "FreeCAD Robust MCP Suite" or "Robust MCP Bridge"
1. Click **Install**
1. Restart FreeCAD
### Manual Installation
1. Download the latest release from [GitHub Releases](https://github.com/spkane/freecad-robust-mcp-and-more/releases)
1. Extract to your FreeCAD Mod directory:
- **Linux:** `~/.local/share/FreeCAD/Mod/`
- **macOS:** `~/Library/Application Support/FreeCAD/Mod/`
- **Windows:** `%APPDATA%\FreeCAD\Mod\`
1. Restart FreeCAD
---
## Verifying Installation
After installation, verify everything is working:
### Step 1: Start FreeCAD with the Robust MCP Bridge
1. **Start FreeCAD** and select the **Robust MCP Bridge** workbench from the workbench selector dropdown
1. **Click "Start MCP Bridge"** in the toolbar (or use the MCP Bridge menu)
1. Check the FreeCAD console for confirmation messages:
```text
MCP Bridge started!
- XML-RPC: localhost:9875
- Socket: localhost:9876
```
### Step 2: Verify the Robust MCP Server
Test that the Robust MCP Server command is available:
```bash
# With pip installation
freecad-mcp --help
# With source installation
uv run freecad-mcp --help
```
### Step 3: Test the Connection
With FreeCAD running and the bridge started, you can verify connectivity:
```bash
# Quick connectivity test using curl (XML-RPC)
curl -X POST http://localhost:9875 \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><methodCall><methodName>ping</methodName></methodCall>'
```
A successful response indicates the bridge is working correctly.
---
## Next Steps
- [Configuration](configuration.md) - Set up environment variables and MCP client settings
- [Quick Start](quickstart.md) - Create your first model with AI assistance
-137
View File
@@ -1,137 +0,0 @@
# Quick Start
Get up and running with AI-assisted FreeCAD modeling in minutes.
---
## Prerequisites
Before starting, ensure you have:
1. FreeCAD installed with the Robust MCP Bridge workbench
1. The MCP server installed (`pip install freecad-robust-mcp`)
1. Your MCP client configured (see [Configuration](configuration.md))
---
## Step 1: Start FreeCAD with the MCP Bridge
### Option A: GUI Mode (Recommended for getting started)
1. Open FreeCAD
1. Switch to the **Robust MCP Bridge** workbench
1. Click **Start Bridge** in the toolbar
1. You should see: "MCP Bridge started! XML-RPC: localhost:9875, Socket: localhost:9876"
### Option B: Headless Mode (For automation)
```bash
# If installed via Addon Manager (Linux)
FreeCADCmd ~/.local/share/FreeCAD/Mod/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py
# If working from source
just freecad::run-headless
```
---
## Step 2: Connect Your AI Assistant
With FreeCAD and the MCP server configured, open your AI assistant (Claude Code, Cursor, etc.) and verify the connection:
```text
"Check the FreeCAD connection status"
```
The AI should respond with information about the connection mode, FreeCAD version, and whether GUI is available.
---
## Step 3: Create Your First Model
Try these example prompts with your AI assistant:
### Simple Box
```text
"Create a new FreeCAD document and add a box that is 20mm x 10mm x 5mm"
```
### Parametric Part with Fillet
```text
"Create a parametric bracket:
1. Start with a 50x30mm rectangular sketch
2. Extrude it 10mm
3. Add a 3mm fillet to all edges"
```
### Export for 3D Printing
```text
"Export the current model to STL format for 3D printing"
```
---
## Step 4: Explore Available Tools
The MCP server provides 82+ tools organized into categories:
| Category | Examples |
| ---------- | ---------------------------------------------------- |
| Primitives | `create_box`, `create_cylinder`, `create_sphere` |
| PartDesign | `create_sketch`, `pad_sketch`, `pocket_sketch` |
| Operations | `boolean_operation`, `fillet_edges`, `chamfer_edges` |
| Export | `export_stl`, `export_step`, `export_3mf` |
| View (GUI) | `get_screenshot`, `set_object_color` |
See the [Tools Reference](../guide/tools.md) for the complete list.
---
## Example Workflows
### Create a Mounting Bracket
```text
"Help me create a mounting bracket with:
- 60x40mm base plate, 5mm thick
- Two mounting holes (5mm diameter) at the corners
- A vertical wall 30mm tall on one edge
- 2mm fillets on all external edges"
```
### Modify an Existing Model
```text
"Open my_part.FCStd and:
1. List all the objects in the document
2. Change the height of the Pad feature from 10mm to 15mm
3. Save the document"
```
### Debug a Macro
```text
"Read the macro 'MyMacro' and explain what it does.
Then run it and show me any errors."
```
---
## Tips for Effective AI-Assisted Modeling
1. **Be specific about dimensions** - Include units (mm, cm, inches) in your requests
1. **Use parametric approaches** - Ask for PartDesign workflows instead of direct Part operations for parts you'll modify
1. **Check console output** - If something goes wrong, ask the AI to check the FreeCAD console for errors
1. **Take screenshots** - In GUI mode, ask for screenshots to verify the model looks correct
1. **Save frequently** - Ask the AI to save your document after significant changes
---
## Next Steps
- [Tools Reference](../guide/tools.md) - Complete API reference for all tools
- [User Guide](../USER_GUIDE.md) - Detailed workflows and best practices
- [Connection Modes](../guide/connection-modes.md) - Understanding connection modes
-202
View File
@@ -1,202 +0,0 @@
# Connection Modes
The FreeCAD Robust MCP Server supports multiple ways to connect to FreeCAD. Choose the mode that best fits your workflow.
---
## Overview
| Mode | Description | Platform | Best For |
| ---------- | --------------------------------- | ------------- | -------------------------------- |
| `xmlrpc` | XML-RPC protocol (port 9875) | All platforms | Production use (recommended) |
| `socket` | JSON-RPC over TCP sockets | All platforms | Alternative to XML-RPC |
| `embedded` | FreeCAD imported into MCP process | Linux only | Fastest execution, CI/automation |
---
## XML-RPC Mode (Recommended)
XML-RPC mode is the **default and recommended** connection method. It works on all platforms and provides robust, reliable communication.
### How It Works
```text
MCP Client <--stdio--> Robust MCP Server <--XML-RPC:9875--> FreeCAD
```
The Robust MCP Server communicates with FreeCAD via XML-RPC protocol on port 9875.
### Setup
1. Start FreeCAD with the Robust MCP Bridge workbench
1. Click **Start Bridge** (or it auto-starts if configured)
1. Configure the Robust MCP Server:
```bash
export FREECAD_MODE=xmlrpc
export FREECAD_XMLRPC_PORT=9875 # default
freecad-mcp
```
### Advantages
- Works on all platforms (macOS, Linux, Windows)
- Process isolation (FreeCAD crash doesn't affect Robust MCP Server)
- Supports both GUI and headless FreeCAD
---
## Socket Mode
Socket mode uses JSON-RPC over TCP sockets instead of XML-RPC.
### How It Works
```text
MCP Client <--stdio--> Robust MCP Server <--JSON-RPC:9876--> FreeCAD
```
### Setup
```bash
export FREECAD_MODE=socket
export FREECAD_SOCKET_HOST=localhost
export FREECAD_SOCKET_PORT=9876
freecad-mcp
```
### Advantages
- JSON-based protocol (easier to debug)
- Lower overhead than XML-RPC
- Works on all platforms
---
## Embedded Mode (Linux Only)
!!! danger "Platform Limitation"
Embedded mode **only works on Linux**. On macOS and Windows, it causes crashes due to Python ABI incompatibility.
Embedded mode imports FreeCAD directly into the Robust MCP Server process, providing the fastest execution.
### Why It Crashes on macOS/Windows
FreeCAD's `FreeCAD.so` library links to `@rpath/libpython3.11.dylib` (FreeCAD's bundled Python). When you try to import it from a different Python interpreter (even the same version), it causes a crash because the Python runtime state is incompatible.
### Setup (Linux Only)
```bash
export FREECAD_MODE=embedded
export FREECAD_PATH=/usr/lib/freecad/lib # Adjust for your system
freecad-mcp
```
### Advantages
- Fastest execution (no IPC overhead)
- No need to start FreeCAD separately
- Works in CI/CD environments on Linux
### Limitations
- **Linux only** - crashes on macOS and Windows
- Headless only (no GUI features)
- **Minimal testing** - embedded mode receives less testing than xmlrpc/socket modes
- Cannot access FreeCAD GUI features (screenshots, colors, etc.)
### Testing Status
Embedded mode is tested in the CI pipeline with unit tests that mock FreeCAD. However, full integration testing with actual FreeCAD is limited compared to the xmlrpc and socket modes which are tested with the FreeCAD AppImage.
---
## Choosing a Mode
```mermaid
graph TD
A[Need FreeCAD Robust MCP?] --> B{Platform?}
B -->|macOS/Windows| C[Use xmlrpc or socket]
B -->|Linux| D{Need GUI features?}
D -->|Yes| C
D -->|No| E{Need fastest execution?}
E -->|Yes| F[Consider embedded]
E -->|No| C
```
### Recommendations
| Use Case | Recommended Mode |
| ----------------------------- | ---------------- |
| General development | `xmlrpc` |
| Interactive modeling with GUI | `xmlrpc` |
| CI/CD pipelines on Linux | `embedded` |
| Docker containers | `xmlrpc` |
| Remote FreeCAD instance | `xmlrpc` |
| Debugging connection issues | `socket` |
---
## Headless vs GUI Mode
Independent of connection mode, FreeCAD itself can run in GUI or headless mode:
| Feature | Headless | GUI |
| ------------------------ | -------- | --- |
| Object creation | Yes | Yes |
| Boolean operations | Yes | Yes |
| Export (STEP, STL, etc.) | Yes | Yes |
| Screenshots | No | Yes |
| Object colors/visibility | No | Yes |
| Camera control | No | Yes |
### Starting FreeCAD
**GUI Mode:**
```bash
# Using workbench - just start FreeCAD and click "Start Bridge"
just freecad::run-gui # From source
```
**Headless Mode:**
```bash
FreeCADCmd /path/to/blocking_bridge.py
just freecad::run-headless # From source
```
---
## Troubleshooting
### Connection Refused
```text
Error: Connection refused on localhost:9875
```
**Solution:** Ensure FreeCAD is running with the Robust MCP Bridge started. Check the bridge status in FreeCAD's toolbar.
### Embedded Mode Crash on macOS
```text
SIGSEGV: Segmentation fault
```
**Solution:** Embedded mode doesn't work on macOS. Switch to `xmlrpc` or `socket` mode.
### Timeout Errors
```text
Error: Execution timed out after 30000ms
```
**Solution:** Increase the timeout with `FREECAD_TIMEOUT_MS=60000` or optimize your operation.
---
## Next Steps
- [Tools Reference](tools.md) - Complete API for all 82+ tools
- [MCP Resources](resources.md) - Query FreeCAD state via MCP resources
-150
View File
@@ -1,150 +0,0 @@
# MCP Macro Tools
The MCP server provides tools for working with FreeCAD macros programmatically.
---
## Available Tools
### list_macros
List available macros in FreeCAD's macro directories.
```python
list_macros() -> list[dict]
```
**Returns:** List of macros with name, path, description, and whether it's a system macro.
### run_macro
Execute a macro by name with optional arguments.
```python
run_macro(
macro_name: str,
args: dict | None = None
) -> dict
```
**Example prompt:**
```text
"Run the ExportSTL macro"
```
### create_macro
Create a new macro programmatically.
```python
create_macro(
name: str,
code: str,
description: str = ""
) -> dict
```
**Example prompt:**
```text
"Create a macro called 'CreateBox' that makes a 10x10x10 box"
```
### read_macro
Read the source code of an existing macro.
```python
read_macro(macro_name: str) -> dict
```
**Example prompt:**
```text
"Show me the code for my custom macro"
```
### delete_macro
Delete a user macro (system macros are protected).
```python
delete_macro(macro_name: str) -> dict
```
### create_macro_from_template
Create a macro from predefined templates.
```python
create_macro_from_template(
name: str,
template: str = "basic",
description: str = ""
) -> dict
```
**Available templates:**
| Template | Description |
| ----------- | --------------------------------- |
| `basic` | Minimal macro with imports |
| `part` | Part workbench operations |
| `sketch` | Sketcher operations |
| `gui` | GUI/dialog template |
| `selection` | Selection handling template |
**Example prompt:**
```text
"Create a new macro from the 'sketch' template called 'DrawGear'"
```
---
## Macro Development with AI
The MCP server excels at helping develop FreeCAD macros. Example workflows:
### Debugging an Existing Macro
```text
"Read the macro 'MyMacro' and explain what it does"
```
```text
"Run the macro and show me any errors from the FreeCAD console"
```
### Creating a New Macro
```text
"Create a macro that:
1. Gets all selected objects
2. Calculates their combined bounding box
3. Creates a box around them with 5mm clearance"
```
### Modifying a Macro
```text
"Read the 'ExportSTL' macro and modify it to also export STEP files"
```
---
## Best Practices for Macro Development
1. **Use templates** - Start from `create_macro_from_template` for proper imports
1. **Test incrementally** - Use `execute_python` for testing snippets before creating full macros
1. **Check console output** - Use `get_console_output` to debug issues
1. **Document your macros** - Add docstrings that explain parameters and usage
1. **Handle errors gracefully** - Wrap operations in try/except blocks
---
## Next Steps
- [Tools Reference](tools.md) - Complete API for all MCP tools
- [Workbench](workbench.md) - Robust MCP Bridge Workbench details
-335
View File
@@ -1,335 +0,0 @@
# MCP Resources
The FreeCAD Robust MCP Server exposes several resources that allow AI assistants to query FreeCAD's state without executing code.
---
## Overview
MCP Resources are read-only endpoints that provide context about FreeCAD's current state. They're useful for:
- Understanding what documents and objects exist
- Getting system information
- Discovering available capabilities
---
## Available Resources
The Robust MCP Server provides 12 resources for querying FreeCAD state:
### freecad://capabilities
Returns a comprehensive JSON catalog of all available tools, resources, and prompts.
**Use case:** Understanding what the Robust MCP Server can do.
**Example response:**
```json
{
"tools": {
"execution": ["execute_python", "get_freecad_version", ...],
"documents": ["create_document", "open_document", ...],
...
},
"resources": ["freecad://capabilities", "freecad://documents", ...],
"prompts": ["freecad-help", "create-parametric-part", ...]
}
```
### freecad://version
Gets FreeCAD version and build information.
**Use case:** Checking FreeCAD compatibility and environment.
**Example response:**
```json
{
"version": "1.0.0",
"build_date": "2024-01-15",
"python_version": "3.11.6",
"gui_available": true
}
```
### freecad://status
Gets current FreeCAD connection and runtime status.
**Use case:** Verifying connection health and mode.
**Example response:**
```json
{
"connected": true,
"mode": "xmlrpc",
"freecad_version": "1.0.0",
"gui_available": true,
"last_ping_ms": 12.5,
"error": null
}
```
### freecad://documents
Lists all open FreeCAD documents with basic information.
**Use case:** Seeing what documents are currently open.
**Example response:**
```json
[
{
"name": "MyPart",
"label": "My Part Design",
"path": "/home/user/projects/mypart.FCStd",
"is_modified": true,
"object_count": 15,
"active_object": "Pad"
}
]
```
### freecad://documents/{name}
Gets detailed information about a specific document.
**Use case:** Examining a document's contents.
**Example response:**
```json
{
"name": "MyPart",
"label": "My Part Design",
"path": "/home/user/projects/mypart.FCStd",
"objects": ["Body", "Sketch", "Pad", "Fillet"],
"is_modified": true,
"active_object": "Fillet"
}
```
### freecad://documents/{name}/objects
Gets list of objects in a specific document.
**Use case:** Listing all objects in a document with their types.
**Example response:**
```json
[
{
"name": "Body",
"label": "Body",
"type_id": "PartDesign::Body",
"visibility": true
},
{
"name": "Sketch",
"label": "Sketch",
"type_id": "Sketcher::SketchObject",
"visibility": false
}
]
```
### freecad://objects/{doc_name}/{obj_name}
Gets detailed information about a specific object including properties and shape data.
**Use case:** Inspecting object properties and geometry.
**Example response:**
```json
{
"name": "Pad",
"label": "Pad",
"type_id": "PartDesign::Pad",
"properties": {
"Length": 10.0,
"Type": "Length",
"Symmetric": false
},
"shape_info": {
"shape_type": "Solid",
"volume": 1000.0,
"area": 600.0,
"is_valid": true
},
"children": [],
"parents": ["Sketch"],
"visibility": true
}
```
### freecad://active-document
Gets the currently active document.
**Use case:** Quick access to the document the user is working on.
**Example response:**
```json
{
"name": "MyPart",
"label": "My Part Design",
"path": "/home/user/projects/mypart.FCStd",
"objects": ["Body", "Sketch", "Pad"],
"is_modified": false,
"active_object": "Pad"
}
```
### freecad://workbenches
Gets list of available FreeCAD workbenches.
**Use case:** Understanding what workbenches are available.
**Example response:**
```json
[
{
"name": "PartDesignWorkbench",
"label": "Part Design",
"is_active": true
},
{
"name": "SketcherWorkbench",
"label": "Sketcher",
"is_active": false
}
]
```
### freecad://workbenches/active
Gets the currently active workbench.
**Use case:** Knowing which workbench context is active.
**Example response:**
```json
{
"name": "PartDesignWorkbench",
"label": "Part Design"
}
```
### freecad://macros
Gets list of available FreeCAD macros.
**Use case:** Discovering available automation macros.
**Example response:**
```json
[
{
"name": "ExportSTL",
"path": "/home/user/.local/share/FreeCAD/Macro/ExportSTL.FCMacro",
"description": "Export selected objects to STL",
"is_system": false
}
]
```
### freecad://console
Gets recent FreeCAD console output.
**Use case:** Debugging and seeing FreeCAD messages.
**Example response:**
```json
{
"lines": [
"MCP Bridge started!",
" - XML-RPC: localhost:9875",
" - Socket: localhost:9876",
"Document created: MyPart"
],
"count": 4
}
```
---
## Using Resources in Prompts
When talking to an AI assistant connected via MCP, resources are automatically available. The AI can read them to understand context.
**Example conversation:**
```text
User: "What documents do I have open?"
AI: [Reads freecad://documents resource]
"You have two documents open:
1. 'Bracket' - modified, 12 objects
2. 'Housing' - saved, 8 objects"
```
---
## Resources vs Tools
| Aspect | Resources | Tools |
| ------------ | ----------------------- | -------------------- |
| Purpose | Query state (read-only) | Perform actions |
| Side effects | None | May modify documents |
| Response | Data/text | Operation result |
| Example | `freecad://documents` | `create_document()` |
---
## Resource URI Summary
| URI | Description |
| ----------------------------------------- | ---------------------------------------- |
| `freecad://capabilities` | All available tools, resources, prompts |
| `freecad://version` | FreeCAD version and build info |
| `freecad://status` | Connection status and mode |
| `freecad://documents` | List of open documents |
| `freecad://documents/{name}` | Single document details |
| `freecad://documents/{name}/objects` | Objects in a document |
| `freecad://objects/{doc_name}/{obj_name}` | Detailed object information |
| `freecad://active-document` | Currently active document |
| `freecad://workbenches` | Available workbenches |
| `freecad://workbenches/active` | Currently active workbench |
| `freecad://macros` | Available macros |
| `freecad://console` | Recent console output |
---
## Implementing Custom Resources
If you're extending the Robust MCP Server, you can add custom resources:
```python
@mcp.resource("freecad://custom/{param}")
async def my_custom_resource(param: str) -> str:
"""Return custom data based on param."""
# Query FreeCAD and return data
result = await bridge.execute_python(f"...")
return json.dumps(result)
```
---
## Next Steps
- [Tools Reference](tools.md) - Complete API for MCP tools
- [Connection Modes](connection-modes.md) - How to connect to FreeCAD
-219
View File
@@ -1,219 +0,0 @@
# Tools Reference
The FreeCAD Robust MCP Server provides 82+ tools for CAD operations. This page provides a quick reference organized by category.
For detailed documentation including parameters and examples, see [MCP Tools Reference](../MCP_TOOLS_REFERENCE.md).
---
## Tool Categories
| Category | Tools | Description |
| ------------------------------- | ----- | ------------------------------------ |
| [Execution](#execution-tools) | 5 | Python execution, debugging |
| [Documents](#document-tools) | 7 | Document management |
| [Primitives](#primitive-tools) | 8 | Basic 3D shapes |
| [Objects](#object-tools) | 12 | Object manipulation |
| [PartDesign](#partdesign-tools) | 19 | Parametric modeling |
| [View & Display](#view-tools) | 11 | View control, screenshots (GUI only) |
| [Export/Import](#export-tools) | 7 | File format conversion |
| [Macros](#macro-tools) | 6 | Macro management |
| [Utility](#utility-tools) | 7 | Undo/redo, parts library |
---
## Execution Tools
| Tool | Description |
| ---------------------------- | ------------------------------------------ |
| `execute_python` | Execute arbitrary Python in FreeCAD |
| `get_freecad_version` | Get FreeCAD version and build info |
| `get_connection_status` | Check MCP bridge connection |
| `get_console_output` | Get recent console output |
| `get_mcp_server_environment` | Get Robust MCP Server environment info |
---
## Document Tools
| Tool | Description |
| --------------------- | ----------------------------- |
| `list_documents` | List all open documents |
| `get_active_document` | Get currently active document |
| `create_document` | Create a new document |
| `open_document` | Open an existing .FCStd file |
| `save_document` | Save a document |
| `close_document` | Close a document |
| `recompute_document` | Recompute all features |
---
## Primitive Tools
| Tool | Description |
| ----------------- | ---------------------------- |
| `create_box` | Create a parametric box |
| `create_cylinder` | Create a parametric cylinder |
| `create_sphere` | Create a parametric sphere |
| `create_cone` | Create a parametric cone |
| `create_torus` | Create a torus (donut) |
| `create_wedge` | Create a tapered wedge |
| `create_helix` | Create a helix curve |
| `create_object` | Create any object by type ID |
---
## Object Tools
| Tool | Description |
| ------------------- | -------------------------------- |
| `list_objects` | List objects in a document |
| `inspect_object` | Get detailed object information |
| `edit_object` | Modify object properties |
| `delete_object` | Delete an object |
| `boolean_operation` | Union, cut, or intersect objects |
| `set_placement` | Set position and rotation |
| `rotate_object` | Rotate around an axis |
| `scale_object` | Scale uniformly or non-uniformly |
| `copy_object` | Create a copy |
| `mirror_object` | Mirror across a plane |
| `get_selection` | Get selected objects (GUI) |
| `set_selection` | Select objects (GUI) |
| `clear_selection` | Clear selection (GUI) |
---
## PartDesign Tools
### Bodies and Sketches
| Tool | Description |
| ------------------------ | ------------------------------- |
| `create_partdesign_body` | Create a PartDesign body |
| `create_sketch` | Create a sketch on a plane/face |
### Sketch Geometry
| Tool | Description |
| ---------------------- | ----------------------- |
| `add_sketch_rectangle` | Add rectangle to sketch |
| `add_sketch_circle` | Add circle to sketch |
| `add_sketch_line` | Add line to sketch |
| `add_sketch_arc` | Add arc to sketch |
| `add_sketch_point` | Add point to sketch |
### Additive Features
| Tool | Description |
| ------------------- | ------------------------------ |
| `pad_sketch` | Extrude sketch (additive) |
| `revolution_sketch` | Revolve sketch around axis |
| `loft_sketches` | Loft through multiple sketches |
| `sweep_sketch` | Sweep profile along path |
### Subtractive Features
| Tool | Description |
| --------------- | ----------------------- |
| `pocket_sketch` | Cut by extruding sketch |
| `groove_sketch` | Cut by revolving sketch |
| `create_hole` | Create parametric holes |
### Edge Operations & Patterns
| Tool | Description |
| ------------------ | --------------------------- |
| `fillet_edges` | Add rounded edges |
| `chamfer_edges` | Add beveled edges |
| `linear_pattern` | Repeat feature linearly |
| `polar_pattern` | Repeat feature circularly |
| `mirrored_feature` | Mirror feature across plane |
---
## View Tools
!!! warning "GUI Mode Required"
Tools marked with **GUI** only work when FreeCAD is running in GUI mode.
| Tool | Mode | Description |
| ----------------------- | ---- | ---------------------------------- |
| `get_screenshot` | GUI | Capture 3D view screenshot |
| `set_view_angle` | Both | Set camera angle |
| `fit_all` | Both | Fit all objects in view |
| `zoom_in` | GUI | Zoom in |
| `zoom_out` | GUI | Zoom out |
| `set_camera_position` | GUI | Set exact camera position |
| `set_object_visibility` | GUI | Show/hide objects |
| `set_display_mode` | GUI | Set display mode (wireframe, etc.) |
| `set_object_color` | GUI | Change object color |
| `list_workbenches` | Both | List available workbenches |
| `activate_workbench` | Both | Switch workbench |
---
## Export Tools
| Tool | Description |
| ------------- | ---------------------------------- |
| `export_step` | Export to STEP format |
| `export_stl` | Export to STL (3D printing) |
| `export_3mf` | Export to 3MF (modern 3D printing) |
| `export_obj` | Export to OBJ format |
| `export_iges` | Export to IGES format |
| `import_step` | Import STEP files |
| `import_stl` | Import STL files |
---
## Macro Tools
| Tool | Description |
| ---------------------------- | ------------------------------- |
| `list_macros` | List available macros |
| `run_macro` | Execute a macro |
| `create_macro` | Create a new macro |
| `read_macro` | Read macro source code |
| `delete_macro` | Delete a user macro |
| `create_macro_from_template` | Create from predefined template |
---
## Utility Tools
| Tool | Description |
| -------------------------- | --------------------------- |
| `undo` | Undo last operation |
| `redo` | Redo undone operation |
| `get_undo_redo_status` | Get undo/redo availability |
| `recompute` | Force recompute all objects |
| `get_console_log` | Get console log with levels |
| `list_parts_library` | List parts library |
| `insert_part_from_library` | Insert part from library |
---
## GUI vs Headless Mode
When running in headless mode, GUI-only tools return structured errors instead of crashing:
```json
{
"success": false,
"error": "GUI not available - screenshots cannot be captured in headless mode"
}
```
To check the current mode programmatically:
```python
result = await execute_python("_result_ = FreeCAD.GuiUp")
is_gui_mode = result["result"]
```
---
## Next Steps
- [MCP Tools Reference](../MCP_TOOLS_REFERENCE.md) - Detailed documentation with parameters and examples
- [MCP Resources](resources.md) - Query FreeCAD state via MCP resources
-260
View File
@@ -1,260 +0,0 @@
# Robust MCP Bridge Workbench
The Robust MCP Bridge Workbench is a FreeCAD addon that provides the server-side connection point for the Robust MCP Server. It runs inside FreeCAD and exposes XML-RPC and JSON-RPC interfaces.
---
## Overview
The workbench provides:
- **Toolbar controls** for starting/stopping the MCP bridge
- **Status indicator** showing connection state
- **XML-RPC server** on port 9875 (default)
- **JSON-RPC socket server** on port 9876 (default)
- **Headless mode support** for automation and CI/CD
---
## Installation
### Via FreeCAD Addon Manager (Recommended)
1. Open FreeCAD
1. Go to **Tools > Addon Manager**
1. Search for "FreeCAD Robust MCP Suite" or "Robust MCP Bridge"
1. Click **Install**
1. Restart FreeCAD
### Manual Installation
Download from [GitHub Releases](https://github.com/spkane/freecad-robust-mcp-and-more/releases) and extract to your FreeCAD Mod directory:
- **Linux:** `~/.local/share/FreeCAD/Mod/FreecadRobustMCPBridge/`
- **macOS:** `~/Library/Application Support/FreeCAD/Mod/FreecadRobustMCPBridge/`
- **Windows:** `%APPDATA%\FreeCAD\Mod\FreecadRobustMCPBridge\`
---
## GUI Mode Usage
### Starting the Bridge
1. Switch to the **MCP Bridge** workbench in FreeCAD
1. Click **Start Bridge** in the toolbar
1. The status indicator turns green when running
You'll see a confirmation message:
```text
MCP Bridge started!
- XML-RPC: localhost:9875
- Socket: localhost:9876
```
### Stopping the Bridge
Click **Stop Bridge** in the toolbar. The status indicator turns red.
### Status Indicator
| Color | Status |
| ------ | ------------------------------------- |
| Green | Bridge running, accepting connections |
| Red | Bridge stopped |
| Yellow | Bridge starting/stopping |
---
## Headless Mode Usage
The workbench includes a blocking bridge script for running in server mode (keeps FreeCAD running).
### Starting Headless Mode
**Linux:**
```bash
freecadcmd ~/.local/share/FreeCAD/Mod/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py
```
**macOS:**
```bash
/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd \
~/Library/Application\ Support/FreeCAD/Mod/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py
```
**Using just commands (from source):**
```bash
just freecad::run-headless
```
### Headless Output
```text
FreeCAD version: 1.0.0
============================================================
MCP Bridge started in headless mode!
- XML-RPC: localhost:9875
- Socket: localhost:9876
Note: Screenshot and view features are not available in headless mode.
Press Ctrl+C to stop.
============================================================
```
---
## Features Available by Mode
| Feature | GUI Mode | Headless Mode |
| ------------------------ | -------- | ------------- |
| Object creation | Yes | Yes |
| Boolean operations | Yes | Yes |
| Export (STEP, STL, etc.) | Yes | Yes |
| Macro execution | Yes | Yes |
| Document management | Yes | Yes |
| Screenshots | Yes | **No** |
| Object colors | Yes | **No** |
| Object visibility | Yes | **No** |
| Camera/view control | Yes | **No** |
| Interactive selection | Yes | **No** |
!!! info "GUI-Only Features"
When a GUI-only feature is requested in headless mode, the Robust MCP Server returns a structured error response instead of crashing: `{"success": false, "error": "GUI not available - screenshots cannot be captured in headless mode"}`
---
## Configuration
### Workbench Preferences (FreeCAD Side)
The workbench has its own preferences that control how the bridge runs inside FreeCAD. Access them via:
- **Edit → Preferences → Robust MCP Bridge** (in FreeCAD's main Preferences dialog)
- **Robust MCP Bridge → MCP Bridge Preferences...** (from the workbench menu)
| Setting | Description | Default |
| --------------------- | -------------------------------------------- | -------- |
| Auto-start bridge | Start bridge automatically on FreeCAD launch | Disabled |
| Show status indicator | Display status in FreeCAD's status bar | Enabled |
| XML-RPC Port | Port for XML-RPC connections | 9875 |
| Socket Port | Port for JSON-RPC socket connections | 9876 |
!!! note "Port Configuration"
If you change the ports in the workbench preferences while the bridge is running, it will automatically restart with the new configuration.
### MCP Server Configuration (Client Side)
The external Robust MCP Server (used by Claude Code, etc.) is configured separately using environment variables. **These must match the workbench ports:**
| Environment Variable | Description | Default |
| --------------------- | ---------------------------------------------- | ----------- |
| `FREECAD_MODE` | Connection mode: `xmlrpc`, `socket`, `embedded`| `xmlrpc` |
| `FREECAD_XMLRPC_PORT` | XML-RPC server port | 9875 |
| `FREECAD_SOCKET_PORT` | JSON-RPC socket server port | 9876 |
| `FREECAD_SOCKET_HOST` | Socket/XML-RPC server hostname | `localhost` |
Example MCP client configuration with custom ports:
```json
{
"mcpServers": {
"freecad": {
"command": "freecad-mcp",
"env": {
"FREECAD_MODE": "xmlrpc",
"FREECAD_XMLRPC_PORT": "9877"
}
}
}
}
```
!!! info "Choosing a Connection Mode"
- **`xmlrpc`** (recommended): Most reliable, works on all platforms. Connects to FreeCAD via XML-RPC protocol.
- **`socket`**: Alternative protocol using JSON-RPC over TCP sockets. Also works on all platforms.
- **`embedded`**: Direct Python import of FreeCAD (Linux only). Does not require the workbench but crashes on macOS due to library linking issues. Not recommended for production use.
!!! warning "Port Matching Required"
The ports configured in the MCP Server (via environment variables) **must match** the ports configured in the FreeCAD workbench preferences. If they don't match, the server won't be able to connect to FreeCAD.
---
## Architecture
```text
┌─────────────────────────────────────────────────────────────┐
│ FreeCAD (GUI or Headless) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Robust MCP Bridge Workbench/Plugin │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ XML-RPC Server │ │ Socket Server │ │ │
│ │ │ (port 9875) │ │ (port 9876) │ │ │
│ │ └────────┬────────┘ └────────┬────────┘ │ │
│ │ │ │ │ │
│ │ └────────┬───────────┘ │ │
│ │ │ │ │
│ │ ┌────────▼────────┐ │ │
│ │ │ FreecadMCPPlugin│ │ │
│ │ │ (Thread-safe │ │ │
│ │ │ queue system) │ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ┌────────▼────────┐ │ │
│ │ │ FreeCAD Python │ │ │
│ │ │ Console │ │ │
│ │ └─────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ Network (localhost)
┌─────────────────────────────────────────────────────────────┐
│ FreeCAD Robust MCP Server (External Process) │
└─────────────────────────────────────────────────────────────┘
```
The workbench uses a **queue-based thread safety system** to ensure FreeCAD operations run on the main GUI thread, preventing crashes from thread-unsafe operations.
---
## Troubleshooting
### Bridge Won't Start
**Problem:** Clicking "Start Bridge" does nothing or shows an error.
**Solution:**
1. Check the FreeCAD Python console for error messages
1. Ensure no other process is using ports 9875/9876
1. Try restarting FreeCAD
### Connection Refused from Robust MCP Server
**Problem:** Robust MCP Server reports "Connection refused"
**Solution:**
1. Verify the bridge is running (green status indicator)
1. Check that ports match between workbench and Robust MCP Server config
1. If using Docker, ensure you're using `host.docker.internal` as the host
### Headless Mode Hangs
**Problem:** `FreeCADCmd` with headless server never outputs anything
**Solution:**
1. Ensure you're using `FreeCADCmd` (not `freecad`)
1. Check the script path is correct
1. Try running with `-c "print('test')"` first to verify FreeCAD works
---
## Macro Tools
The MCP server provides tools for working with FreeCAD macros. See [Macros](macros.md) for details on using macro tools.
-98
View File
@@ -1,98 +0,0 @@
# FreeCAD Robust MCP Suite
Welcome to the FreeCAD Robust MCP Suite documentation.
This project provides an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server and FreeCAD workbench that enable integration between AI assistants (Claude, GPT, and other MCP-compatible tools) and [FreeCAD](https://www.freecadweb.org/), allowing AI-assisted development and debugging of 3D models, macros, and workbenches.
---
## Features
- **82+ MCP Tools** - Comprehensive CAD operations including primitives, PartDesign, booleans, export
- **Multiple Connection Modes** - XML-RPC (recommended), JSON-RPC socket, or embedded (Linux only)
- **GUI & Headless Support** - Full modeling in headless mode, plus screenshots/colors in GUI mode
- **Macro Development** - Create, edit, run, and template FreeCAD macros via MCP
---
## Quick Start
```bash
# Install the Robust MCP Server
pip install freecad-robust-mcp
# Install the workbench via FreeCAD Addon Manager
# (search for "Robust MCP" - the package is "FreeCAD Robust MCP Suite")
# Start FreeCAD and switch to the "Robust MCP Bridge" workbench
# Click "Start Bridge" in the toolbar
# Configure your MCP client and start building!
```
See [Installation](getting-started/installation.md) for detailed setup instructions.
---
## Connection Modes
| Mode | Description | Platform |
| ---------- | ---------------------------- | --------------------------- |
| `xmlrpc` | XML-RPC protocol (port 9875) | All platforms (recommended) |
| `socket` | JSON-RPC socket (port 9876) | All platforms |
| `embedded` | In-process FreeCAD | Linux only |
See [Connection Modes](guide/connection-modes.md) for details on choosing the right mode.
---
## GUI vs Headless Mode
The Robust MCP Server works with FreeCAD in both GUI and headless mode:
| Feature | Headless | GUI |
| ------------------------ | -------- | --- |
| Object creation | Yes | Yes |
| Boolean operations | Yes | Yes |
| Export (STEP, STL, etc.) | Yes | Yes |
| Screenshots | No | Yes |
| Object colors/visibility | No | Yes |
| Camera control | No | Yes |
---
## FreeCAD Macros
The MCP server provides tools for working with FreeCAD macros:
- **List macros** - Discover available macros in your FreeCAD installation
- **Run macros** - Execute macros with parameter passing
- **Create macros** - Generate new macros from templates or custom code
See [Macros Guide](guide/macros.md) for details on using macros with the MCP server.
---
## Documentation
| Section | Description |
| -------------------------------------------------- | ------------------------------------------------- |
| [Getting Started](getting-started/installation.md) | Installation, configuration, and quick start |
| [User Guide](guide/connection-modes.md) | Connection modes, workbench, macros, and tools |
| [Tools Reference](MCP_TOOLS_REFERENCE.md) | Complete API reference for all 82+ MCP tools |
| [API Reference](api/server.md) | Python API documentation |
| [Development](development/contributing.md) | Contributing, architecture, and development setup |
| [Comparison](COMPARISON.md) | Compare with other FreeCAD MCP implementations |
---
## Links
- [GitHub Repository](https://github.com/spkane/freecad-robust-mcp-and-more) - Source code and issue tracker
- [PyPI Package](https://pypi.org/project/freecad-robust-mcp/) - Python package for pip installation
- [Docker Hub](https://hub.docker.com/r/spkane/freecad-robust-mcp) - Pre-built Docker images
---
!!! tip "Share This Documentation"
Direct link: **[https://spkane.github.io/freecad-robust-mcp-and-more/](https://spkane.github.io/freecad-robust-mcp-and-more/)**
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Redirecting</title>
<noscript>
<meta http-equiv="refresh" content="1; url=latest/" />
</noscript>
<script>
window.location.replace(
"latest/" + window.location.search + window.location.hash
);
</script>
</head>
<body>
Redirecting to <a href="latest/">latest/</a>...
</body>
</html>
-210
View File
@@ -1,210 +0,0 @@
# CodeRabbit CLI commands
# Usage: just coderabbit::install, just coderabbit::review, etc.
#
# CodeRabbit CLI provides AI-powered code reviews in your terminal.
# https://www.coderabbit.ai/cli
#
# IMPORTANT - RATE LIMITS:
# - Free tier: 1 review per hour
# - Pro tier: 5 reviews per hour
# Run reviews manually when needed to avoid hitting limits.
#
# Note: In CI, skip CodeRabbit CLI since the GitHub App handles PR reviews.
# These commands are for local, on-demand development workflow only.
# =============================================================================
# Installation
# =============================================================================
# Install CodeRabbit CLI (prefers Homebrew on macOS/Linux, falls back to official installer)
#
# SECURITY NOTE:
# - Homebrew (preferred): Uses Homebrew's cask verification and signed binaries
# from https://formulae.brew.sh/cask/coderabbit
# - Fallback installer: Downloads from https://cli.coderabbit.ai/install.sh
# The installer script is from CodeRabbit's official domain and installs
# signed binaries. We download to a temp file first for inspection if needed.
#
# Trust decision: CodeRabbit is a well-known code review service with a public
# GitHub organization (https://github.com/coderabbitai). The CLI is optional
# and only used for local development reviews.
install:
#!/usr/bin/env bash
set -euo pipefail
echo "Installing CodeRabbit CLI..."
# Prefer Homebrew if available (macOS and Linux)
if command -v brew &>/dev/null; then
echo "Using Homebrew to install CodeRabbit CLI..."
brew install --cask coderabbit
echo ""
echo "CodeRabbit CLI installed via Homebrew."
echo "Run 'just coderabbit::login' to authenticate."
exit 0
fi
# Fallback: Download installer to temp file first (allows inspection)
echo "Homebrew not found. Using official installer..."
INSTALLER_URL="https://cli.coderabbit.ai/install.sh"
TEMP_SCRIPT=$(mktemp /tmp/coderabbit-install.XXXXXX.sh)
echo "Downloading installer to: $TEMP_SCRIPT"
curl -fsSL "$INSTALLER_URL" -o "$TEMP_SCRIPT"
echo "Installer downloaded. You can inspect it at: $TEMP_SCRIPT"
echo "Executing installer..."
sh "$TEMP_SCRIPT"
# Clean up
rm -f "$TEMP_SCRIPT"
echo ""
echo "CodeRabbit CLI installed. Run 'just coderabbit::login' to authenticate."
# Check if CodeRabbit CLI is installed (silent check)
check-installed:
@command -v coderabbit >/dev/null 2>&1 || { echo "CodeRabbit CLI not installed. Run: just coderabbit::install"; exit 1; }
# =============================================================================
# Authentication
# =============================================================================
# Login to CodeRabbit (opens browser for authentication)
login: check-installed
coderabbit auth login
# Logout from CodeRabbit
logout: check-installed
coderabbit auth logout
# Check authentication status
auth-status: check-installed
coderabbit auth status
# =============================================================================
# Code Reviews
# =============================================================================
# Review staged changes (preserves user's staging state)
review: check-installed
#!/usr/bin/env bash
set -euo pipefail
echo "Reviewing staged changes..."
# Check if there are staged changes
if ! git diff --cached --quiet; then
# Stash unstaged changes, keeping staged changes in working tree
# This allows coderabbit to review only what's staged
STASH_OUTPUT=$(git stash push --keep-index -m "coderabbit-review-temp" 2>&1) || true
# Run the review on staged changes
coderabbit review --plain --type uncommitted || true
# Restore unstaged changes if we stashed anything
if [[ "$STASH_OUTPUT" != "No local changes to save" ]]; then
git stash pop --quiet || true
fi
else
echo "No staged changes to review. Stage changes with 'git add' first."
echo "Or use 'just coderabbit::review-all' to review all uncommitted changes."
fi
# Review staged changes with auto-fix suggestions (preserves user's staging state)
review-fix: check-installed
#!/usr/bin/env bash
set -euo pipefail
echo "Reviewing staged changes with auto-fix..."
# Check if there are staged changes
if ! git diff --cached --quiet; then
# Stash unstaged changes, keeping staged changes in working tree
STASH_OUTPUT=$(git stash push --keep-index -m "coderabbit-review-temp" 2>&1) || true
# Run the review with auto-fix on staged changes
coderabbit review --plain --type uncommitted --auto-fix || true
# Restore unstaged changes if we stashed anything
if [[ "$STASH_OUTPUT" != "No local changes to save" ]]; then
git stash pop --quiet || true
fi
else
echo "No staged changes to review. Stage changes with 'git add' first."
fi
# Review ALL uncommitted changes (staged + unstaged)
review-all: check-installed
@echo "Reviewing all uncommitted changes..."
coderabbit review --plain --type uncommitted
# Review the last commit
review-last: check-installed
coderabbit review --plain --type committed --base-commit HEAD~1
# Review changes since a specific commit (usage: just coderabbit::review-since abc123)
review-since commit: check-installed
coderabbit review --plain --type committed --base-commit {{commit}}
# Review changes between current branch and main
review-branch: check-installed
coderabbit review --plain --type committed --base-commit main
# =============================================================================
# Output Formats
# =============================================================================
# Generate prompt-only output for staged changes (for AI agents like Claude Code)
prompt-only: check-installed
#!/usr/bin/env bash
set -euo pipefail
# Check if there are staged changes
if ! git diff --cached --quiet; then
# Stash unstaged changes, keeping staged changes in working tree
STASH_OUTPUT=$(git stash push --keep-index -m "coderabbit-prompt-temp" 2>&1) || true
# Generate prompt for staged changes
coderabbit review --prompt-only --type uncommitted || true
# Restore unstaged changes if we stashed anything
if [[ "$STASH_OUTPUT" != "No local changes to save" ]]; then
git stash pop --quiet || true
fi
else
echo "No staged changes. Stage changes with 'git add' first."
fi
# Review staged changes with JSON output
review-json: check-installed
#!/usr/bin/env bash
set -euo pipefail
# Check if there are staged changes
if ! git diff --cached --quiet; then
# Stash unstaged changes, keeping staged changes in working tree
STASH_OUTPUT=$(git stash push --keep-index -m "coderabbit-json-temp" 2>&1) || true
# Run review with JSON output
coderabbit review --format json --type uncommitted || true
# Restore unstaged changes if we stashed anything
if [[ "$STASH_OUTPUT" != "No local changes to save" ]]; then
git stash pop --quiet || true
fi
else
echo "No staged changes. Stage changes with 'git add' first."
fi
# =============================================================================
# Help & Info
# =============================================================================
# Show help for all CodeRabbit commands
help: check-installed
coderabbit --help
# Show CodeRabbit CLI version
version: check-installed
@coderabbit --version
-80
View File
@@ -1,80 +0,0 @@
# Development utility commands
# Usage: just dev::clean, just dev::repl, etc.
#
# Miscellaneous development tools and utilities.
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# =============================================================================
# Setup & Dependencies
# =============================================================================
# Install all project dependencies (Python packages + dev tools)
install-deps:
cd {{project_root}} && uv sync --all-extras
@echo ""
@echo "Dependencies installed!"
@echo ""
@echo "To run the Robust MCP Server:"
@echo " just mcp::run # stdio mode"
@echo " just mcp::run-debug # with debug logging"
@echo " just mcp::run-http # HTTP mode for remote access"
# Install pre-commit hooks (for git commit/push integration)
install-pre-commit:
cd {{project_root}} && uv run pre-commit install
cd {{project_root}} && uv run pre-commit install --hook-type commit-msg
# Update all dependencies to latest versions (mise tools, uv.lock, pre-commit hooks)
update-deps:
#!/usr/bin/env bash
set -euo pipefail
cd "{{project_root}}"
echo "Updating mise-managed tools..."
mise upgrade
echo ""
echo "Updating Python dependencies..."
uv lock --upgrade
uv sync --all-extras
echo ""
echo "Updating pre-commit hooks..."
uv run pre-commit autoupdate
echo ""
echo "All dependencies updated!"
echo " - mise tools: updated (see .mise.toml)"
echo " - Python deps: updated (see uv.lock)"
echo " - pre-commit hooks: updated (see .pre-commit-config.yaml)"
# =============================================================================
# Development Utilities
# =============================================================================
# Clean build artifacts and caches
clean:
rm -rf {{project_root}}/.pytest_cache {{project_root}}/.mypy_cache {{project_root}}/.ruff_cache {{project_root}}/.coverage {{project_root}}/htmlcov {{project_root}}/dist {{project_root}}/build
find {{project_root}} -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find {{project_root}} -type f -name "*.pyc" -delete 2>/dev/null || true
# Open Python REPL with project modules available
repl:
cd {{project_root}} && uv run python -c "import freecad_mcp; print('FreeCAD Robust MCP loaded')" && uv run python
# Show project structure (tree view, requires 'tree' command)
tree:
#!/usr/bin/env bash
if ! command -v tree &> /dev/null; then
echo "The 'tree' command is not installed."
echo "Install it with: brew install tree (macOS) or apt install tree (Linux)"
exit 1
fi
cd "{{project_root}}" && tree -I '__pycache__|*.egg-info|.git|.mypy_cache|.pytest_cache|.ruff_cache|htmlcov|dist|build|.venv|site' -a
# Validate project configuration files (pyproject.toml, etc.)
validate:
cd {{project_root}} && uv pip check
@echo "Package dependencies are valid."
-374
View File
@@ -1,374 +0,0 @@
# Docker build and run commands
# Usage: just docker::build, just docker::build-all, etc.
# Default Docker image name (matches Docker Hub and PyPI package name)
image_name := "freecad-robust-mcp"
registry := "spkane"
gui_test_image := "freecad-gui-test"
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# Build Docker image for local architecture only
build:
docker build -t {{image_name}} {{project_root}}
# Build Docker image with specific tag
build-tag tag:
docker build --load -t {{image_name}}:{{tag}} {{project_root}}
# Validate multi-architecture build (amd64 and arm64)
# This is a dry-run that verifies both architectures compile successfully.
# The build populates the builder cache but does NOT produce a usable image.
# Use cases:
# - CI validation before pushing (verify PR doesn't break either arch)
# - Local verification that changes build on both architectures
# To actually publish a multi-arch image, use: just docker::build-push
build-multi: setup-buildx
docker buildx build --platform linux/amd64,linux/arm64 -t {{image_name}} {{project_root}}
# Build and push multi-architecture image to registry (produces usable multi-arch image)
build-push tag="latest": setup-buildx
docker buildx build --platform linux/amd64,linux/arm64 \
-t {{registry}}/{{image_name}}:{{tag}} \
--push {{project_root}}
# Build and load image for current architecture using buildx
build-load: setup-buildx
#!/usr/bin/env bash
set -euo pipefail
# Detect current architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64) PLATFORM="linux/amd64" ;;
aarch64|arm64) PLATFORM="linux/arm64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
echo "Building for detected architecture: $PLATFORM"
docker buildx build --platform "$PLATFORM" \
-t {{image_name}} \
--load {{project_root}}
# Run the Docker container (connects to FreeCAD on host)
run:
docker run --rm -i \
--add-host=host.docker.internal:host-gateway \
-e FREECAD_MODE=xmlrpc \
-e FREECAD_SOCKET_HOST=host.docker.internal \
{{image_name}}
# Run with custom environment variables
run-env *args:
docker run --rm -i \
--add-host=host.docker.internal:host-gateway \
{{args}} \
{{image_name}}
# Run container interactively with shell
shell:
docker run --rm -it \
--add-host=host.docker.internal:host-gateway \
-e FREECAD_MODE=xmlrpc \
-e FREECAD_SOCKET_HOST=host.docker.internal \
--entrypoint /bin/sh \
{{image_name}}
# Show image size and layers
inspect:
docker images {{image_name}}
@echo ""
docker history {{image_name}}
# Remove local Docker image
clean:
docker rmi {{image_name}} 2>/dev/null || true
docker rmi {{registry}}/{{image_name}} 2>/dev/null || true
# Remove Docker images and build cache
clean-all:
docker rmi {{image_name}} 2>/dev/null || true
docker rmi {{registry}}/{{image_name}} 2>/dev/null || true
docker builder prune -f
@echo "Docker images and build cache cleaned."
# Scan Docker image for vulnerabilities (warn on all severities)
# Uses .trivyignore file to skip CVEs without available fixes
scan:
#!/usr/bin/env bash
set -euo pipefail
IGNOREFILE="{{project_root}}/.trivyignore"
IGNORE_FLAG=""
if [ -f "$IGNOREFILE" ]; then
IGNORE_FLAG="--ignorefile $IGNOREFILE"
echo "Using ignore file: $IGNOREFILE"
fi
echo "Scanning {{image_name}} for vulnerabilities..."
echo ""
trivy image --severity HIGH,CRITICAL $IGNORE_FLAG {{image_name}}
echo ""
echo "Scanning for MEDIUM/LOW (informational)..."
trivy image --severity MEDIUM,LOW $IGNORE_FLAG {{image_name}} || true
# Scan Docker image with strict settings (fail on HIGH or CRITICAL)
# Uses .trivyignore file to skip CVEs without available fixes
scan-strict:
#!/usr/bin/env bash
set -euo pipefail
IGNOREFILE="{{project_root}}/.trivyignore"
IGNORE_FLAG=""
if [ -f "$IGNOREFILE" ]; then
IGNORE_FLAG="--ignorefile $IGNOREFILE"
echo "Using ignore file: $IGNOREFILE"
fi
echo "Scanning {{image_name}} for HIGH/CRITICAL vulnerabilities..."
echo "(Build will fail if any HIGH or CRITICAL CVEs are found)"
echo ""
trivy image --severity HIGH,CRITICAL --exit-code 1 $IGNORE_FLAG {{image_name}}
echo ""
echo "✓ No HIGH or CRITICAL vulnerabilities found!"
echo ""
echo "Scanning for MEDIUM/LOW (informational only)..."
trivy image --severity MEDIUM,LOW --exit-code 0 $IGNORE_FLAG {{image_name}} || true
# Scan Docker image WITHOUT ignore file (shows all CVEs including unfixable)
scan-all:
#!/usr/bin/env bash
set -euo pipefail
echo "Scanning {{image_name}} for ALL vulnerabilities (ignoring .trivyignore)..."
echo ""
trivy image --severity HIGH,CRITICAL {{image_name}}
echo ""
echo "Scanning for MEDIUM/LOW..."
trivy image --severity MEDIUM,LOW {{image_name}} || true
# Scan Docker image and output SARIF report
scan-sarif output="trivy-results.sarif":
#!/usr/bin/env bash
set -euo pipefail
IGNOREFILE="{{project_root}}/.trivyignore"
IGNORE_FLAG=""
if [ -f "$IGNOREFILE" ]; then
IGNORE_FLAG="--ignorefile $IGNOREFILE"
fi
trivy image --format sarif --output {{project_root}}/{{output}} $IGNORE_FLAG {{image_name}}
echo "SARIF report written to {{project_root}}/{{output}}"
# Create and configure buildx builder for multi-arch builds
setup-buildx:
#!/usr/bin/env bash
set -euo pipefail
if ! docker buildx inspect freecad-builder > /dev/null 2>&1; then
echo "Creating buildx builder 'freecad-builder'..."
docker buildx create --name freecad-builder --use --bootstrap
else
echo "Builder 'freecad-builder' already exists, using it..."
docker buildx use freecad-builder
fi
docker buildx inspect --bootstrap
# Test Docker container integration with FreeCAD
# This builds the image, starts FreeCAD with the bridge, runs the container,
# and verifies communication is working correctly.
test:
#!/usr/bin/env bash
set -euo pipefail
# Initialize variables for cleanup
STARTED_FREECAD=false
FREECAD_PID=""
FREECAD_LOG=""
MCP_INPUT=""
# Comprehensive cleanup trap for all exit paths
cleanup() {
[ -n "${MCP_INPUT:-}" ] && rm -f "$MCP_INPUT" 2>/dev/null || true
[ -n "${FREECAD_LOG:-}" ] && rm -f "$FREECAD_LOG" 2>/dev/null || true
if [ "$STARTED_FREECAD" = true ] && [ -n "${FREECAD_PID:-}" ]; then
kill "$FREECAD_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM
echo "=========================================="
echo "Docker Integration Test"
echo "=========================================="
echo ""
# Function to check if XML-RPC server is ready using a simple POST request
check_xmlrpc() {
# Send a minimal XML-RPC system.listMethods call
curl -s --max-time 2 -X POST \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
http://localhost:9875 > /dev/null 2>&1
}
# Detect timeout command (not available on macOS by default)
TIMEOUT_CMD=""
if command -v timeout >/dev/null 2>&1; then
TIMEOUT_CMD="timeout"
elif command -v gtimeout >/dev/null 2>&1; then
TIMEOUT_CMD="gtimeout" # macOS coreutils
fi
# Build the Docker image
echo "Step 1: Building Docker image..."
docker build -t {{image_name}}:test {{project_root}}
echo "✓ Docker image built successfully"
echo ""
# Check if FreeCAD bridge is already running
echo "Step 2: Checking for FreeCAD Robust MCP Bridge..."
if check_xmlrpc; then
echo "✓ FreeCAD Robust MCP Bridge is already running on port 9875"
STARTED_FREECAD=false
else
echo "FreeCAD Robust MCP Bridge not detected. Starting FreeCAD headless..."
echo " (This may take 30-60 seconds for FreeCAD to initialize...)"
# Start FreeCAD headless in background, capturing output
FREECAD_LOG=$(mktemp)
just freecad::run-headless > "$FREECAD_LOG" 2>&1 &
FREECAD_PID=$!
# Wait for bridge to be ready (longer timeout for FreeCAD startup)
MAX_RETRIES=60
RETRY_COUNT=0
while ! check_xmlrpc; do
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
echo "✗ ERROR: FreeCAD Robust MCP Bridge did not start within ${MAX_RETRIES}s"
echo ""
echo "FreeCAD log output:"
tail -30 "$FREECAD_LOG"
exit 1
fi
# Show progress less frequently to reduce noise
if [ $((RETRY_COUNT % 5)) -eq 0 ]; then
echo " Waiting for bridge... ($RETRY_COUNT/$MAX_RETRIES)"
fi
sleep 1
done
rm -f "$FREECAD_LOG"
FREECAD_LOG="" # Clear so cleanup doesn't try to delete again
echo "✓ FreeCAD Robust MCP Bridge started (took ${RETRY_COUNT}s)"
STARTED_FREECAD=true
fi
echo ""
# Run the container and test communication
echo "Step 3: Running container and testing MCP communication..."
echo " Running Robust MCP Server in container..."
# Send MCP initialize request via JSON-RPC over stdio
# Using temp file for input so stdin closes after message
# Note: Container may exit non-zero when stdin closes (ClosedResourceError), which is expected
MCP_INPUT=$(mktemp)
echo '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' > "$MCP_INPUT"
# Capture output; ignore exit code since stdin close causes expected error
# Use timeout command if available for better reliability
if [ -n "$TIMEOUT_CMD" ]; then
CONTAINER_OUTPUT=$($TIMEOUT_CMD 30 docker run --rm -i \
--add-host=host.docker.internal:host-gateway \
-e FREECAD_MODE=xmlrpc \
-e FREECAD_SOCKET_HOST=host.docker.internal \
{{image_name}}:test 2>&1 < "$MCP_INPUT" || true)
else
CONTAINER_OUTPUT=$(docker run --rm -i \
--add-host=host.docker.internal:host-gateway \
-e FREECAD_MODE=xmlrpc \
-e FREECAD_SOCKET_HOST=host.docker.internal \
{{image_name}}:test 2>&1 < "$MCP_INPUT" || true)
fi
rm -f "$MCP_INPUT"
MCP_INPUT="" # Clear so cleanup doesn't try to delete again
echo ""
echo "Step 4: Verifying response..."
# Track test results
TEST_PASSED=true
# Check if we got a valid MCP initialize response
if echo "$CONTAINER_OUTPUT" | grep -q '"result".*"protocolVersion"'; then
echo "✓ Container responded with valid MCP initialize response"
else
echo "✗ Container failed to respond with valid MCP response"
TEST_PASSED=false
fi
# Check for server info in response
if echo "$CONTAINER_OUTPUT" | grep -q '"serverInfo".*"freecad-mcp"'; then
echo "✓ Server identified as freecad-mcp"
else
echo "⚠ Warning: Could not confirm server identity"
fi
# Check for FreeCAD bridge connection in logs (use grep -E for clearer alternation)
if echo "$CONTAINER_OUTPUT" | grep -Eq 'FreeCAD bridge connected|FreeCAD.*GUI'; then
echo "✓ FreeCAD bridge connection logged"
else
echo "⚠ Warning: No FreeCAD bridge connection in logs"
fi
echo ""
echo "=========================================="
echo "Raw container output (last 20 lines):"
echo "=========================================="
echo "$CONTAINER_OUTPUT" | tail -20
echo ""
# Note: FreeCAD cleanup is handled by the EXIT trap
echo ""
echo "=========================================="
if [ "$TEST_PASSED" = true ]; then
echo "✓ PASSED: Docker integration test succeeded!"
echo " - Container built and ran successfully"
echo " - Robust MCP Server responded to initialize request"
else
echo "✗ FAILED: Docker integration test had errors"
echo " - Review the output above for details"
fi
echo "=========================================="
# Exit with appropriate code
if [ "$TEST_PASSED" = false ]; then
exit 1
fi
# ============================================================================
# GUI Test Container (for CI debugging)
# ============================================================================
# Build the GUI test container (replicates GitHub Actions CI environment)
# Supports both x86_64 and aarch64 architectures (downloads correct AppImage)
build-gui-test:
docker build -f {{project_root}}/tests/ci-test/Dockerfile.gui-test \
-t {{gui_test_image}} \
{{project_root}}
# Run GUI test container interactively for debugging
gui-test-shell:
docker run --rm -it \
-v {{project_root}}:/workspace \
{{gui_test_image}} \
/bin/bash
# Run the automated GUI tests in the container
gui-test-run:
docker run --rm -i \
-v {{project_root}}:/workspace \
{{gui_test_image}} \
/usr/local/bin/run-gui-test.sh
# Run GUI test with custom command
gui-test-cmd *args:
docker run --rm -it \
-v {{project_root}}:/workspace \
{{gui_test_image}} \
{{args}}
# Quick rebuild and test cycle for GUI debugging
gui-test: build-gui-test gui-test-run
-75
View File
@@ -1,75 +0,0 @@
# Documentation commands
# Usage: just documentation::build, just documentation::serve, etc.
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# Build documentation
build:
cd {{project_root}} && uv run mkdocs build
# Build documentation with strict mode (fails on warnings, for CI)
build-strict:
cd {{project_root}} && uv run mkdocs build --strict
# Serve documentation locally
# Note: The leading `-` suppresses the error when the user interrupts with Ctrl+C
serve:
-cd {{project_root}} && uv run mkdocs serve
# Build and open documentation in browser
open:
#!/usr/bin/env bash
cd "{{project_root}}"
uv run mkdocs build
if [[ "$OSTYPE" == "darwin"* ]]; then
open "{{project_root}}/site/index.html"
elif command -v xdg-open &> /dev/null; then
xdg-open "{{project_root}}/site/index.html"
else
echo "Documentation built at: {{project_root}}/site/index.html"
fi
# --- Versioned Documentation (mike) ---
# List all deployed documentation versions
list-versions:
cd {{project_root}} && uv run mike list
# Serve versioned documentation locally (from gh-pages branch)
serve-versioned:
-cd {{project_root}} && uv run mike serve
# Deploy documentation as "dev" version (for local testing)
# Note: This modifies the gh-pages branch locally
deploy-dev:
#!/usr/bin/env bash
cd "{{project_root}}"
git config user.name "local-dev"
git config user.email "local@dev"
uv run mike deploy dev
# Deploy a specific version (for local testing)
# Usage: just documentation::deploy-version 1.0.0
# Note: This modifies the gh-pages branch locally
deploy-version VERSION:
#!/usr/bin/env bash
cd "{{project_root}}"
git config user.name "local-dev"
git config user.email "local@dev"
uv run mike deploy "{{VERSION}}"
# Deploy a version and set it as latest (for local testing)
# Usage: just documentation::deploy-latest 1.0.0
deploy-latest VERSION:
#!/usr/bin/env bash
cd "{{project_root}}"
git config user.name "local-dev"
git config user.email "local@dev"
uv run mike deploy --update-aliases "{{VERSION}}" latest
uv run mike set-default latest
# Delete a deployed version (for local cleanup)
# Usage: just documentation::delete-version 1.0.0
delete-version VERSION:
cd {{project_root}} && uv run mike delete "{{VERSION}}"
-200
View File
@@ -1,200 +0,0 @@
# FreeCAD workbench and macro commands
# Usage: just freecad::run-gui, just freecad::install-workbench, etc.
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# =============================================================================
# Running FreeCAD with MCP Bridge
# =============================================================================
# Run MCP bridge server in FreeCAD headless mode (blocking)
run-headless:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
# Use the blocking bridge script from the addon directory (source of truth)
SCRIPT_PATH="${PROJECT_DIR}/addon/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py"
# Find FreeCADCmd executable based on OS
FREECAD_CMD=""
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS: Check common locations
if [[ -x "/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd" ]]; then
FREECAD_CMD="/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd"
elif [[ -x "$HOME/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd" ]]; then
FREECAD_CMD="$HOME/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd"
fi
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux: Check common locations and PATH
if command -v freecadcmd &> /dev/null; then
FREECAD_CMD="freecadcmd"
elif command -v FreeCADCmd &> /dev/null; then
FREECAD_CMD="FreeCADCmd"
elif [[ -x "/usr/bin/freecadcmd" ]]; then
FREECAD_CMD="/usr/bin/freecadcmd"
elif [[ -x "/usr/local/bin/freecadcmd" ]]; then
FREECAD_CMD="/usr/local/bin/freecadcmd"
elif [[ -x "/opt/freecad/bin/FreeCADCmd" ]]; then
FREECAD_CMD="/opt/freecad/bin/FreeCADCmd"
fi
else
# Windows: Check common locations
if [[ -x "$PROGRAMFILES/FreeCAD 1.0/bin/FreeCADCmd.exe" ]]; then
FREECAD_CMD="$PROGRAMFILES/FreeCAD 1.0/bin/FreeCADCmd.exe"
elif [[ -x "$PROGRAMFILES/FreeCAD/bin/FreeCADCmd.exe" ]]; then
FREECAD_CMD="$PROGRAMFILES/FreeCAD/bin/FreeCADCmd.exe"
fi
fi
if [[ -z "$FREECAD_CMD" ]]; then
echo "ERROR: FreeCADCmd not found!"
echo ""
echo "Please install FreeCAD or set FREECAD_CMD environment variable:"
echo " export FREECAD_CMD=/path/to/FreeCADCmd"
echo " just freecad::run-headless"
echo ""
echo "Common locations:"
echo " macOS: /Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd"
echo " Linux: /usr/bin/freecadcmd or freecadcmd"
echo " Windows: C:\\Program Files\\FreeCAD 1.0\\bin\\FreeCADCmd.exe"
exit 1
fi
echo "Using FreeCAD: $FREECAD_CMD"
echo ""
# Run FreeCADCmd with the blocking bridge script
"$FREECAD_CMD" "$SCRIPT_PATH"
# Run MCP bridge with custom FreeCAD path (blocking)
run-headless-custom freecad_cmd:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
SCRIPT_PATH="${PROJECT_DIR}/addon/FreecadRobustMCPBridge/freecad_mcp_bridge/blocking_bridge.py"
if [[ ! -x "{{freecad_cmd}}" ]]; then
echo "ERROR: FreeCADCmd not found or not executable: {{freecad_cmd}}"
exit 1
fi
echo "Using FreeCAD: {{freecad_cmd}}"
echo ""
# Run FreeCADCmd with the blocking bridge script
"{{freecad_cmd}}" "$SCRIPT_PATH"
# Run FreeCAD GUI with MCP bridge (uses local source code for development)
# Note: Uses default ports (XML-RPC: 9875, Socket: 9876) regardless of workbench
# preferences. For custom ports, use the workbench GUI instead.
run-gui:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
# Use the shared startup script from the addon directory
STARTUP_SCRIPT="${PROJECT_DIR}/addon/FreecadRobustMCPBridge/freecad_mcp_bridge/startup_bridge.py"
if [[ ! -f "$STARTUP_SCRIPT" ]]; then
echo "ERROR: Startup script not found: $STARTUP_SCRIPT"
exit 1
fi
# Find FreeCAD GUI executable based on OS
FREECAD_GUI=""
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS: Use 'open' command for .app bundles
if [[ -d "/Applications/FreeCAD.app" ]]; then
FREECAD_GUI="/Applications/FreeCAD.app"
elif [[ -d "$HOME/Applications/FreeCAD.app" ]]; then
FREECAD_GUI="$HOME/Applications/FreeCAD.app"
fi
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux: Check common locations and PATH
if command -v freecad &> /dev/null; then
FREECAD_GUI="freecad"
elif command -v FreeCAD &> /dev/null; then
FREECAD_GUI="FreeCAD"
elif [[ -x "/usr/bin/freecad" ]]; then
FREECAD_GUI="/usr/bin/freecad"
elif [[ -x "/usr/local/bin/freecad" ]]; then
FREECAD_GUI="/usr/local/bin/freecad"
elif [[ -x "/opt/freecad/bin/FreeCAD" ]]; then
FREECAD_GUI="/opt/freecad/bin/FreeCAD"
fi
else
# Windows: Check common locations
if [[ -x "$PROGRAMFILES/FreeCAD 1.0/bin/FreeCAD.exe" ]]; then
FREECAD_GUI="$PROGRAMFILES/FreeCAD 1.0/bin/FreeCAD.exe"
elif [[ -x "$PROGRAMFILES/FreeCAD/bin/FreeCAD.exe" ]]; then
FREECAD_GUI="$PROGRAMFILES/FreeCAD/bin/FreeCAD.exe"
fi
fi
if [[ -z "$FREECAD_GUI" ]]; then
echo "ERROR: FreeCAD not found!"
echo ""
echo "Please install FreeCAD or use:"
echo " just freecad::run-gui-custom /path/to/FreeCAD"
echo ""
echo "Common locations:"
echo " macOS: /Applications/FreeCAD.app"
echo " Linux: /usr/bin/freecad or freecad"
echo " Windows: C:\\Program Files\\FreeCAD 1.0\\bin\\FreeCAD.exe"
exit 1
fi
echo "Starting FreeCAD with MCP bridge..."
echo "Using FreeCAD: $FREECAD_GUI"
echo ""
# Launch FreeCAD with the startup script
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS: Use 'open' with --args to pass the script
open -a "$FREECAD_GUI" --args "$STARTUP_SCRIPT"
else
# Linux/Windows: Run directly with script as argument
"$FREECAD_GUI" "$STARTUP_SCRIPT" &
fi
echo "FreeCAD is starting..."
echo "The MCP bridge will start automatically once FreeCAD initializes."
echo ""
echo "Note: You can now start/restart your MCP client (Claude Code, etc.) to connect."
# Run FreeCAD GUI with custom path
run-gui-custom freecad_path:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
# Use the shared startup script from the addon directory
STARTUP_SCRIPT="${PROJECT_DIR}/addon/FreecadRobustMCPBridge/freecad_mcp_bridge/startup_bridge.py"
if [[ ! -f "$STARTUP_SCRIPT" ]]; then
echo "ERROR: Startup script not found: $STARTUP_SCRIPT"
exit 1
fi
echo "Starting FreeCAD with MCP bridge..."
echo "Using FreeCAD: {{freecad_path}}"
echo ""
if [[ "$OSTYPE" == "darwin"* ]] && [[ "{{freecad_path}}" == *.app ]]; then
open -a "{{freecad_path}}" --args "$STARTUP_SCRIPT"
else
"{{freecad_path}}" "$STARTUP_SCRIPT" &
fi
echo "FreeCAD is starting with MCP bridge..."
# =============================================================================
# Deprecated Aliases (use install:: module instead)
# =============================================================================
# These are kept for backwards compatibility but will be removed in a future version.
# Use the new install:: module commands instead:
# just install::mcp-bridge-workbench (was: just freecad::install-workbench)
# just install::status (was: just freecad::mcp-status)
-482
View File
@@ -1,482 +0,0 @@
# Installation commands for users
# Usage: just install::mcp-server, just install::mcp-bridge-workbench, etc.
#
# This module installs components for end users:
# - Robust MCP Server (as a uv tool, available system-wide)
# - Robust MCP Bridge Workbench (FreeCAD addon)
#
# For developer setup (Python dependencies in virtualenv), use: just dev::install-deps
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# =============================================================================
# Helper: FreeCAD Directory Detection
# =============================================================================
# This function sets MOD_DIR and MACRO_DIR based on the current OS.
# Source it at the start of any recipe that needs FreeCAD paths.
#
# Usage in recipes:
# eval "$(just install::_freecad-dirs)"
# echo "Mod directory: $MOD_DIR"
# echo "Macro directory: $MACRO_DIR"
# Private recipe that outputs shell code to set FreeCAD directories
# FreeCAD 1.x uses versioned directories (v1-1, v1-2, etc.) for user data.
# This helper detects the latest versioned directory if present.
[private]
_freecad-dirs:
#!/usr/bin/env bash
cat << 'DIRS_EOF'
# Determine base FreeCAD directory based on OS
if [[ "$OSTYPE" == "darwin"* ]]; then
FREECAD_BASE="$HOME/Library/Application Support/FreeCAD"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
FREECAD_BASE="$HOME/.local/share/FreeCAD"
else
# Windows: validate APPDATA or use fallback
if [[ -n "$APPDATA" ]]; then
FREECAD_BASE="$APPDATA/FreeCAD"
elif [[ -n "$HOME" ]]; then
FREECAD_BASE="$HOME/AppData/Roaming/FreeCAD"
echo "Warning: APPDATA not set, using fallback: $FREECAD_BASE" >&2
else
echo "Error: Neither APPDATA nor HOME is set. Cannot determine FreeCAD directory." >&2
exit 1
fi
fi
# FreeCAD 1.x+ uses versioned directories (v1-1, v1-2, v2-0, etc.)
# Find the latest versioned directory if present
VERSIONED_DIR=""
if [[ -d "$FREECAD_BASE" ]]; then
# Find directories matching v*-* pattern (supports v1-*, v2-*, etc.)
# Use sort -t- -k1.2 -k2 -n to sort by major then minor version
LATEST_VERSION=$(ls -d "$FREECAD_BASE"/v*-* 2>/dev/null | sort -t- -k1.2 -k2 -n | tail -n 1)
if [[ -n "$LATEST_VERSION" && -d "$LATEST_VERSION" ]]; then
VERSIONED_DIR="$LATEST_VERSION"
fi
fi
# Use versioned directory if found, otherwise use base directory
if [[ -n "$VERSIONED_DIR" ]]; then
MOD_DIR="$VERSIONED_DIR/Mod"
MACRO_DIR="$VERSIONED_DIR/Macro"
echo "Note: Using FreeCAD versioned directory: $VERSIONED_DIR" >&2
else
MOD_DIR="$FREECAD_BASE/Mod"
MACRO_DIR="$FREECAD_BASE/Macro"
fi
DIRS_EOF
# =============================================================================
# Robust MCP Server Installation
# =============================================================================
# Install the Robust MCP Server as a user tool (available system-wide via uv)
# Uses cached builds for faster installation. For development with uncommitted
# changes, use mcp-server-clean instead.
mcp-server:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
echo "Installing Robust MCP Server as a uv tool..."
echo ""
# Install from the local project directory
# --force handles reinstallation automatically, no need to uninstall first
uv tool install --force "$PROJECT_DIR"
echo ""
echo "=========================================="
echo "Robust MCP Server installed!"
echo "=========================================="
echo ""
echo "The 'freecad-mcp' command is now available system-wide."
echo ""
echo "To run:"
echo " freecad-mcp # stdio mode (for Claude Code)"
echo " freecad-mcp --help # show all options"
echo ""
echo "To configure Claude Code, add to your MCP settings:"
echo ' "freecad": {'
echo ' "command": "freecad-mcp"'
echo ' }'
echo ""
echo "Note: If you have uncommitted local changes, use 'just install::mcp-server-clean'"
echo ""
# Install with cache clearing (for development with uncommitted changes)
# Clears uv cache first to ensure the build picks up all local changes.
mcp-server-clean:
#!/usr/bin/env bash
set -euo pipefail
echo "Clearing uv cache for fresh build..."
uv cache clean --force 2>/dev/null || true
just install::mcp-server
# Uninstall the Robust MCP Server tool
uninstall-mcp-server:
#!/usr/bin/env bash
set -euo pipefail
echo "Uninstalling Robust MCP Server..."
uv tool uninstall freecad-robust-mcp || echo "Robust MCP Server was not installed as a uv tool"
# =============================================================================
# Robust MCP Bridge Workbench Installation
# =============================================================================
# Install the FreeCAD Robust MCP workbench addon to FreeCAD's Mod directory
mcp-bridge-workbench:
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="{{project_root}}"
ADDON_NAME="FreecadRobustMCPBridge"
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
ADDON_DEST="$MOD_DIR/$ADDON_NAME"
# Create Mod directory if it doesn't exist
mkdir -p "$MOD_DIR"
# Remove existing installation if present
if [[ -d "$ADDON_DEST" ]]; then
echo "Removing existing installation at: $ADDON_DEST"
rm -rf "$ADDON_DEST"
fi
# Verify source exists before copying
ADDON_SRC="${PROJECT_DIR}/addon/$ADDON_NAME"
if [[ ! -d "$ADDON_SRC" ]]; then
echo "Error: Addon source directory not found: $ADDON_SRC" >&2
exit 1
fi
# Copy the addon directory
cp -r "$ADDON_SRC" "$ADDON_DEST"
# Generate package.xml for the workbench from root package.xml
# FreeCAD requires package.xml in the addon directory for proper workbench detection
ROOT_PACKAGE_XML="${PROJECT_DIR}/package.xml"
if [[ -f "$ROOT_PACKAGE_XML" ]]; then
echo "Generating package.xml from root package.xml..."
export ROOT_PACKAGE_XML ADDON_DEST
python3 << 'PYEOF'
import xml.etree.ElementTree as ET
import sys
import os
try:
root_pkg = os.environ.get('ROOT_PACKAGE_XML', '')
addon_dest = os.environ.get('ADDON_DEST', '')
tree = ET.parse(root_pkg)
root = tree.getroot()
ns = {'pkg': 'https://wiki.freecad.org/Package_Metadata'}
# Find the workbench content element
workbench = root.find('.//pkg:content/pkg:workbench', ns)
if workbench is None:
print("Warning: No workbench found in root package.xml", file=sys.stderr)
sys.exit(0)
# Extract workbench metadata
wb_name = workbench.find('pkg:name', ns)
wb_version = workbench.find('pkg:version', ns)
wb_date = workbench.find('pkg:date', ns)
wb_description = workbench.find('pkg:description', ns)
wb_classname = workbench.find('pkg:classname', ns)
wb_icon = workbench.find('pkg:icon', ns)
wb_freecadmin = workbench.find('pkg:freecadmin', ns)
# Get maintainer and license from root
maintainer = root.find('pkg:maintainer', ns)
license_el = root.find('pkg:license', ns)
repo_url = root.find('pkg:url[@type="repository"]', ns)
readme_url = root.find('pkg:url[@type="readme"]', ns)
# Create standalone package.xml
standalone = ET.Element('package', {
'format': '1',
'xmlns': 'https://wiki.freecad.org/Package_Metadata'
})
# Add metadata
name_text = wb_name.text if wb_name is not None else 'Robust MCP Bridge'
ET.SubElement(standalone, 'name').text = name_text
desc_text = wb_description.text if wb_description is not None else 'MCP Bridge for FreeCAD'
ET.SubElement(standalone, 'description').text = desc_text
ver_text = wb_version.text if wb_version is not None else '0.0.0'
ET.SubElement(standalone, 'version').text = ver_text
# Fall back to today's date if not specified
from datetime import date
date_text = wb_date.text if wb_date is not None else date.today().isoformat()
ET.SubElement(standalone, 'date').text = date_text
if maintainer is not None:
m = ET.SubElement(standalone, 'maintainer')
m.text = maintainer.text
if maintainer.get('email'):
m.set('email', maintainer.get('email'))
if license_el is not None:
l = ET.SubElement(standalone, 'license')
l.text = license_el.text
if license_el.get('file'):
l.set('file', license_el.get('file'))
if repo_url is not None:
u = ET.SubElement(standalone, 'url', type='repository')
u.text = repo_url.text
if repo_url.get('branch'):
u.set('branch', repo_url.get('branch'))
if readme_url is not None:
u = ET.SubElement(standalone, 'url', type='readme')
u.text = readme_url.text
icon_text = wb_icon.text if wb_icon is not None else 'FreecadRobustMCPBridge.svg'
ET.SubElement(standalone, 'icon').text = icon_text
fcmin_text = wb_freecadmin.text if wb_freecadmin is not None else '0.21'
ET.SubElement(standalone, 'freecadmin').text = fcmin_text
# Add content/workbench section
content = ET.SubElement(standalone, 'content')
wb_el = ET.SubElement(content, 'workbench')
cls_text = wb_classname.text if wb_classname is not None else 'FreecadRobustMCPBridgeWorkbench'
ET.SubElement(wb_el, 'classname').text = cls_text
ET.SubElement(wb_el, 'subdirectory').text = './'
# Add tags
for tag in ['MCP', 'AI', 'automation', 'Claude', 'bridge', 'headless']:
ET.SubElement(wb_el, 'tag').text = tag
# Helper to indent XML for Python < 3.9 compatibility
def indent_xml(elem, level=0, space=' '):
"""Indent XML element tree (fallback for Python < 3.9)."""
indent_str = '\n' + level * space
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = indent_str + space
for child in elem:
indent_xml(child, level + 1, space)
if not child.tail or not child.tail.strip():
child.tail = indent_str
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = indent_str
# Write the standalone package.xml
# Use ET.indent if available (Python 3.9+), otherwise use fallback
if hasattr(ET, 'indent'):
ET.indent(standalone, space=' ')
else:
indent_xml(standalone)
tree = ET.ElementTree(standalone)
output_path = os.path.join(addon_dest, 'package.xml')
tree.write(output_path, encoding='UTF-8', xml_declaration=True)
print("Generated package.xml successfully")
except Exception as e:
print(f"Warning: Could not generate package.xml: {e}", file=sys.stderr)
# Don't fail the installation if package.xml generation fails
PYEOF
else
echo "Warning: Root package.xml not found, skipping package.xml generation"
fi
echo ""
echo "=========================================="
echo "FreeCAD Robust MCP Workbench installed!"
echo "=========================================="
echo ""
echo "Installation path: $ADDON_DEST"
echo ""
echo "To use:"
echo " 1. Start FreeCAD"
echo " 2. Select the 'Robust MCP Bridge' workbench from the workbench selector"
echo " 3. Click 'Start MCP Bridge' in the toolbar"
echo " 4. Connect your MCP client (Claude Code, etc.) to FreeCAD"
echo ""
# Uninstall the FreeCAD Robust MCP workbench addon
uninstall-mcp-bridge-workbench:
#!/usr/bin/env bash
set -euo pipefail
ADDON_NAME="FreecadRobustMCPBridge"
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
ADDON_DEST="$MOD_DIR/$ADDON_NAME"
if [[ -d "$ADDON_DEST" ]]; then
rm -rf "$ADDON_DEST"
echo "FreeCAD Robust MCP Workbench uninstalled from: $ADDON_DEST"
else
echo "Workbench not found at: $ADDON_DEST"
fi
# =============================================================================
# Status Check
# =============================================================================
# Check installation status of all components
status:
#!/usr/bin/env bash
set -euo pipefail
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
# Helper function to get file modification time (cross-platform)
get_mod_time() {
local file="$1"
if [[ "$OSTYPE" == "darwin"* ]]; then
stat -f "%Sm" -t "%Y-%m-%d %H:%M" "$file" 2>/dev/null || echo "unknown"
else
stat -c "%y" "$file" 2>/dev/null | cut -d'.' -f1 || echo "unknown"
fi
}
echo "=========================================="
echo "Installation Status"
echo "=========================================="
echo ""
# Check Robust MCP Server (installed as uv tool)
if command -v freecad-mcp &> /dev/null; then
MCP_VERSION=$(freecad-mcp --version 2>/dev/null || echo "unknown")
MCP_PATH=$(command -v freecad-mcp)
MCP_MOD_TIME=$(get_mod_time "$MCP_PATH")
echo "✓ Robust MCP Server: INSTALLED (as uv tool)"
echo " Version: $MCP_VERSION"
echo " Updated: $MCP_MOD_TIME"
echo " Run: freecad-mcp"
elif [[ -f "{{project_root}}/pyproject.toml" ]] && grep -q 'name = "freecad-robust-mcp"' "{{project_root}}/pyproject.toml" 2>/dev/null; then
# Dev environment exists - check if synced by looking for .venv
if [[ -d "{{project_root}}/.venv" ]]; then
DEV_VERSION=$(cd "{{project_root}}" && uv run python -c "from freecad_mcp import __version__; print(__version__)" 2>/dev/null || echo "unknown")
echo "✓ Robust MCP Server: AVAILABLE (via dev environment)"
echo " Version: $DEV_VERSION"
echo " Run: just mcp::run"
echo " For system-wide install: just install::mcp-server"
else
echo "○ Robust MCP Server: DEV SOURCE AVAILABLE (needs setup)"
echo " Setup: uv sync --all-extras"
echo " Then run: just mcp::run"
fi
else
echo "✗ Robust MCP Server: NOT INSTALLED"
echo " Install: just install::mcp-server"
fi
echo ""
# Helper function to extract version from package.xml files
# Uses environment variable to pass file path safely to Python (avoids shell interpolation)
extract_package_version() {
local package_file="$1"
PACKAGE_FILE="$package_file" python3 -c '
import os
import xml.etree.ElementTree as ET
try:
package_file = os.environ.get("PACKAGE_FILE", "")
tree = ET.parse(package_file)
root = tree.getroot()
ns = {"pkg": "https://wiki.freecad.org/Package_Metadata"}
# Try with namespace first, then without
ver = root.find("pkg:version", ns) or root.find("version")
print(ver.text if ver is not None else "unknown")
except Exception:
print("unknown")
' 2>/dev/null || echo "unknown"
}
# Check workbench
if [[ -d "$MOD_DIR/FreecadRobustMCPBridge" ]]; then
WB_VERSION="unknown"
if [[ -f "$MOD_DIR/FreecadRobustMCPBridge/package.xml" ]]; then
WB_VERSION=$(extract_package_version "$MOD_DIR/FreecadRobustMCPBridge/package.xml")
fi
WB_MOD_TIME=$(get_mod_time "$MOD_DIR/FreecadRobustMCPBridge/InitGui.py")
echo "✓ Robust MCP Bridge Workbench: INSTALLED"
echo " Version: $WB_VERSION"
echo " Updated: $WB_MOD_TIME"
echo " Path: $MOD_DIR/FreecadRobustMCPBridge"
else
echo "✗ Robust MCP Bridge Workbench: NOT INSTALLED"
echo " Install: just install::mcp-bridge-workbench"
fi
echo ""
# Check for legacy installations
LEGACY_COUNT=0
if [[ -d "$MOD_DIR/MCPBridge" ]]; then
echo "⚠ Legacy plugin found: $MOD_DIR/MCPBridge"
echo " Run: rm -rf \"$MOD_DIR/MCPBridge\""
((LEGACY_COUNT++)) || true
fi
if [[ -f "$MACRO_DIR/StartMCPBridge.FCMacro" ]]; then
echo "⚠ Legacy macro found: $MACRO_DIR/StartMCPBridge.FCMacro"
echo " Run: rm \"$MACRO_DIR/StartMCPBridge.FCMacro\""
((LEGACY_COUNT++)) || true
fi
if [[ $LEGACY_COUNT -gt 0 ]]; then
echo ""
echo "Note: Legacy installations can be removed. The workbench replaces them."
fi
echo "=========================================="
# =============================================================================
# Convenience Commands
# =============================================================================
# Uninstall all components (MCP server and workbench)
uninstall:
#!/usr/bin/env bash
set -euo pipefail
echo "Uninstalling all Robust MCP components..."
echo ""
just install::uninstall-mcp-server
echo ""
just install::uninstall-mcp-bridge-workbench
echo ""
echo "All components uninstalled."
# Clean up everything (uninstall all + remove legacy installations)
cleanup:
#!/usr/bin/env bash
set -euo pipefail
# Set FreeCAD directories
eval "$(just install::_freecad-dirs)"
echo "Cleaning up all Robust MCP installations..."
echo ""
# Uninstall current components
just install::uninstall
echo ""
echo "Removing legacy installations..."
# Remove legacy MCPBridge if present
if [[ -d "$MOD_DIR/MCPBridge" ]]; then
rm -rf "$MOD_DIR/MCPBridge"
echo " Removed: $MOD_DIR/MCPBridge"
fi
# Remove legacy macro if present
if [[ -f "$MACRO_DIR/StartMCPBridge.FCMacro" ]]; then
rm "$MACRO_DIR/StartMCPBridge.FCMacro"
echo " Removed: $MACRO_DIR/StartMCPBridge.FCMacro"
fi
echo ""
echo "Cleanup complete."
-58
View File
@@ -1,58 +0,0 @@
# Robust MCP Server commands
# Usage: just mcp::run, just mcp::run-debug, etc.
#
# These commands run the Robust MCP Server that connects to FreeCAD.
# Note: FreeCAD must be running with the MCP bridge for the server to connect.
# Start FreeCAD with: just freecad::run-gui or just freecad::run-headless
# Check if FreeCAD bridge is available (test connection without starting server)
check:
uv run freecad-mcp --check
# Run the Robust MCP Server (stdio mode - default for Claude Code integration)
run:
#!/usr/bin/env bash
set -euo pipefail
echo "Checking FreeCAD bridge connection..."
if uv run freecad-mcp --check; then
echo ""
echo "Starting MCP server..."
uv run freecad-mcp
else
echo ""
echo "Cannot start MCP server - FreeCAD bridge is not available."
echo "Start FreeCAD with: just freecad::run-gui"
exit 1
fi
# Run the Robust MCP Server with debug logging
run-debug:
#!/usr/bin/env bash
set -euo pipefail
echo "Checking FreeCAD bridge connection..."
if uv run freecad-mcp --check; then
echo ""
echo "Starting MCP server with debug logging..."
FREECAD_LOG_LEVEL=DEBUG uv run freecad-mcp
else
echo ""
echo "Cannot start MCP server - FreeCAD bridge is not available."
echo "Start FreeCAD with: just freecad::run-gui"
exit 1
fi
# Run in HTTP mode for remote access (useful for testing or remote clients)
run-http port="8000":
#!/usr/bin/env bash
set -euo pipefail
echo "Checking FreeCAD bridge connection..."
if uv run freecad-mcp --check; then
echo ""
echo "Starting MCP server in HTTP mode on port {{port}}..."
FREECAD_TRANSPORT=http FREECAD_HTTP_PORT={{port}} uv run freecad-mcp
else
echo ""
echo "Cannot start MCP server - FreeCAD bridge is not available."
echo "Start FreeCAD with: just freecad::run-gui"
exit 1
fi
-111
View File
@@ -1,111 +0,0 @@
# Code quality commands
# Usage: just quality::check, just quality::format, etc.
#
# Note: Tools installed via mise (gitleaks, markdownlint-cli2, etc.) use `mise exec`.
# Python tools use `uv run`. This ensures commands work even without mise shell activation.
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# Run all pre-commit checks
check: _check-safety-auth
uv run pre-commit run --all-files
# Verify Safety CLI authentication (skipped in CI where SAFETY_API_KEY is used)
_check-safety-auth:
#!/usr/bin/env bash
# Skip check if SAFETY_API_KEY is set (CI environment)
if [[ -n "${SAFETY_API_KEY:-}" ]]; then
exit 0
fi
# Check if authenticated locally
if ! uv run safety auth status >/dev/null 2>&1; then
echo "ERROR: Safety CLI not authenticated."
echo ""
echo "Safety CLI requires a free account for dependency vulnerability scanning."
echo "Run the following command to authenticate:"
echo ""
echo " uv run safety auth login"
echo ""
echo "This only needs to be done once per machine."
echo "See CLAUDE.md for more details."
exit 1
fi
# Format code with ruff
format:
uv run ruff format {{project_root}}/src {{project_root}}/tests
uv run ruff check --fix {{project_root}}/src {{project_root}}/tests
# Run linting
lint:
uv run ruff check {{project_root}}/src {{project_root}}/tests
# Run type checking
typecheck:
cd {{project_root}} && uv run mypy src
# Run security scanning (code vulnerabilities)
security:
uv run bandit -c {{project_root}}/pyproject.toml -r {{project_root}}/src
cd {{project_root}} && uv run safety scan --detailed-output
# Run spell checking
spellcheck:
uv run codespell --ignore-words {{project_root}}/.codespell-ignore-words.txt {{project_root}}/src {{project_root}}/tests {{project_root}}/docs
# =============================================================================
# Secrets Scanning (quality::scan-* commands)
# =============================================================================
# Run all secrets scanners
scan: scan-gitleaks scan-detect scan-trufflehog # pragma: allowlist secret
@echo "All secrets scans complete!"
# Run gitleaks secrets scanner (installed via mise)
scan-gitleaks:
mise exec -- gitleaks detect --source {{project_root}} --config {{project_root}}/.gitleaks.toml --verbose
# Run gitleaks on git history
scan-gitleaks-history:
mise exec -- gitleaks detect --source {{project_root}} --config {{project_root}}/.gitleaks.toml --verbose --log-opts="--all"
# Check for new secrets against baseline (does NOT modify baseline file)
# Uses pre-commit to run detect-secrets with proper file enumeration
# Use scan-baseline-update to actually update the baseline
scan-detect:
@echo "Checking for new secrets against baseline..."
@uv run pre-commit run detect-secrets --all-files && echo "✓ No new secrets detected"
# Audit detect-secrets baseline (interactive)
scan-audit:
uv run detect-secrets audit {{project_root}}/.secrets.baseline
# Update detect-secrets baseline with current scan (updates generated_at timestamp)
# Run this when you want to add new files or refresh the baseline
# This WILL update the timestamp - only run when you intend to commit changes
scan-baseline-update:
cd {{project_root}} && uv run detect-secrets scan \
--exclude-files '\.secrets\.baseline$$' \
--exclude-files '\.gitleaks\.toml$$' \
--exclude-files 'uv\.lock$$' \
--exclude-files 'poetry\.lock$$' \
--exclude-files 'package-lock\.json$$' \
--update .secrets.baseline
@echo "✓ Baseline updated (run 'just quality::scan-audit' to review any findings)"
# Run trufflehog for verified secrets (via pre-commit - not installed standalone)
scan-trufflehog:
uv run pre-commit run trufflehog --all-files
# =============================================================================
# Markdown Linting
# =============================================================================
# Lint all markdown files (markdownlint-cli2 installed via mise)
markdown-lint:
cd {{project_root}} && mise exec -- markdownlint-cli2 "**/*.md" "#.venv" "#.pytest_cache" "#node_modules" "#site" "#htmlcov"
# Lint and fix markdown files
markdown-fix:
cd {{project_root}} && mise exec -- markdownlint-cli2 --fix "**/*.md" "#.venv" "#.pytest_cache" "#node_modules" "#site" "#htmlcov"
-1086
View File
File diff suppressed because it is too large Load Diff
-565
View File
@@ -1,565 +0,0 @@
# Testing commands
# Usage: just testing::unit, just testing::integration, etc.
# Project root directory (justfile_directory() returns the main justfile's directory)
project_root := justfile_directory()
# Run unit tests only (excludes integration tests)
unit:
uv run pytest {{project_root}}/tests/unit
# Run tests with coverage (excludes integration tests)
# Note: Uses bash script to ensure .coverage file is created in project root
cov:
#!/usr/bin/env bash
set -euo pipefail
cd "{{project_root}}"
uv run pytest tests/unit --cov=freecad_mcp --cov-report=term-missing --cov-report=html:htmlcov
# Run tests without slow markers (excludes integration tests)
quick:
uv run pytest {{project_root}}/tests/unit -m "not slow"
# Run only integration tests (requires running FreeCAD Robust MCP Bridge)
integration:
uv run pytest {{project_root}}/tests/integration -v
# Run tests with verbose output (excludes integration tests)
verbose:
uv run pytest {{project_root}}/tests/unit -v --tb=long
# Run all tests including integration (auto-starts FreeCAD headless)
# Runs unit tests first (no FreeCAD needed), then delegates to integration-freecad-auto
all:
#!/usr/bin/env bash
set -euo pipefail
echo "Running unit tests..."
echo ""
uv run pytest "{{project_root}}/tests/unit" -v
echo ""
echo "Unit tests passed! Now running integration tests..."
echo ""
# Delegate to integration-freecad-auto for FreeCAD lifecycle management
just testing::integration-freecad-auto
# Run tests in watch mode (re-runs on file changes)
# Note: --config specifies .pytest-watch.cfg to avoid pytest-watch parsing
# pyproject.toml as INI (it fails on valid TOML [[array.tables]] syntax)
watch:
#!/usr/bin/env bash
set -euo pipefail
echo ""
echo "========================================"
echo " WATCH MODE - Running initial tests..."
echo "========================================"
echo ""
uv run pytest-watch \
--config "{{project_root}}/.pytest-watch.cfg" \
--afterrun "echo '' && echo '========================================' && echo ' WATCHING for file changes...' && echo ' Press Ctrl+C to exit watch mode' && echo '========================================' && echo ''" \
"{{project_root}}/tests/unit"
# Run integration tests with automatic FreeCAD headless startup
integration-freecad-auto:
#!/usr/bin/env bash
set -euo pipefail
# Source shared bridge helper functions
. "{{project_root}}/scripts/bridge-helpers.sh"
# Track whether we started FreeCAD (so cleanup knows to stop it)
STARTED_FREECAD=false
# Cleanup function to ensure FreeCAD is stopped
# We kill by port since the subshell approach makes PID tracking unreliable
cleanup() {
if [ "$STARTED_FREECAD" = true ]; then
echo ""
echo "Stopping FreeCAD..."
graceful_kill_bridge_ports
fi
}
# Set trap: EXIT runs cleanup on normal exit, INT/TERM run cleanup then exit
trap cleanup EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM
echo "Starting FreeCAD headless server for integration tests..."
echo ""
# Check if a bridge is already running and responsive
if curl -s --connect-timeout 1 --max-time 1 http://localhost:9875 > /dev/null 2>&1; then
# Try to ping - if it responds, there's a healthy bridge already running
if uv run python -c "import socket; socket.setdefaulttimeout(2); import xmlrpc.client; print(xmlrpc.client.ServerProxy('http://localhost:9875').ping())" 2>/dev/null | grep -q "pong"; then
echo "ERROR: A FreeCAD Robust MCP Bridge is already running on port 9875."
echo ""
echo "Options:"
echo " 1. Use 'just testing::integration' to run tests against the existing bridge"
echo " 2. Stop the existing FreeCAD instance and try again"
echo " 3. If this is a zombie process, run: just testing::kill-bridge"
exit 1
else
# Port is bound but not responding to ping - likely a zombie
echo "WARNING: Port 9875 is bound but not responding (zombie process?)"
echo "Attempting to kill zombie process..."
kill_port 9875 -9
kill_port 9876 -9
sleep 2
fi
fi
# Mark that we're starting FreeCAD (for cleanup)
STARTED_FREECAD=true
# Start FreeCAD headless in background
# Redirect stderr to log file (not /dev/null) so startup failures are visible
# Background process won't fail the script when killed by cleanup trap
FREECAD_LOG="{{project_root}}/freecad-headless.log"
just freecad::run-headless 2>"$FREECAD_LOG" &
# Give FreeCAD time to start the XML-RPC server
echo "Waiting for FreeCAD Robust MCP Bridge to start..."
sleep 5
# Check if the bridge is ready (verify XML-RPC ping, not just port open)
MAX_RETRIES=30
RETRY_COUNT=0
while ! uv run python -c "import socket; socket.setdefaulttimeout(2); import xmlrpc.client; print(xmlrpc.client.ServerProxy('http://localhost:9875').ping())" 2>/dev/null | grep -q "pong"; do
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
echo "ERROR: FreeCAD Robust MCP Bridge did not start within timeout"
echo "Check log file for details: $FREECAD_LOG"
if [ -f "$FREECAD_LOG" ]; then
echo "--- Last 20 lines of log ---"
tail -20 "$FREECAD_LOG"
fi
exit 1
fi
echo " Waiting... ($RETRY_COUNT/$MAX_RETRIES)"
sleep 1
done
echo "FreeCAD Robust MCP Bridge is ready!"
echo ""
# Run integration tests
TEST_EXIT_CODE=0
uv run pytest "{{project_root}}/tests/integration" -v || TEST_EXIT_CODE=$?
# Cleanup is handled by trap
exit $TEST_EXIT_CODE
# =============================================================================
# Test Setup
# =============================================================================
# Check if test dependencies are installed (fails if any are missing)
check-deps:
@uv run python -c "import pytest" 2>/dev/null && echo "✓ pytest installed" || { echo "✗ pytest not installed"; exit 1; }
@uv run python -c "import pytest_cov" 2>/dev/null && echo "✓ pytest-cov installed" || { echo "✗ pytest-cov not installed"; exit 1; }
@uv run python -c "import pytest_asyncio" 2>/dev/null && echo "✓ pytest-asyncio installed" || { echo "✗ pytest-asyncio not installed"; exit 1; }
# =============================================================================
# Just Command Tests
# =============================================================================
# Run just command syntax tests (fast, validates all commands parse correctly)
just-syntax:
uv run pytest {{project_root}}/tests/just_commands -m "just_syntax" -v
# Run just command runtime tests (slower, actually executes commands)
just-runtime:
uv run pytest {{project_root}}/tests/just_commands -m "just_runtime and not slow" -v
# Run all just command tests
just-all:
uv run pytest {{project_root}}/tests/just_commands -v
# Run just command release tests (tests release commands with cleanup)
just-release:
uv run pytest {{project_root}}/tests/just_commands -m "just_release" -v
# =============================================================================
# Release Testing (Comprehensive Pre-Release Validation)
# =============================================================================
# Run all tests required before a release can be created
# This includes: unit tests, headless integration, GUI integration, Docker, and just commands
# All tests must pass for a release to proceed.
release-test:
#!/usr/bin/env bash
set -euo pipefail
echo "============================================================"
echo " RELEASE TEST SUITE"
echo " All tests must pass before creating a release"
echo "============================================================"
echo ""
# Track overall test results
TESTS_PASSED=true
# Arrays to track failures (name and command)
declare -a FAILED_NAMES=()
declare -a FAILED_COMMANDS=()
# Helper function to record test failure
record_failure() {
TESTS_PASSED=false
FAILED_NAMES+=("$1")
FAILED_COMMANDS+=("$2")
}
# -------------------------------------------------------------------------
# Step 1: Unit Tests with Coverage
# -------------------------------------------------------------------------
echo "============================================================"
echo " Step 1/5: Unit Tests with Coverage"
echo "============================================================"
echo ""
if just testing::cov; then
echo ""
echo "✓ Unit tests passed"
else
echo ""
echo "✗ Unit tests FAILED"
record_failure "Unit tests with coverage" "just testing::cov"
fi
echo ""
# -------------------------------------------------------------------------
# Step 2: Headless Integration Tests
# -------------------------------------------------------------------------
echo "============================================================"
echo " Step 2/5: Headless Integration Tests"
echo "============================================================"
echo ""
if just testing::integration-headless-release; then
echo ""
echo "✓ Headless integration tests passed"
else
echo ""
echo "✗ Headless integration tests FAILED"
record_failure "Headless integration tests" "just testing::integration-headless-release"
fi
echo ""
# -------------------------------------------------------------------------
# Step 3: GUI Integration Tests
# -------------------------------------------------------------------------
echo "============================================================"
echo " Step 3/5: GUI Integration Tests"
echo "============================================================"
echo ""
if just testing::integration-gui-release; then
echo ""
echo "✓ GUI integration tests passed"
else
echo ""
echo "✗ GUI integration tests FAILED"
record_failure "GUI integration tests" "just testing::integration-gui-release"
fi
echo ""
# -------------------------------------------------------------------------
# Step 4: Docker Integration Test
# -------------------------------------------------------------------------
echo "============================================================"
echo " Step 4/5: Docker Integration Test"
echo "============================================================"
echo ""
if just docker::test; then
echo ""
echo "✓ Docker integration test passed"
else
echo ""
echo "✗ Docker integration test FAILED"
record_failure "Docker integration test" "just docker::test"
fi
echo ""
# -------------------------------------------------------------------------
# Step 5: Just Command Tests
# -------------------------------------------------------------------------
echo "============================================================"
echo " Step 5/5: Just Command Tests"
echo "============================================================"
echo ""
if just testing::just-all; then
echo ""
echo "✓ Just command tests passed"
else
echo ""
echo "✗ Just command tests FAILED"
record_failure "Just command tests" "just testing::just-all"
fi
echo ""
# -------------------------------------------------------------------------
# Summary
# -------------------------------------------------------------------------
echo "============================================================"
echo " RELEASE TEST SUMMARY"
echo "============================================================"
echo ""
if [ "$TESTS_PASSED" = true ]; then
echo "✓ ALL TESTS PASSED - Ready for release!"
echo ""
echo "============================================================"
echo " NEXT STEPS FOR RELEASE"
echo "============================================================"
echo ""
echo "1. UPDATE RELEASE NOTES (for each component you're releasing):"
echo ""
echo " Edit the RELEASE_NOTES.md file for each component:"
echo " - MCP Server: src/freecad_mcp/RELEASE_NOTES.md"
echo " - Workbench: addon/FreecadRobustMCPBridge/RELEASE_NOTES.md"
echo ""
echo " Add a new version section at the top:"
echo " ## Version X.Y.Z (YYYY-MM-DD)"
echo " ### Added"
echo " - New feature description"
echo " ### Changed"
echo " - Change description"
echo " ### Fixed"
echo " - Bug fix description"
echo ""
echo " Tip: Use 'just release::draft-notes <component>' to generate draft notes"
echo " from git commits since the last release."
echo ""
echo "2. BUMP VERSIONS (for workbench only - MCP server auto-bumps from tag):"
echo ""
echo " just release::bump-workbench <version>"
echo ""
echo "3. COMMIT AND PUSH your RELEASE_NOTES.md and version bump changes"
echo ""
echo "4. CREATE RELEASE TAGS:"
echo ""
echo " just release::tag-mcp-server <version>"
echo " just release::tag-workbench <version>"
echo ""
echo "NOTE: The MCP server version is determined by the git tag, so no bump needed."
echo "NOTE: Release workflows extract notes from each component's RELEASE_NOTES.md."
else
echo "✗ SOME TESTS FAILED - Cannot proceed with release"
echo ""
echo "============================================================"
echo " FAILED TESTS"
echo "============================================================"
echo ""
for i in "${!FAILED_NAMES[@]}"; do
echo " ✗ ${FAILED_NAMES[$i]}"
done
echo ""
echo "============================================================"
echo " TO RECREATE FAILURES"
echo "============================================================"
echo ""
echo "Run these commands to reproduce the failing tests:"
echo ""
for i in "${!FAILED_COMMANDS[@]}"; do
echo " ${FAILED_COMMANDS[$i]}"
done
echo ""
echo "Please fix the failing tests before creating a release."
exit 1
fi
# Run headless integration tests for release (isolated, starts/stops FreeCAD)
integration-headless-release:
#!/usr/bin/env bash
set -euo pipefail
# Source shared bridge helper functions
. "{{project_root}}/scripts/bridge-helpers.sh"
# Track whether we started FreeCAD (so cleanup knows to stop it)
STARTED_FREECAD=false
# Cleanup function
cleanup() {
if [ "$STARTED_FREECAD" = true ]; then
echo ""
echo "Stopping FreeCAD headless..."
graceful_kill_bridge_ports
fi
}
trap cleanup EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM
echo "Headless Integration Test (Release Mode)"
echo "----------------------------------------"
echo ""
# Check for existing FreeCAD
if is_bridge_running; then
echo "ERROR: A FreeCAD Robust MCP Bridge is already running on port 9875."
echo ""
echo "For release testing, we need to start FreeCAD fresh."
echo "Please stop the existing FreeCAD instance and try again."
echo ""
echo "You can run: just testing::kill-bridge"
exit 1
fi
# Install latest workbench code to FreeCAD's Mod directory
# This ensures tests run against the current source code, not stale installed code
echo "Installing latest workbench code..."
just install::mcp-bridge-workbench
echo ""
# Start FreeCAD headless
STARTED_FREECAD=true
echo "Starting FreeCAD headless..."
FREECAD_LOG="{{project_root}}/freecad-headless-release.log"
just freecad::run-headless 2>"$FREECAD_LOG" &
# Wait for bridge to be ready
echo "Waiting for FreeCAD Robust MCP Bridge to start..."
MAX_RETRIES=60
RETRY_COUNT=0
while ! is_bridge_running; do
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
echo "ERROR: FreeCAD Robust MCP Bridge did not start within timeout"
if [ -f "$FREECAD_LOG" ]; then
echo "--- Last 20 lines of log ---"
tail -20 "$FREECAD_LOG"
fi
exit 1
fi
if [ $((RETRY_COUNT % 10)) -eq 0 ]; then
echo " Waiting... ($RETRY_COUNT/$MAX_RETRIES)"
fi
sleep 1
done
echo "FreeCAD Robust MCP Bridge is ready! (headless mode)"
echo ""
# Run integration tests
TEST_EXIT_CODE=0
uv run pytest "{{project_root}}/tests/integration" -v || TEST_EXIT_CODE=$?
# Cleanup handled by trap
rm -f "$FREECAD_LOG" 2>/dev/null || true
exit $TEST_EXIT_CODE
# Run GUI integration tests for release (isolated, starts/stops FreeCAD)
integration-gui-release:
#!/usr/bin/env bash
set -euo pipefail
# Source shared bridge helper functions
. "{{project_root}}/scripts/bridge-helpers.sh"
# Track whether we started FreeCAD (so cleanup knows to stop it)
STARTED_FREECAD=false
# Cleanup function
cleanup() {
if [ "$STARTED_FREECAD" = true ]; then
echo ""
echo "Stopping FreeCAD GUI..."
graceful_kill_bridge_ports
# On macOS, also try to quit FreeCAD gracefully
if [[ "$OSTYPE" == "darwin"* ]]; then
osascript -e 'tell application "FreeCAD" to quit' 2>/dev/null || true
fi
fi
}
trap cleanup EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM
echo "GUI Integration Test (Release Mode)"
echo "------------------------------------"
echo ""
# Check for existing FreeCAD
if is_bridge_running; then
echo "ERROR: A FreeCAD Robust MCP Bridge is already running on port 9875."
echo ""
echo "For release testing, we need to start FreeCAD fresh."
echo "Please stop the existing FreeCAD instance and try again."
echo ""
echo "You can run: just testing::kill-bridge"
exit 1
fi
# Install latest workbench code to FreeCAD's Mod directory
# This ensures tests run against the current source code, not stale installed code
echo "Installing latest workbench code..."
just install::mcp-bridge-workbench
echo ""
# Start FreeCAD GUI
STARTED_FREECAD=true
echo "Starting FreeCAD GUI..."
FREECAD_LOG="{{project_root}}/freecad-gui-release.log"
# Start FreeCAD GUI (this returns immediately on macOS)
just freecad::run-gui 2>"$FREECAD_LOG" || true
# Wait for bridge to be ready (GUI takes longer to start)
echo "Waiting for FreeCAD Robust MCP Bridge to start (GUI mode, may take 30-60s)..."
MAX_RETRIES=90
RETRY_COUNT=0
while ! is_bridge_running; do
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
echo "ERROR: FreeCAD Robust MCP Bridge did not start within timeout"
if [ -f "$FREECAD_LOG" ]; then
echo "--- Last 20 lines of log ---"
tail -20 "$FREECAD_LOG"
fi
exit 1
fi
if [ $((RETRY_COUNT % 10)) -eq 0 ]; then
echo " Waiting... ($RETRY_COUNT/$MAX_RETRIES)"
fi
sleep 1
done
echo "FreeCAD Robust MCP Bridge is ready! (GUI mode)"
echo ""
# Run integration tests
TEST_EXIT_CODE=0
uv run pytest "{{project_root}}/tests/integration" -v || TEST_EXIT_CODE=$?
# Cleanup handled by trap
rm -f "$FREECAD_LOG" 2>/dev/null || true
exit $TEST_EXIT_CODE
# =============================================================================
# Bridge Management
# =============================================================================
# Kill any zombie FreeCAD Robust MCP Bridge processes on the default ports
kill-bridge:
#!/usr/bin/env bash
set -euo pipefail
# Source shared bridge helper functions
. "{{project_root}}/scripts/bridge-helpers.sh"
echo "Killing any processes on MCP bridge ports (9875, 9876)..."
force_kill_bridge_ports
echo "Done."
-101
View File
@@ -1,101 +0,0 @@
# FreeCAD Robust MCP Suite - Development Workflow Commands
# https://just.systems/
#
# Commands are organized into modules. Run `just` to see top-level commands,
# or `just list-<module>` to see commands in a specific module:
# just list-coderabbit - AI code review commands
# just list-dev - Development utilities
# just list-docker - Docker build/run commands
# just list-documentation - Documentation commands
# just list-freecad - FreeCAD plugin/macro commands
# just list-install - Installation commands
# just list-mcp - MCP server commands
# just list-quality - Code quality commands
# just list-release - Release and tagging commands
# just list-testing - Test commands
#
# Or use `just list-all` to see all commands from all modules at once.
# Import modules
mod coderabbit 'just/coderabbit.just'
mod dev 'just/dev.just'
mod docker 'just/docker.just'
mod documentation 'just/documentation.just'
mod freecad 'just/freecad.just'
mod install 'just/install.just'
mod mcp 'just/mcp.just'
mod quality 'just/quality.just'
mod release 'just/release.just'
mod testing 'just/testing.just'
# Default recipe - show top-level commands and available modules
default:
@just --list --unsorted
# =============================================================================
# Setup & Installation
# =============================================================================
# Full development environment setup (installs deps + pre-commit hooks)
setup: (dev::install-deps) (dev::install-pre-commit)
@echo "Development environment ready!"
# =============================================================================
# Combined Workflows
# =============================================================================
# Run all quality checks and unit/coverage tests (use before committing)
all: (quality::check) (testing::cov)
@echo "All checks (minus integration) passed!"
# Run all quality checks and ALL tests including integration
all-with-integration: (quality::check) (testing::cov) (testing::integration-freecad-auto)
@echo "All checks and integration tests passed!"
# =============================================================================
# Module Listings (use these to explore available commands)
# =============================================================================
# List ALL commands from all modules
list-all:
@just --list --list-submodules
# List AI code review commands
list-coderabbit:
@just --list coderabbit
# List development utility commands
list-dev:
@just --list dev
# List Docker build/run commands
list-docker:
@just --list docker
# List documentation commands
list-documentation:
@just --list documentation
# List FreeCAD plugin and macro commands
list-freecad:
@just --list freecad
# List installation commands
list-install:
@just --list install
# List MCP server commands
list-mcp:
@just --list mcp
# List code quality commands
list-quality:
@just --list quality
# List release and tagging commands
list-release:
@just --list release
# List testing commands
list-testing:
@just --list testing
+1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+237
View File
@@ -0,0 +1,237 @@
/* Avoid breaking parameter names, etc. in table cells. */
.doc-contents td code {
word-break: normal !important;
}
/* No line break before first paragraph of descriptions. */
.doc-md-description,
.doc-md-description>p:first-child {
display: inline;
}
/* No text transformation from Material for MkDocs for H5 headings. */
.md-typeset h5 .doc-object-name {
text-transform: none;
}
/* Max width for docstring sections tables. */
.doc .md-typeset__table,
.doc .md-typeset__table table {
display: table !important;
width: 100%;
}
.doc .md-typeset__table tr {
display: table-row;
}
/* Defaults in Spacy table style. */
.doc-param-default,
.doc-type_param-default {
float: right;
}
/* Parameter headings must be inline, not blocks. */
.doc-heading-parameter,
.doc-heading-type_parameter {
display: inline;
}
/* Default font size for parameter headings. */
.md-typeset .doc-heading-parameter {
font-size: inherit;
}
/* Prefer space on the right, not the left of parameter permalinks. */
.doc-heading-parameter .headerlink,
.doc-heading-type_parameter .headerlink {
margin-left: 0 !important;
margin-right: 0.2rem;
}
/* Backward-compatibility: docstring section titles in bold. */
.doc-section-title {
font-weight: bold;
}
/* Backlinks crumb separator. */
.doc-backlink-crumb {
display: inline-flex;
gap: .2rem;
white-space: nowrap;
align-items: center;
vertical-align: middle;
}
.doc-backlink-crumb:not(:first-child)::before {
background-color: var(--md-default-fg-color--lighter);
content: "";
display: inline;
height: 1rem;
--md-path-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6z"/></svg>');
-webkit-mask-image: var(--md-path-icon);
mask-image: var(--md-path-icon);
width: 1rem;
}
.doc-backlink-crumb.last {
font-weight: bold;
}
/* Symbols in Navigation and ToC. */
:root, :host,
[data-md-color-scheme="default"] {
--doc-symbol-parameter-fg-color: #df50af;
--doc-symbol-type_parameter-fg-color: #df50af;
--doc-symbol-attribute-fg-color: #953800;
--doc-symbol-function-fg-color: #8250df;
--doc-symbol-method-fg-color: #8250df;
--doc-symbol-class-fg-color: #0550ae;
--doc-symbol-type_alias-fg-color: #0550ae;
--doc-symbol-module-fg-color: #5cad0f;
--doc-symbol-parameter-bg-color: #df50af1a;
--doc-symbol-type_parameter-bg-color: #df50af1a;
--doc-symbol-attribute-bg-color: #9538001a;
--doc-symbol-function-bg-color: #8250df1a;
--doc-symbol-method-bg-color: #8250df1a;
--doc-symbol-class-bg-color: #0550ae1a;
--doc-symbol-type_alias-bg-color: #0550ae1a;
--doc-symbol-module-bg-color: #5cad0f1a;
}
[data-md-color-scheme="slate"] {
--doc-symbol-parameter-fg-color: #ffa8cc;
--doc-symbol-type_parameter-fg-color: #ffa8cc;
--doc-symbol-attribute-fg-color: #ffa657;
--doc-symbol-function-fg-color: #d2a8ff;
--doc-symbol-method-fg-color: #d2a8ff;
--doc-symbol-class-fg-color: #79c0ff;
--doc-symbol-type_alias-fg-color: #79c0ff;
--doc-symbol-module-fg-color: #baff79;
--doc-symbol-parameter-bg-color: #ffa8cc1a;
--doc-symbol-type_parameter-bg-color: #ffa8cc1a;
--doc-symbol-attribute-bg-color: #ffa6571a;
--doc-symbol-function-bg-color: #d2a8ff1a;
--doc-symbol-method-bg-color: #d2a8ff1a;
--doc-symbol-class-bg-color: #79c0ff1a;
--doc-symbol-type_alias-bg-color: #79c0ff1a;
--doc-symbol-module-bg-color: #baff791a;
}
code.doc-symbol {
border-radius: .1rem;
font-size: .85em;
padding: 0 .3em;
font-weight: bold;
}
code.doc-symbol-parameter,
a code.doc-symbol-parameter {
color: var(--doc-symbol-parameter-fg-color);
background-color: var(--doc-symbol-parameter-bg-color);
}
code.doc-symbol-parameter::after {
content: "param";
}
code.doc-symbol-type_parameter,
a code.doc-symbol-type_parameter {
color: var(--doc-symbol-type_parameter-fg-color);
background-color: var(--doc-symbol-type_parameter-bg-color);
}
code.doc-symbol-type_parameter::after {
content: "type-param";
}
code.doc-symbol-attribute,
a code.doc-symbol-attribute {
color: var(--doc-symbol-attribute-fg-color);
background-color: var(--doc-symbol-attribute-bg-color);
}
code.doc-symbol-attribute::after {
content: "attr";
}
code.doc-symbol-function,
a code.doc-symbol-function {
color: var(--doc-symbol-function-fg-color);
background-color: var(--doc-symbol-function-bg-color);
}
code.doc-symbol-function::after {
content: "func";
}
code.doc-symbol-method,
a code.doc-symbol-method {
color: var(--doc-symbol-method-fg-color);
background-color: var(--doc-symbol-method-bg-color);
}
code.doc-symbol-method::after {
content: "meth";
}
code.doc-symbol-class,
a code.doc-symbol-class {
color: var(--doc-symbol-class-fg-color);
background-color: var(--doc-symbol-class-bg-color);
}
code.doc-symbol-class::after {
content: "class";
}
code.doc-symbol-type_alias,
a code.doc-symbol-type_alias {
color: var(--doc-symbol-type_alias-fg-color);
background-color: var(--doc-symbol-type_alias-bg-color);
}
code.doc-symbol-type_alias::after {
content: "type";
}
code.doc-symbol-module,
a code.doc-symbol-module {
color: var(--doc-symbol-module-fg-color);
background-color: var(--doc-symbol-module-bg-color);
}
code.doc-symbol-module::after {
content: "mod";
}
.doc-signature .autorefs {
color: inherit;
border-bottom: 1px dotted currentcolor;
}
/* Source code blocks (admonitions). */
:root {
--md-admonition-icon--mkdocstrings-source: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.22 4.97a.75.75 0 0 1 1.06 0l6.5 6.5a.75.75 0 0 1 0 1.06l-6.5 6.5a.749.749 0 0 1-1.275-.326.75.75 0 0 1 .215-.734L21.19 12l-5.97-5.97a.75.75 0 0 1 0-1.06m-6.44 0a.75.75 0 0 1 0 1.06L2.81 12l5.97 5.97a.749.749 0 0 1-.326 1.275.75.75 0 0 1-.734-.215l-6.5-6.5a.75.75 0 0 1 0-1.06l6.5-6.5a.75.75 0 0 1 1.06 0"/></svg>')
}
.md-typeset .admonition.mkdocstrings-source,
.md-typeset details.mkdocstrings-source {
border: none;
padding: 0;
}
.md-typeset .admonition.mkdocstrings-source:focus-within,
.md-typeset details.mkdocstrings-source:focus-within {
box-shadow: none;
}
.md-typeset .mkdocstrings-source > .admonition-title,
.md-typeset .mkdocstrings-source > summary {
background-color: inherit;
}
.md-typeset .mkdocstrings-source > .admonition-title::before,
.md-typeset .mkdocstrings-source > summary::before {
background-color: var(--md-default-fg-color);
-webkit-mask-image: var(--md-admonition-icon--mkdocstrings-source);
mask-image: var(--md-admonition-icon--mkdocstrings-source);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More