Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0426e6203f | ||
|
|
fb8c802f5d | ||
|
|
6c824eb3bd | ||
|
|
29f63d19c7 | ||
|
|
9db4138fb9 | ||
|
|
4615da7a70 | ||
|
|
eb2f6fba2a | ||
|
|
610654ae38 | ||
|
|
2bef958994 | ||
|
|
a797038f6f | ||
|
|
47e80f1bc7 | ||
|
|
62f6f4dbcf | ||
|
|
b896a69b84 | ||
|
|
3e07be281d | ||
|
|
4ab3f95bc9 | ||
|
|
86c883a688 | ||
|
|
13b0d96261 | ||
|
|
9dde1cef21 | ||
|
|
75726329c7 | ||
|
|
a24189b07c | ||
|
|
f14ab8177d | ||
|
|
8c338f6da7 | ||
|
|
bcc3048876 | ||
|
|
a775852ce9 | ||
|
|
d7d6c2c8e7 | ||
|
|
6a6cad9e65 | ||
|
|
4344ff7536 | ||
|
|
f58b32831c | ||
|
|
f3c92ec413 | ||
|
|
c271630de0 | ||
|
|
57c798b9b8 | ||
|
|
6ecc20bcf6 | ||
|
|
e11ba127cd | ||
|
|
1e7fa38cf4 | ||
|
|
f59035e2b9 | ||
|
|
fd60a3a3ba | ||
|
|
ee0be0a315 | ||
|
|
cd77560297 | ||
|
|
8d1271995e |
@@ -0,0 +1,199 @@
|
||||
# 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
|
||||
@@ -0,0 +1,64 @@
|
||||
# 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
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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
|
||||
@@ -0,0 +1,5 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: spkane
|
||||
buy_me_a_coffee: spkane
|
||||
thanks_dev: u/gh/spkane
|
||||
@@ -0,0 +1,128 @@
|
||||
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
|
||||
@@ -0,0 +1,5 @@
|
||||
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
|
||||
@@ -0,0 +1,104 @@
|
||||
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
|
||||
@@ -0,0 +1,215 @@
|
||||
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
|
||||
@@ -0,0 +1,60 @@
|
||||
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"
|
||||
@@ -0,0 +1,50 @@
|
||||
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 }}"
|
||||
@@ -0,0 +1,170 @@
|
||||
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
|
||||
@@ -0,0 +1,125 @@
|
||||
# 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
|
||||
@@ -0,0 +1,437 @@
|
||||
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"
|
||||
@@ -0,0 +1,246 @@
|
||||
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"
|
||||
@@ -0,0 +1,58 @@
|
||||
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
|
||||
@@ -0,0 +1,55 @@
|
||||
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/
|
||||
@@ -0,0 +1,124 @@
|
||||
# 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
|
||||
@@ -0,0 +1,125 @@
|
||||
# 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
|
||||
@@ -0,0 +1,222 @@
|
||||
# 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
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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
|
||||
@@ -0,0 +1,316 @@
|
||||
# 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
|
||||
@@ -0,0 +1,4 @@
|
||||
# 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]
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
id = freecad-addon-robust-mcp-server
|
||||
url = /codebases/freecad-addon-robust-mcp-server/findings
|
||||
name = freecad-addon-robust-mcp-server
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# 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
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
# 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"]
|
||||
@@ -0,0 +1,21 @@
|
||||
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.
|
||||
@@ -0,0 +1,693 @@
|
||||
# FreeCAD Robust MCP Server
|
||||
|
||||
[](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/test.yaml)
|
||||
[](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/docker.yaml)
|
||||
[](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/pre-commit.yaml)
|
||||
[](https://github.com/spkane/freecad-addon-robust-mcp-server/actions/workflows/codeql.yaml)
|
||||
[](https://pypi.org/project/freecad-robust-mcp/)
|
||||
[](https://pypi.org/project/freecad-robust-mcp/)
|
||||
|
||||
[](https://hub.docker.com/r/spkane/freecad-robust-mcp)
|
||||
[](https://spkane.github.io/freecad-addon-robust-mcp-server/)
|
||||
[](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.
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,53 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,231 @@
|
||||
"""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")
|
||||
@@ -0,0 +1,294 @@
|
||||
"""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"
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,470 @@
|
||||
"""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()
|
||||
@@ -0,0 +1,13 @@
|
||||
"""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__"]
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,272 @@
|
||||
"""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
|
||||
@@ -0,0 +1,931 @@
|
||||
"""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
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,57 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,46 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,84 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,119 @@
|
||||
"""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 ""
|
||||
@@ -0,0 +1,150 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,209 @@
|
||||
"""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"
|
||||
)
|
||||
@@ -0,0 +1,358 @@
|
||||
"""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
|
||||
@@ -0,0 +1,196 @@
|
||||
<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:}}]]
|
||||
@@ -0,0 +1,299 @@
|
||||
# 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)
|
||||
```
|
||||
@@ -0,0 +1,307 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,485 @@
|
||||
# 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
|
||||
@@ -0,0 +1,34 @@
|
||||
# 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
|
||||
@@ -0,0 +1,6 @@
|
||||
# Configuration API Reference
|
||||
|
||||
::: freecad_mcp.config
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_source: true
|
||||
@@ -0,0 +1,11 @@
|
||||
# Server API Reference
|
||||
|
||||
::: freecad_mcp.server
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_source: true
|
||||
members:
|
||||
\- mcp
|
||||
\- get_bridge
|
||||
\- startup
|
||||
\- shutdown
|
||||
@@ -0,0 +1,348 @@
|
||||
# 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
|
||||
@@ -0,0 +1,222 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,570 @@
|
||||
# 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
|
||||
{ .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
|
||||

|
||||
```
|
||||
|
||||
### Image with Caption
|
||||
|
||||
```markdown
|
||||
<figure markdown>
|
||||

|
||||
<figcaption>This is the caption</figcaption>
|
||||
</figure>
|
||||
```
|
||||
|
||||
### Image Alignment
|
||||
|
||||
```markdown
|
||||
{ align=left }
|
||||
{ align=right }
|
||||
```
|
||||
|
||||
### Image Size
|
||||
|
||||
```markdown
|
||||
{ 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/)
|
||||
@@ -0,0 +1,388 @@
|
||||
# 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>
|
||||
```
|
||||
@@ -0,0 +1,162 @@
|
||||
# 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
|
||||
@@ -0,0 +1,126 @@
|
||||
# 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
|
||||
@@ -0,0 +1,137 @@
|
||||
# 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
|
||||
@@ -0,0 +1,202 @@
|
||||
# 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
|
||||
@@ -0,0 +1,150 @@
|
||||
# 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
|
||||
@@ -0,0 +1,335 @@
|
||||
# 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
|
||||
@@ -0,0 +1,219 @@
|
||||
# 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
|
||||
@@ -0,0 +1,260 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,98 @@
|
||||
# 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/)**
|
||||
@@ -1,18 +0,0 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,210 @@
|
||||
# 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
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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."
|
||||
@@ -0,0 +1,374 @@
|
||||
# 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
|
||||
@@ -0,0 +1,75 @@
|
||||
# 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}}"
|
||||
@@ -0,0 +1,200 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,482 @@
|
||||
# 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."
|
||||
@@ -0,0 +1,58 @@
|
||||
# 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
|
||||
@@ -0,0 +1,111 @@
|
||||
# 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"
|
||||
@@ -0,0 +1,565 @@
|
||||
# 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."
|
||||
@@ -0,0 +1,101 @@
|
||||
# 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,237 +0,0 @@
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |