feat: MCP Bridge Workbench, just command cleanup, testing, etc. (#24)

* fix: lots of fixes and name refactoring

* feat: Add workbench preferences

* fix: MCP bridge status widget and just command fixes

* fix(tests): Use the correct mesa-glx package

* fix(ci): Add fontconfig to GUI test dependencies

FreeCAD GUI was failing to start with:
"Fontconfig error: Cannot load default config file: No such file"

Added fontconfig and fonts-dejavu-core packages to the GUI test job
dependencies to resolve the font configuration issue.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(addon): Extract path utilities into shared module

Create path_utils.py module that consolidates duplicated path-finding
logic from commands.py and InitGui.py:
- get_addon_path(): Find addon directory with caching and fallbacks
- get_icon_path(): Get full path to an icon file
- get_icons_dir(): Get path to icons directory
- get_workbench_icon(): Get path to workbench main icon

This removes ~100 lines of duplicated code while preserving the same
behavior including _addon_path_cache and all fallback methods.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(addon): Prevent stale plugin state on startup failure

The StartMCPBridgeCommand.Activated method could leave _mcp_plugin in
a partially initialized state if FreecadMCPPlugin.start() failed after
the plugin was instantiated.

Changes:
- Create plugin in a local variable first
- Only assign to _mcp_plugin after start() succeeds
- Explicitly clear _mcp_plugin and _running_config in exception
  handlers to ensure clean state for subsequent retry attempts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Lot of broad improvements

* fix(ci): Use blocking headless_server.py for GUI tests

The GUI test was using startup_bridge.py which is non-blocking
(designed for interactive use). For CI, even in GUI mode, we need
the blocking headless_server.py that calls run_forever() to keep
FreeCAD running. GUI features are still available since we use
the 'freecad' executable instead of 'freecadcmd'.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(addon): Rename headless_server.py to blocking_bridge.py

The old name was misleading because:
- It works with both GUI (freecad) and headless (freecadcmd) modes
- The key characteristic is that it BLOCKS with run_forever()

New naming convention clarifies the difference:
- blocking_bridge.py: Starts bridge and blocks (for CI, servers)
- startup_bridge.py: Starts bridge and returns (for interactive GUI)

Updated all references across:
- GitHub workflow (macro-test.yaml)
- Just commands (freecad.just)
- Unit tests (test_addon_structure.py)
- Documentation (5 files)
- CLAUDE.md

Also improved the script to detect GUI mode dynamically using
FreeCAD.GuiUp and display the appropriate status message.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(just): Remove erroneous rm of startup_bridge.py on error

The startup script is now a permanent source file in the repository,
not a generated temporary file. The rm -f would have deleted source
code if FreeCAD wasn't found.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: General improvements

* fix: Lots of general fixes and only stable to PyPi

* fix: small cleanup

* fix: Small fixes and hopefully fixes the GUI tests

* fix: Add proper library paths for FreeCAD GUI in CI

- Create wrapper scripts instead of symlinks for AppImage binaries
- Set LD_LIBRARY_PATH, QT_PLUGIN_PATH for GUI mode
- Add diagnostic output to identify startup failures

* fix: Use apprun for GUI tests in CI

* fix: Improving Xvfb tests

* fix: GUI tests worlk

* chore: remove invalid --no-splash comments

* fix: ARM64 architecture support and other fixes

* fix: cleanup

* test: just commands test suite

* test: improve just command tests

* fix: more general improvements

* fix: more cleanup

* fix: more updates

* fix: small tweaks

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sean P. Kane
2026-01-10 15:26:33 -08:00
committed by GitHub
co-authored by Claude Opus 4.5
parent bcc3048876
commit 8c338f6da7
110 changed files with 7821 additions and 1276 deletions
+164 -43
View File
@@ -39,7 +39,7 @@ FreeCAD's `FreeCAD.so` library links to `@rpath/libpython3.11.dylib` (FreeCAD's
1. Start FreeCAD and start the MCP bridge:
- Install the MCP Bridge workbench via Addon Manager, or
- Install the Robust MCP Bridge workbench via Addon Manager, or
- Use `just freecad::run-gui` from the source repository
1. The MCP server will then connect to FreeCAD over the network
@@ -197,7 +197,7 @@ just testing::unit # Run unit tests
just testing::cov # Run tests with coverage
just testing::fast # Run tests without slow markers
just testing::integration # Run integration tests
just testing::integration-freecad # Integration tests with auto FreeCAD startup
just testing::integration-freecad-auto # Integration tests with auto FreeCAD startup
just testing::watch # Run tests in watch mode
just testing::all # Run all tests including integration
@@ -228,7 +228,7 @@ just coderabbit::review-fix # Review with auto-fix suggestions
# Release commands (component-specific tagging)
just release::status # Show unreleased changes across all components
just release::tag-mcp-server 1.0.0 # Release MCP server (PyPI + Docker)
just release::tag-workbench 1.0.0 # Release MCP Bridge workbench
just release::tag-workbench 1.0.0 # Release Robust MCP Bridge workbench
just release::tag-macro-magnets 1.0.0 # Release Cut Object for Magnets macro
just release::tag-macro-export 1.0.0 # Release Multi Export macro
just release::list-tags # List all release tags
@@ -237,9 +237,8 @@ just release::delete-tag <tag> # Delete a release tag (local and remote)
# Combined workflows
just setup # Full dev setup (install deps + hooks)
just all # Run all quality checks and unit tests
just all-with-integration # Run all checks + integration tests
just ci # Full CI pipeline (checks + coverage)
just all # Run all quality checks and unit/coverage tests
just all-with-integration # Run all checks and integration tests
```
#### Just Module Structure
@@ -250,7 +249,7 @@ just ci # Full CI pipeline (checks + coverage)
| `freecad` | FreeCAD running commands | `run-gui`, `run-headless`, `run-gui-custom` |
| `install` | User installation commands | `mcp-server`, `mcp-bridge-workbench`, `macro-all` |
| `quality` | Code quality and linting | `check`, `lint`, `format`, `scan` |
| `testing` | Test execution | `unit`, `cov`, `integration-freecad`, `watch` |
| `testing` | Test execution | `unit`, `cov`, `integration-freecad-auto`, `watch` |
| `docker` | Docker build and run commands | `build`, `build-multi`, `run`, `clean-all` |
| `documentation` | Documentation building | `build`, `serve`, `open` |
| `dev` | Development utilities | `install-deps`, `update-deps`, `clean` |
@@ -320,6 +319,32 @@ Pre-commit runs these checks:
- Maximum line length: 88 characters (ruff/black default)
- Use modern Python syntax (3.10+ features encouraged)
### Accessible Language
> This isn't about politics—it's about clarity. Literal terms translate better, search better, and are understood by more people regardless of cultural background. Good communication is good engineering.
Use clear, literal language in code, comments, documentation, and commit messages. Avoid idioms, metaphors, and jargon that may be unclear, exclusionary, or carry unintended connotations:
| Avoid | Prefer |
| ------------------------------------------ | ------------------------------------------------ |
| sanity check, sanity test | validation, verification, smoke test, quick test |
| sane defaults, insane behavior | sensible defaults, unexpected behavior |
| whitelist, blacklist | allowlist, blocklist |
| master, slave | main, primary, replica, secondary |
| kill, abort, nuke (as metaphors) | stop, terminate, cancel, remove |
| war room, battle-tested | operations center, production-tested |
| cripple, blind to | disable, unaware of, ignore |
| dummy, handicapped | placeholder, stub, limited |
**Note:** Actual command names (e.g., `kill -9`, `kill_port()`, `git rebase --abort`) are fine when discussing or documenting those specific commands. The guidance above applies to metaphorical usage in prose, comments, and naming.
**Why this matters:**
- Literal terms are clearer to non-native speakers and those unfamiliar with idioms
- Avoids unintentionally alienating contributors
- Makes code more accessible and professional
- Many organizations and open-source projects have adopted similar guidelines
### Security Scanning
- Bandit scans for common security issues
@@ -491,6 +516,10 @@ tests/
├── integration/ # Integration tests
│ ├── __init__.py
│ └── test_*.py
├── just_commands/ # Just command tests
│ ├── __init__.py
│ ├── conftest.py # Just test fixtures
│ └── test_*.py # Tests for each just module
└── fixtures/ # Test data files
```
@@ -534,6 +563,69 @@ uv run pytest tests/unit/ # Run specific test directory
uv run pytest -k "test_name" # Run specific test by name
```
### Just Command Testing
The project includes a comprehensive test suite for all `just` commands in `tests/just_commands/`. This ensures that justfile syntax errors, missing dependencies, and runtime failures are caught early.
**Test Categories:**
| Marker | Description | Command |
| ---------------- | ---------------------------------------------------- | --------------------------------- |
| `just_syntax` | Validates just can parse commands (--dry-run) | `just testing::just-syntax` |
| `just_runtime` | Actually executes commands and verifies behavior | `just testing::just-runtime` |
| `just_release` | Release command tests with cleanup | `just testing::just-release` |
| (all) | Run all just command tests | `just testing::just-all` |
**Running Just Command Tests:**
```bash
just testing::just-syntax # Fast syntax validation (recommended before commits)
just testing::just-runtime # Runtime tests (slower, actually runs commands)
just testing::just-release # Release command tests with cleanup
just testing::just-all # All just command tests
```
#### Updating Tests When Changing Just Commands
**MANDATORY**: When you add, modify, or remove a just command, you MUST update the corresponding test file:
| Module | Test File |
| -------------- | ------------------------------------------- |
| Main justfile | `tests/just_commands/test_main.py` |
| coderabbit | `tests/just_commands/test_coderabbit.py` |
| dev | `tests/just_commands/test_dev.py` |
| docker | `tests/just_commands/test_docker.py` |
| documentation | `tests/just_commands/test_documentation.py` |
| freecad | `tests/just_commands/test_freecad.py` |
| install | `tests/just_commands/test_install.py` |
| mcp | `tests/just_commands/test_mcp.py` |
| quality | `tests/just_commands/test_quality.py` |
| release | `tests/just_commands/test_release.py` |
| testing | `tests/just_commands/test_testing.py` |
**What to Update:**
1. **New command**: Add to `COMMANDS` list in syntax tests, add runtime test if applicable
2. **Modified command**: Update any tests that depend on command behavior/output
3. **Removed command**: Remove from `COMMANDS` list and delete related tests
4. **Changed arguments**: Update parametrized tests with correct arguments
**Release Command Testing Strategy:**
Release commands are tested carefully to avoid accidental releases:
- **Syntax tests**: Use `--dry-run` for all commands
- **Read-only tests**: Safe commands like `status`, `list-tags`, `latest-versions`
- **Version bump tests**: Test bump commands (modify local files, restored after test)
- **Tag validation**: Test version format validation, dirty tree detection
- **Skip push tests**: Commands that push to remote are only syntax-tested
For actual release testing with cleanup, tests use:
- Test versions like `99.99.99-test` that are clearly non-production
- Backup and restore of modified files
- Tag prefixes like `test-release-XXXXXX` with random suffixes
---
## Workflow for Code Changes
@@ -625,7 +717,7 @@ project-root/
│ │ └── test.yaml # Unit/integration tests
│ └── dependabot.yaml # Dependency updates
├── addon/ # FreeCAD addon (workbench)
│ └── FreecadRobustMCP/ # MCP Bridge workbench
│ └── FreecadRobustMCP/ # Robust MCP Bridge workbench
│ ├── freecad_mcp_bridge/ # Bridge Python package
│ ├── Init.py # FreeCAD workbench init
│ ├── InitGui.py # FreeCAD GUI init
@@ -719,11 +811,11 @@ This section describes the purpose and key settings in each configuration file.
### Tool Management
| File | Purpose |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `.mise.toml` | Pins versions for Python, uv, just, pre-commit, and security tools (trivy, gitleaks, hadolint, shellcheck, actionlint, markdownlint-cli2). Also sets environment variables for FreeCAD connection settings. |
| `pyproject.toml` | Python project configuration: dependencies, build system, tool configs (ruff, mypy, pytest, bandit, codespell, commitizen). |
| `uv.lock` | Exact locked versions of all Python dependencies for reproducible builds. |
| File | Purpose |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.mise.toml` | Pins versions for Python, uv, just, pre-commit, and security tools (trivy, gitleaks, actionlint, markdownlint-cli2). Also sets environment variables for FreeCAD connection settings. |
| `pyproject.toml` | Python project configuration: dependencies, build system, tool configs (ruff, mypy, pytest, bandit, codespell, commitizen). |
| `uv.lock` | Exact locked versions of all Python dependencies for reproducible builds. |
### Code Quality
@@ -743,23 +835,23 @@ This section describes the purpose and key settings in each configuration file.
### Documentation
| File | Purpose |
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
| File | Purpose |
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
| `mkdocs.yaml` | MkDocs configuration: Material theme, plugins (macros, mkdocstrings, git-revision-date), navigation structure. |
| `docs/variables.yaml` | Variables for MkDocs macros plugin (project name, ports, paths). Use `{{@ variable @}}` syntax in docs. |
| `docs/variables.yaml` | Variables for MkDocs macros plugin (project name, ports, paths). Use `{{@ variable @}}` syntax in docs. |
### FreeCAD Addon
| File | Purpose |
| ------------- | ------------------------------------------------------------------------------------------ |
| File | Purpose |
| ------------- | ------------------------------------------------------------------------------------------------- |
| `package.xml` | FreeCAD addon metadata with per-component versioning. Updated automatically by release workflows. |
### GitHub
| File | Purpose |
| ---------------------------- | -------------------------------------------------------------------------- |
| `.github/dependabot.yaml` | Dependabot configuration for automated dependency updates. |
| `.github/workflows/*.yaml` | CI/CD workflows. See [GitHub Workflows](#github-actions-workflows) for details. |
| File | Purpose |
| -------------------------- | ------------------------------------------------------------------------------- |
| `.github/dependabot.yaml` | Dependabot configuration for automated dependency updates. |
| `.github/workflows/*.yaml` | CI/CD workflows. See [GitHub Workflows](#github-actions-workflows) for details. |
---
@@ -1009,7 +1101,7 @@ just freecad::run-headless
**CRITICAL**: Code running inside FreeCAD's Python environment cannot import packages that aren't available in FreeCAD's bundled Python (like `mcp`, `pydantic`, etc.).
The `headless_server.py` script in the workbench addon imports the plugin directly from the module file to avoid triggering the `mcp` import:
The `blocking_bridge.py` script in the workbench addon imports the plugin directly from the module file to avoid triggering the `mcp` import:
```python
# CORRECT - import directly from the module file in the same directory
@@ -1084,6 +1176,33 @@ This project uses relaxed mypy settings because FastMCP lacks proper type stubs.
- **Horizontal rules**: Must use `---` format (3 dashes)
### Markdownlint (MD060) - Table Formatting
- **Table column style**: All Markdown tables must use the "padded/aligned" style
- Every row in a table must have the same total character width
- Column separators (`|`) must align vertically across all rows
- The separator row dashes must match the column width set by the widest content
**Example - Correct (aligned):**
```markdown
| File | Purpose |
| ---------------- | -------------------------------------------- |
| `.mise.toml` | Tool version management configuration. |
| `pyproject.toml` | Python project configuration and deps. |
```
**Example - Incorrect (misaligned):**
```markdown
| File | Purpose |
|------|---------|
| `.mise.toml` | Tool version management configuration. |
| `pyproject.toml` | Python project configuration and deps. |
```
When editing tables, ensure all columns align by adding padding spaces before the closing `|`.
### Bandit Security
These checks are intentionally skipped in `pyproject.toml`:
@@ -1127,13 +1246,13 @@ This project uses component-specific release workflows along with CI/CD pipeline
### Release Workflows
| Workflow | Trigger | Purpose |
| --------------------------------- | ------------------------------------ | -------------------------------------------------------- |
| `mcp-server-release.yaml` | Tag: `robust-mcp-server-v*` | Builds and publishes MCP server to PyPI and Docker Hub |
| `mcp-workbench-release.yaml` | Tag: `robust-mcp-workbench-v*` | Creates GitHub Release with workbench addon archive |
| `macro-cut-magnets-release.yaml` | Tag: `macro-cut-object-for-magnets-v*` | Creates GitHub Release with macro archive |
| `macro-multi-export-release.yaml` | Tag: `macro-multi-export-v*` | Creates GitHub Release with macro archive |
| `macro-release-reusable.yaml` | Called by macro release workflows | Shared logic for macro releases (DRY) |
| Workflow | Trigger | Purpose |
| --------------------------------- | ---------------------------------------- | ------------------------------------------------------ |
| `mcp-server-release.yaml` | Tag: `robust-mcp-server-v*` | Builds and publishes MCP server to PyPI and Docker Hub |
| `mcp-workbench-release.yaml` | Tag: `robust-mcp-workbench-v*` | Creates GitHub Release with workbench addon archive |
| `macro-cut-magnets-release.yaml` | Tag: `macro-cut-object-for-magnets-v*` | Creates GitHub Release with macro archive |
| `macro-multi-export-release.yaml` | Tag: `macro-multi-export-v*` | Creates GitHub Release with macro archive |
| `macro-release-reusable.yaml` | Called by macro release workflows | Shared logic for macro releases (DRY) |
### Release Workflow Features
@@ -1142,7 +1261,7 @@ This project uses component-specific release workflows along with CI/CD pipeline
- Validates SemVer tag format
- Builds Python wheel and sdist
- Tests installation on Ubuntu and macOS
- Publishes to PyPI (stable) or TestPyPI (alpha)
- Publishes to PyPI (stable) or TestPyPI (alpha, beta, rc)
- Builds multi-arch Docker image (amd64 + arm64)
- Pushes to Docker Hub with version tags
- Creates GitHub Release with artifacts and changelog
@@ -1162,12 +1281,14 @@ This project uses component-specific release workflows along with CI/CD pipeline
This project uses **component-specific versioning**. Each component has its own git tag and release workflow:
| Component | Tag Format | Releases To |
| --------------------------- | ------------------------------------- | ------------------------------------ |
| MCP Server | `robust-mcp-server-vX.Y.Z` | PyPI, Docker Hub, GitHub Release |
| MCP Bridge Workbench | `robust-mcp-workbench-vX.Y.Z` | GitHub Release (archive) |
| Cut Object for Magnets Macro| `macro-cut-object-for-magnets-vX.Y.Z` | GitHub Release (archive) |
| Multi Export Macro | `macro-multi-export-vX.Y.Z` | GitHub Release (archive) |
| 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) |
| Cut Object for Magnets Macro | `macro-cut-object-for-magnets-vX.Y.Z` | GitHub Release (archive) |
| Multi Export Macro | `macro-multi-export-vX.Y.Z` | GitHub Release (archive) |
*Stable releases (`X.Y.Z`) publish to PyPI; non-stable releases (alpha, beta, rc) publish to TestPyPI only.
### Changelog Management
@@ -1194,7 +1315,7 @@ The project uses a single `CHANGELOG.md` with sections for each component. Befor
---
### MCP Bridge Workbench vX.Y.Z
### Robust MCP Bridge Workbench vX.Y.Z
...
```
@@ -1215,7 +1336,7 @@ just release::changes-since workbench
# Release the MCP server (triggers PyPI, Docker, GitHub release)
just release::tag-mcp-server 1.0.0
# Release the MCP Bridge workbench
# Release the Robust MCP Bridge workbench
just release::tag-workbench 1.0.0
# Release macros
@@ -1231,10 +1352,10 @@ just release::latest-versions
All versions follow SemVer 2.0:
- `X.Y.Z` - Stable release
- `X.Y.Z` - Stable release (PyPI only)
- `X.Y.Z-alpha` or `X.Y.Z-alpha.N` - Alpha (TestPyPI only)
- `X.Y.Z-beta` or `X.Y.Z-beta.N` - Beta (PyPI)
- `X.Y.Z-rc.N` - Release candidate (PyPI)
- `X.Y.Z-beta` or `X.Y.Z-beta.N` - Beta (TestPyPI only)
- `X.Y.Z-rc.N` - Release candidate (TestPyPI only)
### What Happens on Release
@@ -1243,7 +1364,7 @@ All versions follow SemVer 2.0:
1. Validates tag format
2. Builds Python wheel and sdist
3. Tests installation on Ubuntu and macOS
4. Publishes to PyPI (or TestPyPI for alpha)
4. Publishes to PyPI (or TestPyPI for alpha, beta, rc)
5. Builds multi-arch Docker image
6. Pushes to Docker Hub
7. Creates GitHub release with artifacts