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:
co-authored by
Claude Opus 4.5
parent
bcc3048876
commit
8c338f6da7
@@ -62,10 +62,9 @@ install:
|
||||
echo ""
|
||||
echo "CodeRabbit CLI installed. Run 'just coderabbit::login' to authenticate."
|
||||
|
||||
# Check if CodeRabbit CLI is installed
|
||||
# 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; }
|
||||
@coderabbit --version
|
||||
|
||||
# =============================================================================
|
||||
# Authentication
|
||||
@@ -199,13 +198,13 @@ review-json: check-installed
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Configuration
|
||||
# Help & Info
|
||||
# =============================================================================
|
||||
|
||||
# Show CodeRabbit configuration
|
||||
config-show: check-installed
|
||||
coderabbit config show
|
||||
|
||||
# Show help for all CodeRabbit commands
|
||||
help: check-installed
|
||||
coderabbit --help
|
||||
|
||||
# Show CodeRabbit CLI version
|
||||
version: check-installed
|
||||
@coderabbit --version
|
||||
|
||||
+23
-5
@@ -16,7 +16,7 @@ install-deps:
|
||||
@echo ""
|
||||
@echo "Dependencies installed!"
|
||||
@echo ""
|
||||
@echo "To run the MCP server:"
|
||||
@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"
|
||||
@@ -26,11 +26,29 @@ 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 (uv.lock + pre-commit hooks)
|
||||
# Update all dependencies to latest versions (mise tools, uv.lock, pre-commit hooks)
|
||||
update-deps:
|
||||
cd {{project_root}} && uv lock --upgrade
|
||||
cd {{project_root}} && uv sync --all-extras
|
||||
cd {{project_root}} && uv run pre-commit autoupdate
|
||||
#!/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
|
||||
|
||||
+125
-64
@@ -4,6 +4,7 @@
|
||||
# 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()
|
||||
@@ -14,21 +15,37 @@ build:
|
||||
|
||||
# Build Docker image with specific tag
|
||||
build-tag tag:
|
||||
docker build -t {{image_name}}:{{tag}} {{project_root}}
|
||||
docker build --load -t {{image_name}}:{{tag}} {{project_root}}
|
||||
|
||||
# Build multi-architecture image (amd64 and arm64)
|
||||
build-multi:
|
||||
# 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
|
||||
build-push tag="latest":
|
||||
# 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 multi-architecture image locally (loads current arch only)
|
||||
build-load:
|
||||
docker buildx build --platform linux/amd64,linux/arm64 \
|
||||
# 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}}
|
||||
|
||||
@@ -101,8 +118,8 @@ scan-strict:
|
||||
|
||||
# Scan Docker image and output SARIF report
|
||||
scan-sarif output="trivy-results.sarif":
|
||||
trivy image --format sarif --output {{output}} {{image_name}}
|
||||
@echo "SARIF report written to {{output}}"
|
||||
trivy image --format sarif --output {{project_root}}/{{output}} {{image_name}}
|
||||
@echo "SARIF report written to {{project_root}}/{{output}}"
|
||||
|
||||
# Create and configure buildx builder for multi-arch builds
|
||||
setup-buildx:
|
||||
@@ -124,6 +141,22 @@ 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 "=========================================="
|
||||
@@ -138,6 +171,14 @@ test:
|
||||
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}}
|
||||
@@ -167,9 +208,7 @@ test:
|
||||
echo "✗ ERROR: FreeCAD MCP bridge did not start within ${MAX_RETRIES}s"
|
||||
echo ""
|
||||
echo "FreeCAD log output:"
|
||||
cat "$FREECAD_LOG" | tail -30
|
||||
rm -f "$FREECAD_LOG"
|
||||
kill $FREECAD_PID 2>/dev/null || true
|
||||
tail -30 "$FREECAD_LOG"
|
||||
exit 1
|
||||
fi
|
||||
# Show progress less frequently to reduce noise
|
||||
@@ -179,6 +218,7 @@ test:
|
||||
sleep 1
|
||||
done
|
||||
rm -f "$FREECAD_LOG"
|
||||
FREECAD_LOG="" # Clear so cleanup doesn't try to delete again
|
||||
echo "✓ FreeCAD MCP bridge started (took ${RETRY_COUNT}s)"
|
||||
STARTED_FREECAD=true
|
||||
fi
|
||||
@@ -186,63 +226,58 @@ test:
|
||||
|
||||
# Run the container and test communication
|
||||
echo "Step 3: Running container and testing MCP communication..."
|
||||
echo " Running MCP server in container..."
|
||||
echo " Running Robust MCP Server in container..."
|
||||
|
||||
# Send MCP initialize and tool call requests via JSON-RPC over stdio
|
||||
# Note: Using printf with \n to avoid just parsing issues with unindented lines
|
||||
MCP_INIT='{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
|
||||
MCP_CALL='{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_mcp_server_environment","arguments":{}}}'
|
||||
# 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"
|
||||
|
||||
CONTAINER_OUTPUT=$(printf '%s\n%s\n' "$MCP_INIT" "$MCP_CALL" | \
|
||||
timeout 30 docker run --rm -i \
|
||||
# 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) || {
|
||||
echo "✗ Container failed to respond"
|
||||
if [ "$STARTED_FREECAD" = true ]; then
|
||||
kill $FREECAD_PID 2>/dev/null || true
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
{{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
|
||||
DOCKER_CONFIRMED=false
|
||||
|
||||
# Check if the response indicates we're in a Docker container
|
||||
if echo "$CONTAINER_OUTPUT" | grep -q '"in_docker": true\|"in_docker":true'; then
|
||||
echo "✓ Response confirms running in Docker container"
|
||||
DOCKER_CONFIRMED=true
|
||||
elif echo "$CONTAINER_OUTPUT" | grep -q '"os_name": "Linux"\|"os_name":"Linux"'; then
|
||||
echo "✓ Response shows Linux OS (expected for Docker)"
|
||||
DOCKER_CONFIRMED=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 "⚠ Warning: Could not confirm Docker detection"
|
||||
echo "✗ Container failed to respond with valid MCP response"
|
||||
TEST_PASSED=false
|
||||
fi
|
||||
|
||||
# Check for hostname (containers have short random hostnames)
|
||||
if echo "$CONTAINER_OUTPUT" | grep -q '"hostname"'; then
|
||||
HOSTNAME=$(echo "$CONTAINER_OUTPUT" | grep -o '"hostname"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1)
|
||||
echo "✓ Container hostname: $HOSTNAME"
|
||||
# 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: No hostname in response"
|
||||
echo "⚠ Warning: Could not confirm server identity"
|
||||
fi
|
||||
|
||||
# Check we got a valid response (not an error)
|
||||
if echo "$CONTAINER_OUTPUT" | grep -q '"error"'; then
|
||||
# Check if it's just a "not connected" error (expected without proper init)
|
||||
if echo "$CONTAINER_OUTPUT" | grep -q 'Not connected\|Connection refused'; then
|
||||
echo " Note: FreeCAD connection test - bridge communication verified"
|
||||
else
|
||||
echo "✗ Error: Response contained an error"
|
||||
echo " $CONTAINER_OUTPUT" | tail -5
|
||||
TEST_PASSED=false
|
||||
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 ""
|
||||
@@ -252,23 +287,14 @@ test:
|
||||
echo "$CONTAINER_OUTPUT" | tail -20
|
||||
echo ""
|
||||
|
||||
# Cleanup
|
||||
if [ "$STARTED_FREECAD" = true ]; then
|
||||
echo "Step 5: Cleaning up..."
|
||||
kill $FREECAD_PID 2>/dev/null || true
|
||||
echo "✓ Stopped FreeCAD headless server"
|
||||
fi
|
||||
# Note: FreeCAD cleanup is handled by the EXIT trap
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
if [ "$TEST_PASSED" = true ] && [ "$DOCKER_CONFIRMED" = true ]; then
|
||||
if [ "$TEST_PASSED" = true ]; then
|
||||
echo "✓ PASSED: Docker integration test succeeded!"
|
||||
echo " - Container ran successfully"
|
||||
echo " - Confirmed running in Docker environment"
|
||||
elif [ "$TEST_PASSED" = true ]; then
|
||||
echo "⚠ PARTIAL: Docker integration test completed with warnings"
|
||||
echo " - Container ran successfully"
|
||||
echo " - Could not confirm Docker environment detection"
|
||||
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"
|
||||
@@ -279,3 +305,38 @@ test:
|
||||
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
|
||||
|
||||
@@ -13,8 +13,9 @@ 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
|
||||
-cd {{project_root}} && uv run mkdocs serve
|
||||
|
||||
# Build and open documentation in browser
|
||||
open:
|
||||
|
||||
+22
-82
@@ -8,13 +8,13 @@ project_root := justfile_directory()
|
||||
# Running FreeCAD with MCP Bridge
|
||||
# =============================================================================
|
||||
|
||||
# Run MCP bridge server in FreeCAD headless mode
|
||||
# Run MCP bridge server in FreeCAD headless mode (blocking)
|
||||
run-headless:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
PROJECT_DIR="{{project_root}}"
|
||||
# Use the headless server from the addon directory (source of truth)
|
||||
SCRIPT_PATH="${PROJECT_DIR}/addon/FreecadRobustMCP/freecad_mcp_bridge/headless_server.py"
|
||||
# Use the blocking bridge script from the addon directory (source of truth)
|
||||
SCRIPT_PATH="${PROJECT_DIR}/addon/FreecadRobustMCP/freecad_mcp_bridge/blocking_bridge.py"
|
||||
|
||||
# Find FreeCADCmd executable based on OS
|
||||
FREECAD_CMD=""
|
||||
@@ -65,15 +65,15 @@ run-headless:
|
||||
echo "Using FreeCAD: $FREECAD_CMD"
|
||||
echo ""
|
||||
|
||||
# Run FreeCADCmd with the headless server script
|
||||
# Run FreeCADCmd with the blocking bridge script
|
||||
"$FREECAD_CMD" "$SCRIPT_PATH"
|
||||
|
||||
# Run MCP bridge with custom FreeCAD 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/FreecadRobustMCP/freecad_mcp_bridge/headless_server.py"
|
||||
SCRIPT_PATH="${PROJECT_DIR}/addon/FreecadRobustMCP/freecad_mcp_bridge/blocking_bridge.py"
|
||||
|
||||
if [[ ! -x "{{freecad_cmd}}" ]]; then
|
||||
echo "ERROR: FreeCADCmd not found or not executable: {{freecad_cmd}}"
|
||||
@@ -83,60 +83,24 @@ run-headless-custom freecad_cmd:
|
||||
echo "Using FreeCAD: {{freecad_cmd}}"
|
||||
echo ""
|
||||
|
||||
# Run FreeCADCmd with the headless server script
|
||||
# Run FreeCADCmd with the blocking bridge script
|
||||
"{{freecad_cmd}}" "$SCRIPT_PATH"
|
||||
|
||||
# Run FreeCAD GUI with MCP bridge (requires workbench to be installed)
|
||||
# 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}}"
|
||||
|
||||
# Create a temporary startup script that starts the MCP bridge
|
||||
STARTUP_SCRIPT=$(mktemp /tmp/freecad_mcp_startup.XXXXXX.py)
|
||||
# Use the shared startup script from the addon directory
|
||||
STARTUP_SCRIPT="${PROJECT_DIR}/addon/FreecadRobustMCP/freecad_mcp_bridge/startup_bridge.py"
|
||||
|
||||
# Use the server from the installed workbench location
|
||||
# Note: For development, use run-gui-custom which passes the addon path explicitly
|
||||
cat > "$STARTUP_SCRIPT" << 'PYTHON_EOF'
|
||||
# FreeCAD MCP Bridge Auto-Start Script
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Determine installed workbench location based on platform
|
||||
if sys.platform == "darwin":
|
||||
addon_path = Path.home() / "Library" / "Application Support" / "FreeCAD" / "Mod" / "FreecadRobustMCP" / "freecad_mcp_bridge"
|
||||
elif sys.platform == "win32":
|
||||
addon_path = Path(os.environ.get("APPDATA", "")) / "FreeCAD" / "Mod" / "FreecadRobustMCP" / "freecad_mcp_bridge"
|
||||
else:
|
||||
addon_path = Path.home() / ".local" / "share" / "FreeCAD" / "Mod" / "FreecadRobustMCP" / "freecad_mcp_bridge"
|
||||
|
||||
if not addon_path.exists():
|
||||
import FreeCAD
|
||||
FreeCAD.Console.PrintError("MCP Bridge workbench not found.\n")
|
||||
FreeCAD.Console.PrintError(f"Expected at: {addon_path}\n")
|
||||
FreeCAD.Console.PrintError("Install the workbench first: just freecad::install-workbench\n")
|
||||
else:
|
||||
try:
|
||||
addon_path_str = str(addon_path)
|
||||
if addon_path_str not in sys.path:
|
||||
sys.path.insert(0, addon_path_str)
|
||||
from server import FreecadMCPPlugin
|
||||
plugin = FreecadMCPPlugin(
|
||||
host="localhost",
|
||||
port=9876,
|
||||
xmlrpc_port=9875,
|
||||
enable_xmlrpc=True,
|
||||
)
|
||||
plugin.start()
|
||||
import FreeCAD
|
||||
FreeCAD.Console.PrintMessage("\nMCP Bridge started!\n")
|
||||
FreeCAD.Console.PrintMessage(" - XML-RPC: localhost:9875\n")
|
||||
FreeCAD.Console.PrintMessage(" - Socket: localhost:9876\n\n")
|
||||
except Exception as e:
|
||||
import FreeCAD
|
||||
FreeCAD.Console.PrintError(f"Failed to start MCP Bridge: {e}\n")
|
||||
PYTHON_EOF
|
||||
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=""
|
||||
@@ -171,7 +135,6 @@ run-gui:
|
||||
fi
|
||||
|
||||
if [[ -z "$FREECAD_GUI" ]]; then
|
||||
rm -f "$STARTUP_SCRIPT"
|
||||
echo "ERROR: FreeCAD not found!"
|
||||
echo ""
|
||||
echo "Please install FreeCAD or use:"
|
||||
@@ -208,36 +171,13 @@ run-gui-custom freecad_path:
|
||||
set -euo pipefail
|
||||
PROJECT_DIR="{{project_root}}"
|
||||
|
||||
# Create a temporary startup script
|
||||
STARTUP_SCRIPT=$(mktemp /tmp/freecad_mcp_startup.XXXXXX.py)
|
||||
# Use the shared startup script from the addon directory
|
||||
STARTUP_SCRIPT="${PROJECT_DIR}/addon/FreecadRobustMCP/freecad_mcp_bridge/startup_bridge.py"
|
||||
|
||||
# Use the server from the addon directory
|
||||
ADDON_PATH="${PROJECT_DIR}/addon/FreecadRobustMCP/freecad_mcp_bridge"
|
||||
|
||||
cat > "$STARTUP_SCRIPT" << EOF
|
||||
# FreeCAD MCP Bridge Auto-Start Script
|
||||
import sys
|
||||
addon_path = "${ADDON_PATH}"
|
||||
if addon_path not in sys.path:
|
||||
sys.path.insert(0, addon_path)
|
||||
|
||||
try:
|
||||
from server import FreecadMCPPlugin
|
||||
plugin = FreecadMCPPlugin(
|
||||
host="localhost",
|
||||
port=9876,
|
||||
xmlrpc_port=9875,
|
||||
enable_xmlrpc=True,
|
||||
)
|
||||
plugin.start()
|
||||
import FreeCAD
|
||||
FreeCAD.Console.PrintMessage("\\nMCP Bridge started!\\n")
|
||||
FreeCAD.Console.PrintMessage(" - XML-RPC: localhost:9875\\n")
|
||||
FreeCAD.Console.PrintMessage(" - Socket: localhost:9876\\n\\n")
|
||||
except Exception as e:
|
||||
import FreeCAD
|
||||
FreeCAD.Console.PrintError(f"Failed to start MCP Bridge: {e}\\n")
|
||||
EOF
|
||||
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}}"
|
||||
|
||||
+274
-27
@@ -2,8 +2,8 @@
|
||||
# Usage: just install::mcp-server, just install::mcp-bridge-workbench, etc.
|
||||
#
|
||||
# This module installs components for end users:
|
||||
# - MCP Server (as a uv tool, available system-wide)
|
||||
# - MCP Bridge Workbench (FreeCAD addon)
|
||||
# - Robust MCP Server (as a uv tool, available system-wide)
|
||||
# - Robust MCP Bridge Workbench (FreeCAD addon)
|
||||
# - FreeCAD Macros (CutObjectForMagnets, MultiExport)
|
||||
#
|
||||
# For developer setup (Python dependencies in virtualenv), use: just dev::install-deps
|
||||
@@ -23,52 +23,75 @@ project_root := justfile_directory()
|
||||
# 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
|
||||
MOD_DIR="$HOME/Library/Application Support/FreeCAD/Mod"
|
||||
MACRO_DIR="$HOME/Library/Application Support/FreeCAD/Macro"
|
||||
FREECAD_BASE="$HOME/Library/Application Support/FreeCAD"
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
MOD_DIR="$HOME/.local/share/FreeCAD/Mod"
|
||||
MACRO_DIR="$HOME/.local/share/FreeCAD/Macro"
|
||||
FREECAD_BASE="$HOME/.local/share/FreeCAD"
|
||||
else
|
||||
# Windows: validate APPDATA or use fallback
|
||||
if [[ -n "$APPDATA" ]]; then
|
||||
FREECAD_BASE="$APPDATA"
|
||||
FREECAD_BASE="$APPDATA/FreeCAD"
|
||||
elif [[ -n "$HOME" ]]; then
|
||||
# Fallback to standard Windows location under HOME
|
||||
FREECAD_BASE="$HOME/AppData/Roaming"
|
||||
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
|
||||
MOD_DIR="$FREECAD_BASE/FreeCAD/Mod"
|
||||
MACRO_DIR="$FREECAD_BASE/FreeCAD/Macro"
|
||||
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
|
||||
|
||||
# =============================================================================
|
||||
# MCP Server Installation
|
||||
# Robust MCP Server Installation
|
||||
# =============================================================================
|
||||
|
||||
# Install the MCP server as a user tool (available system-wide via uv)
|
||||
# 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 MCP server as a uv tool..."
|
||||
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 "MCP Server installed!"
|
||||
echo "Robust MCP Server installed!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "The 'freecad-mcp' command is now available system-wide."
|
||||
@@ -82,16 +105,27 @@ mcp-server:
|
||||
echo ' "command": "freecad-mcp"'
|
||||
echo ' }'
|
||||
echo ""
|
||||
echo "Note: If you have uncommitted local changes, use 'just install::mcp-server-clean'"
|
||||
echo ""
|
||||
|
||||
# Uninstall the MCP server tool
|
||||
# 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 MCP server..."
|
||||
uv tool uninstall freecad-robust-mcp || echo "MCP server was not installed as a uv tool"
|
||||
echo "Uninstalling Robust MCP Server..."
|
||||
uv tool uninstall freecad-robust-mcp || echo "Robust MCP Server was not installed as a uv tool"
|
||||
|
||||
# =============================================================================
|
||||
# MCP Bridge Workbench Installation
|
||||
# Robust MCP Bridge Workbench Installation
|
||||
# =============================================================================
|
||||
|
||||
# Install the FreeCAD Robust MCP workbench addon to FreeCAD's Mod directory
|
||||
@@ -125,6 +159,135 @@ mcp-bridge-workbench:
|
||||
# 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 'FreecadRobustMCP.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 'FreecadRobustMCPWorkbench'
|
||||
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!"
|
||||
@@ -134,7 +297,7 @@ mcp-bridge-workbench:
|
||||
echo ""
|
||||
echo "To use:"
|
||||
echo " 1. Start FreeCAD"
|
||||
echo " 2. Select the 'MCP Bridge' workbench from the workbench selector"
|
||||
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 ""
|
||||
@@ -179,6 +342,13 @@ macro-cut:
|
||||
|
||||
mkdir -p "$MACRO_DIR"
|
||||
|
||||
# Remove existing installation if present (clean install)
|
||||
if [[ -f "$MACRO_DIR/CutObjectForMagnets.FCMacro" ]]; then
|
||||
echo "Removing existing CutObjectForMagnets macro..."
|
||||
rm -f "$MACRO_DIR/CutObjectForMagnets.FCMacro"
|
||||
rm -f "$MACRO_DIR/CutObjectForMagnets.svg"
|
||||
fi
|
||||
|
||||
# Copy the macro file
|
||||
cp "$MACRO_SRC" "$MACRO_DIR/"
|
||||
|
||||
@@ -230,6 +400,13 @@ macro-export:
|
||||
|
||||
mkdir -p "$MACRO_DIR"
|
||||
|
||||
# Remove existing installation if present (clean install)
|
||||
if [[ -f "$MACRO_DIR/MultiExport.FCMacro" ]]; then
|
||||
echo "Removing existing MultiExport macro..."
|
||||
rm -f "$MACRO_DIR/MultiExport.FCMacro"
|
||||
rm -f "$MACRO_DIR/MultiExport.svg"
|
||||
fi
|
||||
|
||||
# Copy the macro file
|
||||
cp "$MACRO_SRC" "$MACRO_DIR/"
|
||||
|
||||
@@ -281,53 +458,123 @@ status:
|
||||
# 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 MCP Server (installed as uv tool)
|
||||
# Check Robust MCP Server (installed as uv tool)
|
||||
if command -v freecad-mcp &> /dev/null; then
|
||||
echo "✓ MCP Server: INSTALLED (as uv tool)"
|
||||
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
|
||||
echo "✓ MCP Server: AVAILABLE (via dev environment)"
|
||||
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 "○ MCP Server: DEV SOURCE AVAILABLE (needs setup)"
|
||||
echo "○ Robust MCP Server: DEV SOURCE AVAILABLE (needs setup)"
|
||||
echo " Setup: uv sync --all-extras"
|
||||
echo " Then run: just mcp::run"
|
||||
fi
|
||||
else
|
||||
echo "✗ MCP Server: NOT INSTALLED"
|
||||
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"
|
||||
}
|
||||
|
||||
# Helper function to extract __Version__ from macro files (handles single/double quotes)
|
||||
# Uses environment variable to pass file path safely to Python (avoids shell interpolation)
|
||||
extract_macro_version() {
|
||||
local file="$1"
|
||||
MACRO_FILE="$file" python3 -c '
|
||||
import os
|
||||
import re
|
||||
try:
|
||||
macro_file = os.environ.get("MACRO_FILE", "")
|
||||
content = open(macro_file).read()
|
||||
match = re.search(r"__Version__\s*=\s*[\"'"'"']([^\"'"'"']+)[\"'"'"']", content)
|
||||
print(match.group(1) if match else "unknown")
|
||||
except Exception:
|
||||
print("unknown")
|
||||
' 2>/dev/null || echo "unknown"
|
||||
}
|
||||
|
||||
# Check workbench
|
||||
if [[ -d "$MOD_DIR/FreecadRobustMCP" ]]; then
|
||||
echo "✓ MCP Bridge Workbench: INSTALLED"
|
||||
WB_VERSION="unknown"
|
||||
if [[ -f "$MOD_DIR/FreecadRobustMCP/package.xml" ]]; then
|
||||
WB_VERSION=$(extract_package_version "$MOD_DIR/FreecadRobustMCP/package.xml")
|
||||
fi
|
||||
WB_MOD_TIME=$(get_mod_time "$MOD_DIR/FreecadRobustMCP/InitGui.py")
|
||||
echo "✓ Robust MCP Bridge Workbench: INSTALLED"
|
||||
echo " Version: $WB_VERSION"
|
||||
echo " Updated: $WB_MOD_TIME"
|
||||
echo " Path: $MOD_DIR/FreecadRobustMCP"
|
||||
else
|
||||
echo "✗ MCP Bridge Workbench: NOT INSTALLED"
|
||||
echo "✗ Robust MCP Bridge Workbench: NOT INSTALLED"
|
||||
echo " Install: just install::mcp-bridge-workbench"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check macros
|
||||
|
||||
echo "Macros:"
|
||||
if [[ -f "$MACRO_DIR/CutObjectForMagnets.FCMacro" ]]; then
|
||||
CUT_VERSION=$(extract_macro_version "$MACRO_DIR/CutObjectForMagnets.FCMacro")
|
||||
CUT_MOD_TIME=$(get_mod_time "$MACRO_DIR/CutObjectForMagnets.FCMacro")
|
||||
echo " ✓ CutObjectForMagnets: INSTALLED"
|
||||
echo " Version: $CUT_VERSION"
|
||||
echo " Updated: $CUT_MOD_TIME"
|
||||
else
|
||||
echo " ✗ CutObjectForMagnets: NOT INSTALLED"
|
||||
echo " Install: just install::macro-cut"
|
||||
fi
|
||||
|
||||
if [[ -f "$MACRO_DIR/MultiExport.FCMacro" ]]; then
|
||||
EXPORT_VERSION=$(extract_macro_version "$MACRO_DIR/MultiExport.FCMacro")
|
||||
EXPORT_MOD_TIME=$(get_mod_time "$MACRO_DIR/MultiExport.FCMacro")
|
||||
echo " ✓ MultiExport: INSTALLED"
|
||||
echo " Version: $EXPORT_VERSION"
|
||||
echo " Updated: $EXPORT_MOD_TIME"
|
||||
else
|
||||
echo " ✗ MultiExport: NOT INSTALLED"
|
||||
echo " Install: just install::macro-export"
|
||||
|
||||
+48
-8
@@ -1,18 +1,58 @@
|
||||
# MCP Server commands
|
||||
# Robust MCP Server commands
|
||||
# Usage: just mcp::run, just mcp::run-debug, etc.
|
||||
#
|
||||
# These commands run the MCP server that connects to FreeCAD.
|
||||
# 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
|
||||
|
||||
# Run the MCP server (stdio mode - default for Claude Code integration)
|
||||
run:
|
||||
uv run freecad-mcp
|
||||
# Check if FreeCAD bridge is available (test connection without starting server)
|
||||
check:
|
||||
uv run freecad-mcp --check
|
||||
|
||||
# Run the MCP server with debug logging
|
||||
# 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:
|
||||
FREECAD_MCP_LOG_LEVEL=DEBUG uv run freecad-mcp
|
||||
#!/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":
|
||||
FREECAD_MCP_TRANSPORT=http FREECAD_MCP_PORT={{port}} uv run freecad-mcp
|
||||
#!/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
|
||||
|
||||
+4
-4
@@ -48,11 +48,11 @@ typecheck:
|
||||
# Run security scanning (code vulnerabilities)
|
||||
security:
|
||||
uv run bandit -c {{project_root}}/pyproject.toml -r {{project_root}}/src
|
||||
uv run safety scan --detailed-output
|
||||
cd {{project_root}} && uv run safety scan --detailed-output
|
||||
|
||||
# Run spell checking
|
||||
spellcheck:
|
||||
uv run codespell {{project_root}}/src {{project_root}}/tests {{project_root}}/docs
|
||||
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)
|
||||
@@ -78,9 +78,9 @@ scan-detect:
|
||||
scan-audit:
|
||||
uv run detect-secrets audit {{project_root}}/.secrets.baseline
|
||||
|
||||
# Update detect-secrets baseline with new findings
|
||||
# Update detect-secrets baseline with new findings (preserves audit metadata)
|
||||
scan-baseline-update:
|
||||
uv run detect-secrets scan --baseline {{project_root}}/.secrets.baseline --update
|
||||
uv run detect-secrets scan --baseline {{project_root}}/.secrets.baseline --update {{project_root}}
|
||||
|
||||
# Run trufflehog for verified secrets (via pre-commit - not installed standalone)
|
||||
scan-trufflehog:
|
||||
|
||||
+257
-26
@@ -18,11 +18,11 @@
|
||||
#
|
||||
# Version Format (SemVer 2.0):
|
||||
# - X.Y.Z - Stable release
|
||||
# - X.Y.Z-alpha - Alpha (TestPyPI only for MCP server)
|
||||
# - X.Y.Z-alpha.N - Alpha with number
|
||||
# - X.Y.Z-beta - Beta
|
||||
# - X.Y.Z-beta.N - Beta with number
|
||||
# - X.Y.Z-rc.N - Release candidate
|
||||
# - X.Y.Z-alpha - Alpha (TestPyPI only)
|
||||
# - X.Y.Z-alpha.N - Alpha with number (TestPyPI only)
|
||||
# - X.Y.Z-beta - Beta (TestPyPI only)
|
||||
# - X.Y.Z-beta.N - Beta with number (TestPyPI only)
|
||||
# - X.Y.Z-rc.N - Release candidate (TestPyPI only)
|
||||
|
||||
# Project root directory
|
||||
project_root := justfile_directory()
|
||||
@@ -31,7 +31,7 @@ project_root := justfile_directory()
|
||||
# Version Bump Commands
|
||||
# =============================================================================
|
||||
|
||||
# Bump the MCP Bridge workbench version in all source files
|
||||
# Bump the Robust MCP Bridge workbench version in all source files
|
||||
bump-workbench version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@@ -46,7 +46,7 @@ bump-workbench version:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Bumping MCP Bridge Workbench to version: $VERSION (date: $TODAY)"
|
||||
echo "Bumping Robust MCP Bridge Workbench to version: $VERSION (date: $TODAY)"
|
||||
echo ""
|
||||
|
||||
# Update __version__ in the bridge module's __init__.py
|
||||
@@ -191,8 +191,8 @@ bump-macro-export version: (_bump-macro "Multi_Export" "Multi Export" "MultiExpo
|
||||
# Tag Creation Commands
|
||||
# =============================================================================
|
||||
|
||||
# Create and push a release tag for the MCP server (triggers PyPI + Docker release)
|
||||
# Note: MCP server uses setuptools-scm, so version is derived from git tag at build time
|
||||
# Create and push a release tag for the Robust MCP Server (triggers PyPI + Docker release)
|
||||
# Note: Robust MCP Server uses setuptools-scm, so version is derived from git tag at build time
|
||||
tag-mcp-server version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@@ -213,7 +213,7 @@ tag-mcp-server version:
|
||||
echo "Creating tag: $TAG"
|
||||
echo ""
|
||||
echo "This will trigger:"
|
||||
echo " - PyPI release (beta/rc/stable) or TestPyPI (alpha)"
|
||||
echo " - PyPI release (stable) or TestPyPI (alpha/beta/rc)"
|
||||
echo " - Docker Hub release"
|
||||
echo " - GitHub release with wheel and tar.gz"
|
||||
echo ""
|
||||
@@ -224,13 +224,13 @@ tag-mcp-server version:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git tag -a "$TAG" -m "Release MCP Server v{{version}}"
|
||||
git tag -a "$TAG" -m "Release Robust MCP Server v{{version}}"
|
||||
git push origin "$TAG"
|
||||
echo ""
|
||||
echo "Tag $TAG created and pushed!"
|
||||
echo "Watch the release at: https://github.com/spkane/freecad-robust-mcp-and-more/actions"
|
||||
|
||||
# Create and push a release tag for the MCP Bridge workbench
|
||||
# Create and push a release tag for the Robust MCP Bridge workbench
|
||||
tag-workbench version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@@ -289,7 +289,7 @@ tag-workbench version:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git tag -a "$TAG" -m "Release MCP Bridge Workbench v{{version}}"
|
||||
git tag -a "$TAG" -m "Release Robust MCP Bridge Workbench v{{version}}"
|
||||
git push origin "$TAG"
|
||||
echo ""
|
||||
echo "Tag $TAG created and pushed!"
|
||||
@@ -386,7 +386,7 @@ tag-macro-export version: (_tag-macro "Multi_Export" "Multi Export" "MultiExport
|
||||
# List all release tags grouped by component
|
||||
list-tags:
|
||||
#!/usr/bin/env bash
|
||||
echo "=== MCP Server Releases ==="
|
||||
echo "=== Robust MCP Server Releases ==="
|
||||
git tag -l 'robust-mcp-server-v*' --sort=-v:refname | head -10
|
||||
echo ""
|
||||
echo "=== MCP Workbench Releases ==="
|
||||
@@ -407,8 +407,8 @@ latest-versions:
|
||||
WORKBENCH_TAG=$(git tag -l 'robust-mcp-workbench-v*' --sort=-v:refname | head -n1)
|
||||
MAGNETS_TAG=$(git tag -l 'macro-cut-object-for-magnets-v*' --sort=-v:refname | head -n1)
|
||||
EXPORT_TAG=$(git tag -l 'macro-multi-export-v*' --sort=-v:refname | head -n1)
|
||||
echo " MCP Server: ${SERVER_TAG:-none}"
|
||||
echo " MCP Workbench: ${WORKBENCH_TAG:-none}"
|
||||
echo " Robust MCP Server: ${SERVER_TAG:-none}"
|
||||
echo " Robust MCP Workbench: ${WORKBENCH_TAG:-none}"
|
||||
echo " Macro Magnets: ${MAGNETS_TAG:-none}"
|
||||
echo " Macro Export: ${EXPORT_TAG:-none}"
|
||||
|
||||
@@ -518,27 +518,27 @@ status:
|
||||
fi
|
||||
}
|
||||
|
||||
# MCP Server
|
||||
# Robust MCP Server
|
||||
SERVER_CHANGES=$(count_changes "robust-mcp-server-v" "src/freecad_mcp pyproject.toml Dockerfile")
|
||||
SERVER_TAG=$(git tag -l 'robust-mcp-server-v*' --sort=-v:refname | head -1)
|
||||
if [ "$SERVER_CHANGES" -gt 0 ]; then
|
||||
echo "MCP Server: $SERVER_CHANGES unreleased commit(s)"
|
||||
echo "Robust MCP Server: $SERVER_CHANGES unreleased commit(s)"
|
||||
echo " Latest: ${SERVER_TAG:-none}"
|
||||
echo " View: just release::changes-since mcp-server"
|
||||
else
|
||||
echo "MCP Server: up to date (${SERVER_TAG:-no releases})"
|
||||
echo "Robust MCP Server: up to date (${SERVER_TAG:-no releases})"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Workbench
|
||||
# Robust MCP Bridge Workbench
|
||||
WORKBENCH_CHANGES=$(count_changes "robust-mcp-workbench-v" "addon/FreecadRobustMCP")
|
||||
WORKBENCH_TAG=$(git tag -l 'robust-mcp-workbench-v*' --sort=-v:refname | head -1)
|
||||
if [ "$WORKBENCH_CHANGES" -gt 0 ]; then
|
||||
echo "MCP Workbench: $WORKBENCH_CHANGES unreleased commit(s)"
|
||||
echo "Robust MCP Bridge Workbench: $WORKBENCH_CHANGES unreleased commit(s)"
|
||||
echo " Latest: ${WORKBENCH_TAG:-none}"
|
||||
echo " View: just release::changes-since workbench"
|
||||
else
|
||||
echo "MCP Workbench: up to date (${WORKBENCH_TAG:-no releases})"
|
||||
echo "Robust MCP Bridge Workbench: up to date (${WORKBENCH_TAG:-no releases})"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
@@ -580,12 +580,12 @@ draft-notes component:
|
||||
mcp-server|server)
|
||||
PREFIX="robust-mcp-server-v"
|
||||
PATHS="src/freecad_mcp pyproject.toml Dockerfile"
|
||||
COMPONENT_NAME="MCP Server"
|
||||
COMPONENT_NAME="Robust MCP Server"
|
||||
;;
|
||||
workbench)
|
||||
PREFIX="robust-mcp-workbench-v"
|
||||
PATHS="addon/FreecadRobustMCP"
|
||||
COMPONENT_NAME="MCP Bridge Workbench"
|
||||
COMPONENT_NAME="Robust MCP Bridge Workbench"
|
||||
;;
|
||||
macro-magnets|magnets)
|
||||
PREFIX="macro-cut-object-for-magnets-v"
|
||||
@@ -659,10 +659,10 @@ extract-changelog component version:
|
||||
# Match header exactly as it appears in CHANGELOG.md
|
||||
case "{{component}}" in
|
||||
mcp-server|server)
|
||||
HEADER="### MCP Server v{{version}}"
|
||||
HEADER="### Robust MCP Server v{{version}}"
|
||||
;;
|
||||
workbench)
|
||||
HEADER="### MCP Bridge Workbench v{{version}}"
|
||||
HEADER="### Robust MCP Bridge Workbench v{{version}}"
|
||||
;;
|
||||
macro-magnets|magnets)
|
||||
HEADER="### Cut Object for Magnets Macro v{{version}}"
|
||||
@@ -730,3 +730,234 @@ dry-run-tag component version:
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# =============================================================================
|
||||
# FreeCAD Wiki Update Helpers
|
||||
# =============================================================================
|
||||
|
||||
# Helper to update FreeCAD wiki for a macro (copies content to clipboard and opens edit page)
|
||||
wiki-update macro:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
case "{{macro}}" in
|
||||
macro-magnets|magnets|cut)
|
||||
WIKI_SOURCE="{{project_root}}/macros/Cut_Object_for_Magnets/wiki-source.txt"
|
||||
WIKI_PAGE="Macro_Cut_Object_for_Magnets"
|
||||
MACRO_NAME="Cut Object for Magnets"
|
||||
;;
|
||||
macro-export|export|multi)
|
||||
WIKI_SOURCE="{{project_root}}/macros/Multi_Export/wiki-source.txt"
|
||||
WIKI_PAGE="Macro_Multi_Export"
|
||||
MACRO_NAME="Multi Export"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown macro: {{macro}}"
|
||||
echo "Valid options: macro-magnets (or magnets, cut), macro-export (or export, multi)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
WIKI_URL="https://wiki.freecad.org/index.php?title=${WIKI_PAGE}&action=edit"
|
||||
|
||||
echo "=========================================="
|
||||
echo "FreeCAD Wiki Update Helper"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Macro: $MACRO_NAME"
|
||||
echo "Wiki Page: https://wiki.freecad.org/${WIKI_PAGE}"
|
||||
echo ""
|
||||
|
||||
# Check if wiki-source.txt exists
|
||||
if [ ! -f "$WIKI_SOURCE" ]; then
|
||||
echo "ERROR: Wiki source file not found: $WIKI_SOURCE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract current version from wiki-source.txt
|
||||
CURRENT_VERSION=$(grep -o '|Version=[^|]*' "$WIKI_SOURCE" | cut -d= -f2 | tr -d '\n')
|
||||
CURRENT_DATE=$(grep -o '|Date=[^|]*' "$WIKI_SOURCE" | cut -d= -f2 | tr -d '\n')
|
||||
|
||||
echo "Current version in wiki-source.txt:"
|
||||
echo " Version: $CURRENT_VERSION"
|
||||
echo " Date: $CURRENT_DATE"
|
||||
echo ""
|
||||
|
||||
# Try to copy to clipboard (platform-specific)
|
||||
COPIED=false
|
||||
if command -v pbcopy &> /dev/null; then
|
||||
# macOS
|
||||
cat "$WIKI_SOURCE" | pbcopy
|
||||
COPIED=true
|
||||
echo "Content copied to clipboard (macOS pbcopy)"
|
||||
elif command -v xclip &> /dev/null; then
|
||||
# Linux with xclip
|
||||
cat "$WIKI_SOURCE" | xclip -selection clipboard
|
||||
COPIED=true
|
||||
echo "Content copied to clipboard (xclip)"
|
||||
elif command -v xsel &> /dev/null; then
|
||||
# Linux with xsel
|
||||
cat "$WIKI_SOURCE" | xsel --clipboard --input
|
||||
COPIED=true
|
||||
echo "Content copied to clipboard (xsel)"
|
||||
elif command -v wl-copy &> /dev/null; then
|
||||
# Wayland
|
||||
cat "$WIKI_SOURCE" | wl-copy
|
||||
COPIED=true
|
||||
echo "Content copied to clipboard (wl-copy)"
|
||||
else
|
||||
echo "NOTE: No clipboard utility found (pbcopy, xclip, xsel, wl-copy)"
|
||||
echo " You'll need to manually copy the content."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "INSTRUCTIONS"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "1. The wiki edit page will open in your browser"
|
||||
echo "2. Log in to your FreeCAD wiki account if prompted"
|
||||
echo "3. Select ALL content in the edit box (Ctrl+A / Cmd+A)"
|
||||
echo "4. Paste the new content (Ctrl+V / Cmd+V)"
|
||||
echo "5. Add an edit summary like: 'Update to version $CURRENT_VERSION'"
|
||||
echo "6. Click 'Show preview' to verify changes"
|
||||
echo "7. Click 'Save changes' when satisfied"
|
||||
echo ""
|
||||
|
||||
# Ask for confirmation before opening browser
|
||||
read -p "Open wiki edit page in browser? [Y/n] " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||
echo ""
|
||||
echo "Aborted. You can manually visit:"
|
||||
echo " $WIKI_URL"
|
||||
echo ""
|
||||
echo "Wiki source file location:"
|
||||
echo " $WIKI_SOURCE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Open the wiki edit page in browser (platform-specific)
|
||||
if command -v open &> /dev/null; then
|
||||
# macOS
|
||||
open "$WIKI_URL"
|
||||
elif command -v xdg-open &> /dev/null; then
|
||||
# Linux
|
||||
xdg-open "$WIKI_URL"
|
||||
elif command -v wslview &> /dev/null; then
|
||||
# WSL
|
||||
wslview "$WIKI_URL"
|
||||
else
|
||||
echo "Could not open browser automatically."
|
||||
echo "Please manually visit: $WIKI_URL"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Browser opened to: $WIKI_URL"
|
||||
if [ "$COPIED" = true ]; then
|
||||
echo ""
|
||||
echo "The wiki content is in your clipboard - ready to paste!"
|
||||
fi
|
||||
|
||||
# Show the wiki source content for a macro (for review)
|
||||
wiki-show macro:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
case "{{macro}}" in
|
||||
macro-magnets|magnets|cut)
|
||||
WIKI_SOURCE="{{project_root}}/macros/Cut_Object_for_Magnets/wiki-source.txt"
|
||||
MACRO_NAME="Cut Object for Magnets"
|
||||
;;
|
||||
macro-export|export|multi)
|
||||
WIKI_SOURCE="{{project_root}}/macros/Multi_Export/wiki-source.txt"
|
||||
MACRO_NAME="Multi Export"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown macro: {{macro}}"
|
||||
echo "Valid options: macro-magnets (or magnets, cut), macro-export (or export, multi)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "=========================================="
|
||||
echo "Wiki Source: $MACRO_NAME"
|
||||
echo "=========================================="
|
||||
echo "File: $WIKI_SOURCE"
|
||||
echo ""
|
||||
|
||||
# Show version info
|
||||
CURRENT_VERSION=$(grep -o '|Version=[^|]*' "$WIKI_SOURCE" | cut -d= -f2 | tr -d '\n')
|
||||
CURRENT_DATE=$(grep -o '|Date=[^|]*' "$WIKI_SOURCE" | cut -d= -f2 | tr -d '\n')
|
||||
echo "Version: $CURRENT_VERSION"
|
||||
echo "Date: $CURRENT_DATE"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
cat "$WIKI_SOURCE"
|
||||
|
||||
# Diff the local wiki source against the current wiki page (requires curl)
|
||||
wiki-diff macro:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
case "{{macro}}" in
|
||||
macro-magnets|magnets|cut)
|
||||
WIKI_SOURCE="{{project_root}}/macros/Cut_Object_for_Magnets/wiki-source.txt"
|
||||
WIKI_PAGE="Macro_Cut_Object_for_Magnets"
|
||||
MACRO_NAME="Cut Object for Magnets"
|
||||
;;
|
||||
macro-export|export|multi)
|
||||
WIKI_SOURCE="{{project_root}}/macros/Multi_Export/wiki-source.txt"
|
||||
WIKI_PAGE="Macro_Multi_Export"
|
||||
MACRO_NAME="Multi Export"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown macro: {{macro}}"
|
||||
echo "Valid options: macro-magnets (or magnets, cut), macro-export (or export, multi)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
WIKI_RAW_URL="https://wiki.freecad.org/index.php?title=${WIKI_PAGE}&action=raw"
|
||||
|
||||
echo "Fetching current wiki content for $MACRO_NAME..."
|
||||
echo ""
|
||||
|
||||
# Create temp file for wiki content
|
||||
TEMP_WIKI=$(mktemp)
|
||||
trap "rm -f $TEMP_WIKI" EXIT
|
||||
|
||||
# Fetch current wiki content
|
||||
if ! curl -sS "$WIKI_RAW_URL" > "$TEMP_WIKI" 2>/dev/null; then
|
||||
echo "ERROR: Could not fetch wiki page. The page may not exist yet."
|
||||
echo "URL: $WIKI_RAW_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if page exists (MediaWiki returns specific content for missing pages)
|
||||
if grep -q "There is currently no text in this page" "$TEMP_WIKI"; then
|
||||
echo "NOTE: Wiki page does not exist yet."
|
||||
echo "This will be a new page creation."
|
||||
echo ""
|
||||
echo "Local content to be uploaded:"
|
||||
echo "=========================================="
|
||||
head -20 "$WIKI_SOURCE"
|
||||
echo "..."
|
||||
echo "(truncated - run 'just release::wiki-show {{macro}}' to see full content)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Comparing local wiki-source.txt with live wiki page..."
|
||||
echo ""
|
||||
|
||||
# Show diff
|
||||
if diff -u "$TEMP_WIKI" "$WIKI_SOURCE"; then
|
||||
echo "No differences found - wiki is up to date!"
|
||||
else
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Differences found (above)"
|
||||
echo "Run 'just release::wiki-update {{macro}}' to update the wiki"
|
||||
fi
|
||||
|
||||
+189
-20
@@ -9,8 +9,12 @@ 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:
|
||||
cd {{project_root}} && uv run pytest tests/unit --cov=freecad_mcp --cov-report=term-missing --cov-report=html:{{project_root}}/htmlcov
|
||||
#!/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)
|
||||
fast:
|
||||
@@ -24,37 +28,148 @@ integration:
|
||||
verbose:
|
||||
uv run pytest {{project_root}}/tests/unit -v --tb=long
|
||||
|
||||
# Run all tests including integration (requires running FreeCAD MCP bridge)
|
||||
# Run all tests including integration (auto-starts FreeCAD headless)
|
||||
# Runs unit tests first (no FreeCAD needed), then delegates to integration-freecad-auto
|
||||
all:
|
||||
uv run pytest {{project_root}}/tests
|
||||
|
||||
# Run tests in watch mode (re-runs on file changes)
|
||||
watch:
|
||||
uv run pytest-watch {{project_root}}/tests/unit
|
||||
|
||||
# Run integration tests with automatic FreeCAD headless startup
|
||||
integration-freecad:
|
||||
#!/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
|
||||
|
||||
# Track whether we started FreeCAD (so cleanup knows to stop it)
|
||||
STARTED_FREECAD=false
|
||||
|
||||
# Helper function to kill process on a port (with fallback for systems without lsof)
|
||||
# Usage: kill_port PORT [SIGNAL]
|
||||
# Examples: kill_port 9875 (sends SIGTERM)
|
||||
# kill_port 9875 -9 (sends SIGKILL)
|
||||
kill_port() {
|
||||
local port=$1
|
||||
local signal=${2:--TERM} # Default to SIGTERM if no signal specified
|
||||
local pids=""
|
||||
|
||||
if command -v lsof &>/dev/null; then
|
||||
# Collect PIDs first, then kill only if non-empty
|
||||
pids=$(lsof -ti:"$port" 2>/dev/null || true)
|
||||
if [[ -n "$pids" ]]; then
|
||||
echo "$pids" | xargs kill "$signal" 2>/dev/null || true
|
||||
fi
|
||||
elif command -v fuser &>/dev/null; then
|
||||
# fuser -k sends SIGKILL by default; use --signal for others
|
||||
# Check if port is in use first
|
||||
if fuser "$port/tcp" 2>/dev/null; then
|
||||
if [ "$signal" = "-9" ] || [ "$signal" = "-KILL" ]; then
|
||||
fuser -k "$port/tcp" 2>/dev/null || true
|
||||
else
|
||||
fuser -k --signal "${signal#-}" "$port/tcp" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 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..."
|
||||
# Kill processes on our ports - these are the ones we started
|
||||
kill_port 9875
|
||||
kill_port 9876
|
||||
# Wait briefly for graceful shutdown
|
||||
sleep 1
|
||||
# Force kill if still running
|
||||
kill_port 9875 -9
|
||||
kill_port 9876 -9
|
||||
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 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
|
||||
just freecad::run-headless &
|
||||
FREECAD_PID=$!
|
||||
# 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 MCP bridge to start..."
|
||||
sleep 5
|
||||
|
||||
# Check if the bridge is ready
|
||||
# Check if the bridge is ready (verify XML-RPC ping, not just port open)
|
||||
MAX_RETRIES=30
|
||||
RETRY_COUNT=0
|
||||
while ! curl -s http://localhost:9875 > /dev/null 2>&1; do
|
||||
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 MCP bridge did not start within timeout"
|
||||
kill $FREECAD_PID 2>/dev/null || true
|
||||
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)"
|
||||
@@ -68,9 +183,63 @@ integration-freecad:
|
||||
TEST_EXIT_CODE=0
|
||||
uv run pytest "{{project_root}}/tests/integration" -v || TEST_EXIT_CODE=$?
|
||||
|
||||
# Stop FreeCAD
|
||||
echo ""
|
||||
echo "Stopping FreeCAD..."
|
||||
kill $FREECAD_PID 2>/dev/null || true
|
||||
|
||||
# Cleanup is handled by trap
|
||||
exit $TEST_EXIT_CODE
|
||||
|
||||
# =============================================================================
|
||||
# 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
|
||||
|
||||
# =============================================================================
|
||||
# Bridge Management
|
||||
# =============================================================================
|
||||
|
||||
# Kill any zombie FreeCAD MCP bridge processes on the default ports
|
||||
kill-bridge:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Killing any processes on MCP bridge ports (9875, 9876)..."
|
||||
|
||||
# Helper function to kill process on a port (with fallback for systems without lsof)
|
||||
# Collects PIDs first to avoid calling kill with no arguments
|
||||
kill_port() {
|
||||
local port=$1
|
||||
local pids=""
|
||||
|
||||
if command -v lsof &>/dev/null; then
|
||||
# Collect PIDs first, then kill only if non-empty
|
||||
pids=$(lsof -ti:"$port" 2>/dev/null || true)
|
||||
if [[ -n "$pids" ]]; then
|
||||
echo "$pids" | xargs kill -9 2>/dev/null || true
|
||||
fi
|
||||
elif command -v fuser &>/dev/null; then
|
||||
# Check if port is in use first
|
||||
if fuser "$port/tcp" 2>/dev/null; then
|
||||
fuser -k -9 "$port/tcp" 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
echo "Warning: Neither lsof nor fuser available, cannot kill port $port"
|
||||
fi
|
||||
}
|
||||
|
||||
kill_port 9875
|
||||
kill_port 9876
|
||||
|
||||
echo "Done."
|
||||
|
||||
Reference in New Issue
Block a user