7d4ee627bf
Cockpit (54 scripts): - Intelligence/agent TUIs, Mistral agents/studio TUI, e2e agent tests - Mesh health, sync preflight, dirtyset cleanup, SSH healthcheck - Mascarade runtime health, incident registry, dispatch mesh - Machine registry, log ops, product contract audit - Yiacad refonte/backend/proofs/operator TUIs - Sentinelle cron, daily alignment, operator summaries - Fab package, PCB AI fab, artifact WMS index TUIs Ops: - Deploy mascarade tower runtime - Full operator lane sync CAD: - KiCad yiacad plugin, host MCP integration - CAD stack scripts Other: - Mistral finetune tools - Evals framework - Mesh contract checker - Updated AI scripts (zeroclaw, integrations) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
332 lines
9.2 KiB
Bash
Executable File
332 lines
9.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
source "$ROOT_DIR/tools/lib/runtime_home.sh"
|
|
COMPOSE_FILE="$ROOT_DIR/deploy/cad/docker-compose.yml"
|
|
DOCKER_CLIENT_HOME="${HOME:-}"
|
|
VERBOSE=0
|
|
DOCTOR=0
|
|
RUNTIME="${KICAD_MCP_RUNTIME:-auto}"
|
|
FORCE_BUILD=0
|
|
REQUESTED_RUNTIME="$RUNTIME"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: tools/hw/run_kicad_mcp.sh [--doctor] [--verbose] [--debug] [--rebuild] [-- <server args>]
|
|
|
|
Launch the supported KiCad MCP server from the companion `mascarade` repo.
|
|
|
|
Options:
|
|
--doctor Print resolved paths and exit
|
|
--verbose Print resolved command before exec
|
|
--debug Enable verbose launcher + server logs
|
|
--rebuild Force a rebuild of the container image before launch
|
|
-h, --help Show this help
|
|
|
|
Environment:
|
|
MASCARADE_DIR Override the companion repo path (default: ../mascarade)
|
|
KICAD_MCP_ENTRYPOINT Override the Node entrypoint
|
|
KICAD_MCP_HOME Override the MCP runtime home (default: .cad-home/kicad-mcp)
|
|
NODE_BIN Override the Node executable (default: node)
|
|
KICAD_MCP_DATA_DIR Override the writable data directory used by the server
|
|
KICAD_MCP_RUNTIME host | container | auto (default: auto)
|
|
KICAD_MCP_LOG_LEVEL error | warn | info | debug (default: warn)
|
|
EOF
|
|
}
|
|
|
|
log() {
|
|
printf '[kill_life:mcp] %s\n' "$*" >&2
|
|
}
|
|
|
|
debug() {
|
|
if [ "$VERBOSE" -eq 1 ]; then
|
|
log "$*"
|
|
fi
|
|
}
|
|
|
|
cleanup_container() {
|
|
if [ -n "${ACTIVE_CONTAINER_NAME:-}" ]; then
|
|
docker rm -f "$ACTIVE_CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
die() {
|
|
printf '[kill_life:mcp][err] %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
compose() {
|
|
if HOME="$DOCKER_CLIENT_HOME" docker compose version >/dev/null 2>&1; then
|
|
HOME="$DOCKER_CLIENT_HOME" docker compose -f "$COMPOSE_FILE" "$@"
|
|
return
|
|
fi
|
|
|
|
if command -v docker-compose >/dev/null 2>&1; then
|
|
HOME="$DOCKER_CLIENT_HOME" docker-compose -f "$COMPOSE_FILE" "$@"
|
|
return
|
|
fi
|
|
|
|
die "docker compose support not available"
|
|
}
|
|
|
|
build_pythonpath() {
|
|
local -a candidates=()
|
|
local value=""
|
|
|
|
if [ -n "${KICAD_PYTHONPATH:-}" ]; then
|
|
value="${KICAD_PYTHONPATH}"
|
|
elif [ -n "${PYTHONPATH:-}" ]; then
|
|
value="${PYTHONPATH}"
|
|
else
|
|
candidates=(
|
|
/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages
|
|
/usr/lib/kicad/lib/python3/dist-packages
|
|
/usr/local/lib/kicad/lib/python3/dist-packages
|
|
/usr/lib/python3/dist-packages
|
|
/usr/local/lib/python3/dist-packages
|
|
/usr/lib/python3.12/dist-packages
|
|
/usr/local/lib/python3.12/dist-packages
|
|
)
|
|
for candidate in "${candidates[@]}"; do
|
|
if [ -d "$candidate" ]; then
|
|
if [ -n "$value" ]; then
|
|
value="${value}:$candidate"
|
|
else
|
|
value="$candidate"
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
resolve_probe_python() {
|
|
if [ -n "${KICAD_PYTHON:-}" ]; then
|
|
printf '%s' "$KICAD_PYTHON"
|
|
return 0
|
|
fi
|
|
|
|
if [ -x /Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3.9 ]; then
|
|
printf '%s' /Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3.9
|
|
return 0
|
|
fi
|
|
|
|
if [ -x "$SERVER_DIR/venv/bin/python" ]; then
|
|
printf '%s' "$SERVER_DIR/venv/bin/python"
|
|
return 0
|
|
fi
|
|
|
|
command -v python3 || true
|
|
}
|
|
|
|
probe_pcbnew() {
|
|
local probe_python="$1"
|
|
local pythonpath="$2"
|
|
|
|
[ -n "$probe_python" ] || return 1
|
|
|
|
if [ -n "$pythonpath" ]; then
|
|
env PYTHONPATH="$pythonpath" "$probe_python" -c 'import pcbnew' >/dev/null 2>&1
|
|
return $?
|
|
fi
|
|
|
|
"$probe_python" -c 'import pcbnew' >/dev/null 2>&1
|
|
}
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--doctor)
|
|
DOCTOR=1
|
|
shift
|
|
;;
|
|
--runtime)
|
|
[ "$#" -ge 2 ] || die "--runtime requires a value"
|
|
RUNTIME="$2"
|
|
REQUESTED_RUNTIME="$RUNTIME"
|
|
shift 2
|
|
;;
|
|
--runtime=*)
|
|
RUNTIME="${1#*=}"
|
|
REQUESTED_RUNTIME="$RUNTIME"
|
|
shift
|
|
;;
|
|
--container)
|
|
RUNTIME="container"
|
|
REQUESTED_RUNTIME="$RUNTIME"
|
|
shift
|
|
;;
|
|
--host)
|
|
RUNTIME="host"
|
|
REQUESTED_RUNTIME="$RUNTIME"
|
|
shift
|
|
;;
|
|
--verbose)
|
|
VERBOSE=1
|
|
shift
|
|
;;
|
|
--debug)
|
|
VERBOSE=1
|
|
export KICAD_MCP_LOG_LEVEL="${KICAD_MCP_LOG_LEVEL:-debug}"
|
|
export KICAD_PYTHON_STDERR_LOG_LEVEL="${KICAD_PYTHON_STDERR_LOG_LEVEL:-DEBUG}"
|
|
export KICAD_PYTHON_FILE_LOG_LEVEL="${KICAD_PYTHON_FILE_LOG_LEVEL:-DEBUG}"
|
|
shift
|
|
;;
|
|
--profile|--profile=*)
|
|
die "KiCad MCP now exposes a single stable runtime; remove --profile"
|
|
;;
|
|
--rebuild)
|
|
FORCE_BUILD=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
REPO_PARENT="$(cd "$ROOT_DIR/.." && pwd)"
|
|
export CAD_HOST_ROOT="${CAD_HOST_ROOT:-$REPO_PARENT}"
|
|
export CAD_WORKSPACE_DIR="${CAD_WORKSPACE_DIR:-$ROOT_DIR}"
|
|
export KICAD_MCP_IMAGE="${KICAD_MCP_IMAGE:-kill_life_cad-kicad-mcp:latest}"
|
|
export KICAD_MCP_LOG_LEVEL="${KICAD_MCP_LOG_LEVEL:-warn}"
|
|
export KICAD_PYTHON_STDERR_LOG_LEVEL="${KICAD_PYTHON_STDERR_LOG_LEVEL:-WARNING}"
|
|
export KICAD_PYTHON_FILE_LOG_LEVEL="${KICAD_PYTHON_FILE_LOG_LEVEL:-INFO}"
|
|
MASCARADE_DIR="$(
|
|
kill_life_resolve_mascarade_dir \
|
|
"$ROOT_DIR" \
|
|
"finetune"
|
|
)"
|
|
SERVER_DIR="$MASCARADE_DIR/finetune/kicad_mcp_server"
|
|
ENTRYPOINT="${KICAD_MCP_ENTRYPOINT:-$SERVER_DIR/dist/index.js}"
|
|
NODE_BIN="${NODE_BIN:-node}"
|
|
if [ -n "${KICAD_MCP_HOME:-}" ] && [ -z "${KILL_LIFE_RUNTIME_HOME:-}" ]; then
|
|
export KILL_LIFE_RUNTIME_HOME="$KICAD_MCP_HOME"
|
|
fi
|
|
kill_life_runtime_home_init "$ROOT_DIR" "kicad-mcp" "$ROOT_DIR/.cad-home"
|
|
MCP_HOME="$HOME"
|
|
CONFIG_HOME="$XDG_CONFIG_HOME"
|
|
CACHE_HOME="$XDG_CACHE_HOME"
|
|
DATA_DIR="${KICAD_MCP_DATA_DIR:-$MCP_HOME/data}"
|
|
PYTHONPATH_VALUE="$(build_pythonpath)"
|
|
PROBE_PYTHON="$(resolve_probe_python)"
|
|
HOST_PCBNEW_STATUS="missing"
|
|
HOST_ENTRYPOINT_STATE="missing"
|
|
CONTAINER_STATUS="unknown"
|
|
|
|
if probe_pcbnew "$PROBE_PYTHON" "$PYTHONPATH_VALUE"; then
|
|
HOST_PCBNEW_STATUS="ok"
|
|
fi
|
|
|
|
if [ -f "$ENTRYPOINT" ]; then
|
|
HOST_ENTRYPOINT_STATE="present"
|
|
fi
|
|
|
|
if command -v docker >/dev/null 2>&1 && [ -f "$COMPOSE_FILE" ]; then
|
|
CONTAINER_STATUS="available"
|
|
else
|
|
CONTAINER_STATUS="missing"
|
|
fi
|
|
|
|
case "$RUNTIME" in
|
|
auto)
|
|
if [ "$HOST_PCBNEW_STATUS" = "ok" ] && [ "$HOST_ENTRYPOINT_STATE" = "present" ]; then
|
|
RUNTIME="host"
|
|
else
|
|
RUNTIME="container"
|
|
fi
|
|
;;
|
|
host|container)
|
|
;;
|
|
*)
|
|
die "invalid runtime: $RUNTIME"
|
|
;;
|
|
esac
|
|
|
|
if [ "$DOCTOR" -eq 1 ]; then
|
|
cat <<EOF
|
|
ROOT_DIR=$ROOT_DIR
|
|
MASCARADE_DIR=$MASCARADE_DIR
|
|
SERVER_DIR=$SERVER_DIR
|
|
ENTRYPOINT=$ENTRYPOINT
|
|
NODE_BIN=$NODE_BIN
|
|
KICAD_MCP_HOME=$MCP_HOME
|
|
XDG_CONFIG_HOME=$CONFIG_HOME
|
|
XDG_CACHE_HOME=$CACHE_HOME
|
|
KICAD_MCP_DATA_DIR=$DATA_DIR
|
|
PROBE_PYTHON=$PROBE_PYTHON
|
|
KICAD_PYTHONPATH=$PYTHONPATH_VALUE
|
|
HOST_PCBNEW_IMPORT=$HOST_PCBNEW_STATUS
|
|
ENTRYPOINT_STATE=$HOST_ENTRYPOINT_STATE
|
|
CONTAINER_STATUS=$CONTAINER_STATUS
|
|
KICAD_MCP_IMAGE=$KICAD_MCP_IMAGE
|
|
KICAD_MCP_LOG_LEVEL=$KICAD_MCP_LOG_LEVEL
|
|
KICAD_PYTHON_STDERR_LOG_LEVEL=$KICAD_PYTHON_STDERR_LOG_LEVEL
|
|
REQUESTED_RUNTIME=$REQUESTED_RUNTIME
|
|
SELECTED_RUNTIME=$RUNTIME
|
|
EOF
|
|
exit 0
|
|
fi
|
|
|
|
[ -d "$MASCARADE_DIR" ] || die "companion repo not found: $MASCARADE_DIR"
|
|
[ -n "$PROBE_PYTHON" ] || die "no Python executable found for KiCad MCP runtime"
|
|
|
|
kill_life_runtime_home_ensure
|
|
mkdir -p "$DATA_DIR"
|
|
|
|
export HOME="$MCP_HOME"
|
|
export XDG_CONFIG_HOME="$CONFIG_HOME"
|
|
export XDG_CACHE_HOME="$CACHE_HOME"
|
|
export KICAD_MCP_DATA_DIR="$DATA_DIR"
|
|
if [ -n "$PYTHONPATH_VALUE" ]; then
|
|
export KICAD_PYTHONPATH="$PYTHONPATH_VALUE"
|
|
fi
|
|
|
|
if [ "$RUNTIME" = "host" ]; then
|
|
command -v "$NODE_BIN" >/dev/null 2>&1 || die "node executable not found: $NODE_BIN"
|
|
[ -f "$ENTRYPOINT" ] || die "MCP entrypoint missing: $ENTRYPOINT (run npm build in mascarade/finetune/kicad_mcp_server)"
|
|
probe_pcbnew "$PROBE_PYTHON" "${KICAD_PYTHONPATH:-}" || die "pcbnew is not importable from $PROBE_PYTHON. Install KiCad with Python bindings, set KICAD_PYTHON/KICAD_PYTHONPATH, or use --container."
|
|
debug "exec host $NODE_BIN $ENTRYPOINT $*"
|
|
exec "$NODE_BIN" "$ENTRYPOINT" "$@"
|
|
fi
|
|
|
|
command -v docker >/dev/null 2>&1 || die "docker is required for container runtime"
|
|
[ -f "$COMPOSE_FILE" ] || die "compose file not found: $COMPOSE_FILE"
|
|
|
|
if [ "$FORCE_BUILD" -eq 1 ] || ! docker image inspect "$KICAD_MCP_IMAGE" >/dev/null 2>&1; then
|
|
log "Building kicad-mcp container (KiCad v10 via kicad/kicad:nightly-full)"
|
|
compose build kicad-mcp >&2
|
|
fi
|
|
|
|
ACTIVE_CONTAINER_NAME="${KICAD_MCP_CONTAINER_NAME:-kill-life-kicad-mcp-$(id -u)-$$}"
|
|
trap cleanup_container EXIT INT TERM
|
|
|
|
debug "exec container kicad-mcp $*"
|
|
docker run --rm -i --init \
|
|
--name "$ACTIVE_CONTAINER_NAME" \
|
|
--user "$(id -u):$(id -g)" \
|
|
--workdir "$CAD_WORKSPACE_DIR" \
|
|
-v "$CAD_HOST_ROOT:$CAD_HOST_ROOT" \
|
|
-e HOME="$MCP_HOME" \
|
|
-e XDG_CONFIG_HOME="$CONFIG_HOME" \
|
|
-e XDG_CACHE_HOME="$CACHE_HOME" \
|
|
-e KICAD_MCP_DATA_DIR="$DATA_DIR" \
|
|
-e KICAD_MCP_LOG_LEVEL="$KICAD_MCP_LOG_LEVEL" \
|
|
-e KICAD_PYTHON_STDERR_LOG_LEVEL="$KICAD_PYTHON_STDERR_LOG_LEVEL" \
|
|
-e KICAD_PYTHON_FILE_LOG_LEVEL="$KICAD_PYTHON_FILE_LOG_LEVEL" \
|
|
"$KICAD_MCP_IMAGE" \
|
|
sh -lc 'set -e; mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$KICAD_MCP_DATA_DIR"; exec node /opt/kicad-mcp/server/dist/index.js "$@"' sh "$@"
|
|
status=$?
|
|
trap - EXIT INT TERM
|
|
cleanup_container
|
|
exit $status
|